OC 是 C 语言的超集,在其之上提供了面向对象的能力。可我们面向的对象在 OC 中到底是个什么东西,或者说它在内存中如何表现呢?今天我们一起来说道说道。
对象的理解
首先来解决对象是什么的问题。先上结论:对象是类的具体体现;底层以 C 语言的结构体做为支撑;对象所占用的内存存储了结构体中的成员。
对象是类的具体体现
在面向对象中,我们使用类
来描述具有特定属性和行为的一类事物,它是一份蓝图;而对象
是蓝图的具体体现(这里是我对面向对象中类与对象的理解,并非标准定义)。面向对象的理论就说这么多,具体细节还请参考其他权威资料。
底层以 C 语言的结构体做为支撑
打开Runtime
源码,我们可以看到这样的定义:
1 |
|
从这里我们可以知道,id
是一个指向struct objc_object
的指针类型;OC 中所有继承自NSObject
类生成的对象都是struct objc_object
类型。
为什么可以这样认为?
假设现在有一个变量这样声明:XXX pa = NULL;
,此时我们只知道pa
是个XXX
类型的变量;之后你看到了typedef int* XXX;
,是不是瞬间明白了这个XXX
就是代表int*
。类比下就知道id
的场景。
紧接着,我们查看struct objc_object
结构体:
1 |
|
从这里我们可以知道,OC 中所有继承自NSObject
的类生成的对象,都具有 Class 类型的isa
成员
立马一脸问号,Class
又是什么东西?其实在查看id
类型的原始声明时,就看到了下面这句:
1 |
|
原来Class
就是一个指向struct objc_class
的指针类型。所以我们平时定义的类
也就是以struct objc_class
作为支撑。
再瞅瞅objc_class
结构体:
1 |
|
这里是一个C++
定义的结构体,可以继承以及定义方法。根据这个实现,我们可以知道:
Class
也是对象,因为它继承自objc_object
。Class
也有isa
成员,继承自objc_object
,这点很重要,在方法的调用过程时会用到。- 除了
isa
,该结构体还包含了父类指针superclass
,和该类相关联的缓存cache
以及该类的具体信息bits
。
好了,源码层面的东西先看到这里。下面再瞅瞅有趣的东西。
在main.m
中定义一个类WGCat
:
@interface WGCat : NSObject
// 这里没有任何内容
@end
@implementation WGCat
@end
注意保持
main.m
文件中只引入了Foundation
然后我们执行:
1 |
|
将.m
文件重写为.cpp
文件。然后我们找到了下面的代码:
1 |
|
可以看到NSObject_IMPL
和objc_object
如出一辙,虽然两者的名字不一样,但成员变量都只有Class isa;
。可以理解成,这两个家伙在不同范围中表达对象
这一概念。
而WGCat_IMPL
只是有一个成员,NSObject_IMPL
。因为我们当初的定义中没有成员或者属性。
接下来我们尝试在WGCat
中添加一些属性和成员变量,变成下面这样:
@interface WGCat : NSObject {
@public
int _friends;
}
@property (nonatomic, readwrite, assign) unsigned int age;
@property (nonatomic, readwrite, assign) float weight;
@end
@end
// 实现省略
之后重新rewrite
下,得到的WGCat_IMPL
变成了这样:
1 |
|
可以看到,我们写的OC
类生成的对象其实就是用结构体表示的。
对象所占用的内存存储了结构体中的成员
通过上面一节,我们了解到了对象和结构体的关系,下面我们尝试使用struct WGCat_IMPL
指针,指向WGCat
实例对象,看看能不能通过结构体的指针读取对象里面的信息,如果可以,那么就说明实例对象的内存确实存储了对应结构体的成员。看如下代码:
int main(int argc, const char * argv[]) {
WGCat *cat = [[WGCat alloc] init];
cat.age = 10;
cat.weight = 2;
cat->_friends = 3;
struct WGCat_IMPL *pointer = (__bridge struct WGCat_IMPL *)(cat);
NSLog(@"age: %u, weight: %.2f, friends: %d", pointer->_age, pointer->_weight, pointer->_friends);
// 输出 age: 10, weight: 2.00, friends: 3
pointer = NULL;
return 0;
}
可以看到通过结构体指针能正确读出对象中的信息。
但是这里任然有个疑问,系统为对象分配的内存只存储其成员变量吗?上面的输出只能说明为对象分配的内存确实存储了成员变量。但不能说明对象所暂用的内存只
存储成员变量。也就是说存在一种可能:系统分配的内存大小 大于
成员变量需要的内存。下面我们来看看一个WGCat
对象所占的内存大小是多数。
三个获取内存大小的方法:
- C 语言中
sizeof
运算符 - C 语言中
malloc_size
函数 - OC 中 Runtime
class_getInstanceSize
方法
WGCat *cat = [[WGCat alloc] init];
size_t size_from_sizeof = sizeof(struct WGCat_IMPL);
size_t size_from_runtime = class_getInstanceSize(WGCat.class);
size_t size_from_c = malloc_size((__bridge const void *)(cat));
NSLog(@"size_from_sizeof: %zd, size_from_sizeof: %zd, size_from_sizeof: %zd", size_from_sizeof, size_from_runtime, size_from_c);
// 输出
// size_from_sizeof: 24, size_from_sizeof: 24, size_from_sizeof: 32
三者的输出并不一致。
sizeof 的结果
C 语言中sizeof
运算符会返回指定类型需要内存的大小。对于结构体WGCat_IMPL
所需要的内存,我们需要知道结构体的内存对齐
这个知识点。具体参考这里
Class isa;
本质是指针,占 8 字节,偏移量为 0int _friends;
4 个字节,偏移量 8,是字节大小的整数倍。unsigned int _age;
4 个字节,偏移量 12,是字节大小的整数倍。float _weight;
4 个字节,偏移量 16,是字节大小的整数倍。- 整体对齐,全部字节
8+4+4+4=20
,最大成员变量字节大小 8,20 并不是 8 的整数倍,添加填充至 24
class_getInstanceSize 的结果
class_getInstanceSize
方法调用流程:
1 |
|
最终落脚到word_align
方法上,有一个对齐的操作:
1 |
|
这个方法会将入参转换成8
的最小倍数。本例中会将20
对齐为24
。
malloc_size 的结果
在苹果的网站上,我找到了这段说明:
The malloc_size() function returns the size of the memory block that backs the allocation pointed to by ptr. The memory block size is always
1 |
|
大致意思是:该函数会返回指针指向的内存块大小,并且该内存块大小至少等于指定分配大小,还可以大于指定分配的大小。
iOS 的内存分配最终会落脚到malloc
库中,该库中分配内存时会以 16 字节对齐,24 按 16 字节对齐,所以这里分配的内存大小就是 32 了。具体细节还未探索到,有兴趣的查看这里。
从这些信息来看,一个WGCat
对象在内存中确实占据了32
字节,但是这些内存并没有全部使用。
多问几个为什么
善于发现问题,是一种能力!多问几个为什么,你会理解的更加深入!
上面的例子只展示了类中定义属性和成员变量的情况,添加方法或其他元素是否会影响实例对象的内存大小
不会影响。
我在上面的WGCat
中实现了协议,添加了类方法,实例方法,最终rewrite
后得到的结构并没有变化,说明这些信息不会影响实例对象的内存大小。
对象所占用的内存为什么只存储实例变量
对象是类的具体体现,假设实例对象存储了诸如方法或其他信息,那么每一个实例对象都会包含这些重复的信息,属于浪费。不必将这些固定的信息存储在实例对象的内存中。那么问题又来了,类似于方法这类信息到底存储在哪里?我们下篇继续。TODO
在分配的内存多于需要的时候,多出的内存会存储其他信息吗
继续使用这个例子,在第8
行打上短点,然后输出pointer
的地址,比如我这里是0x101973650
,十进制是4321654352
。然后打开Xcode
的地址查看器(Debug->Debug Workflow->View Memory
),输入十六进制地址:
- 输入查看的地址
- 每两个十六进制代表 8bit,一个字节
pointer
指针指向的内存块,一共 32 字节,第一行isa
8 个字节_friends
成员的内存,4 字节_age
成员的内存,4 字节_weight
成员的内存,4 字节- 结构体对齐策略添加的填充,4 字节
malloc
库以 16 字节对齐添加的填充,8 字节
可以看到多出的字节(编号 8、9)并没有存储任何信息。注意 iOS 是小端模式,读取字节数据时从高地址开始。比如isa
的字节序列应该是0x001d800100002271
,_friends
的字节序列应该是0x000003
。
小考验
为了使大家更好的理解该篇的内容,留一个小小的问题:一个NSObject对象真正需要的内存时多少?系统分配的内存又是多少?
欢迎大家一起交流。再会!