对象的本质
探寻OC对象的本质,我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。
那么一个OC对象占用多少内存呢?看完这篇文章你将了解OC/对象的内存布局和内存分配机制。
使用的代码下载
要用的工具:
- Xcode 10.2
 - gotoShell
 - linux-glibc-2.29源码
 - libmalloc源码
 
首先我们使用最基本的代码验证对象是什么?
1  | int main(int argc, const char * argv[]) {  | 
使用clang编译器编译成cpp,
执行clang -rewrite-objc main.m -o main.cpp之后生成的cpp,这个生成的cpp我们不知道是跑在哪个平台的,现在我们指定iphoeos和arm64重新编译一下。xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp,将main64.cpp拖拽到Xcode中并打开。
|clang|编译器|
|————–|——-|
|xcrun|命令|
|sdk|指定编译的平台|
|arch|arm64架构|
|-rewrite-objc|重写|
|main.m|重写的文件|
|main64.cpp|导出的文件|
|-o|导出|
command + F查找int main,找到关键代码,这就是main函数的转化成c/c++的代码:
1  | int main(int argc, const char * argv[]) {  | 
然后搜索
1  | struct NSObject_IMPL {  | 
那么这个结构体是什么呢?
其实我们Object-C编译之后对象会编译成结构体,如图所示:
那么isa是什么吗?通过查看源码得知:
1  | typedef struct objc_class *Class;  | 
class其实是一个指向结构体的指针,然后com+点击class得到:
1  | struct objc_class {  | 
class是一个指针,那么占用多少内存呢?大家都知道指针在32位是4字节,在64位是8字节。
1  | NSObject *obj=[[NSObject alloc]init];  | 
可以理解成实例对象是一个指针,指针占用8或者4字节,那么暂时假设机器是64位,记为对象占用8字节。obj就是指向结构体class的一个指针。
那么我们来验证一下:
1  | int main(int argc, const char * argv[]) {  | 
得出结果是:
1  | size:8 size2:16  | 
结论是:指针是8字节,指针指向的的内存大小为16字节。
查看源码得知[[NSObject alloc]init]的函数运行顺序是:
1  | class_createInstance  | 
1  | id  | 
这个函数前边后边省略,取出关键代码,其实size是cls->instanceSize(extraBytes)执行的结果。那么我们再看下cls->instanceSize的源码:
1  | //成员变量大小 8bytes  | 
可以通过源码注释得知:CF要求所有的objects 最小是16bytes。
class_getInstanceSize函数的内部执行顺序是class_getInstanceSize->cls->alignedInstanceSize()
查阅源码:1
2
3
4//成员变量大小 8bytes
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
所以最终结论是:对象指针实际大小为8bytes,内存分配为16bytes,其实是空出了8bytes。
验证:
在刚才 的代码打断点和设置Debug->Debug Workflow->View Memory,然后运行程序,


点击obj->view *objc得到上图所示的内存布局,从address看出和obj内存一样,左上角是16字节,8个字节有数据,8个字节是空的,默认是0.
使用lldb命令memory read 0x100601f30输出内存布局,如下图:
或者使用x/4xg 0x100601f30输出:

x/4xg 0x100601f30中4是输出4个数据,x 是16进制,后边g是8字节为单位。可以验证刚才的出的结论。
那么我们再使用复杂的一个对象来验证:1
2
3
4
5
6@interface Person : NSObject
{
	int _age;
	int _no;
}
@end
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp编译之后对应的源码是:
1  | struct NSObject_IMPL {  | 
Person——IMPL结构体占用16bytes
1  | Person *obj=[[Person alloc]init];  | 
使用代码验证:
1  | Person *obj=[[Person alloc]init];  | 
使用内存布局验证:

以十进制输出每个4字节
使用内存布局查看数据验证,Person占用16 bytes。
下边是一个直观的内存布局图:

再看一下更复杂的继承关系的内存布局:
1  | @interface Person : NSObject  | 
那小伙伴可能要说这一定是32字节,因为Person上边已经证明是16字节,Student又多了个成员变量_no,由于内存对齐,一定是16的整倍数,那就是16+16=32字节。
其实不然,Person是内存分配16字节,其实占用了8+4=12字节,剩余4字节位子空着而已,Student是一个对象,不可能在成员变量和指针中间有内存对齐的,参数和指针是对象指针+偏移量得出来的,多个不同的对象才会存在内存对齐。所以Student是占用了16字节。
那么我们来证明一下:
1  | Student *obj=[[Student alloc]init];  | 
再看一下LLDB查看的内存布局:
1  | (lldb) x/8xw 0x10071ae30  | 
可以看出来0x00000006和0x00000007就是两个成员变量的值,占用内存是16字节。
我们将Student新增一个成员变量:1
2
3
4
5
6
7
8
9
10//Student
@interface Student : Person
{
@public
	int _no;//4bytes
	int _no2;//4bytes
}
@end
@implementation Student
@end
然后查看内存布局:1
2
3
4
5(lldb) x/8xg 0x102825db0
0x102825db0: 0x001d8001000012c1 0x0000000700000006
0x102825dc0: 0x0000000000000000 0x0000000000000000
0x102825dd0: 0x001dffff8736ae71 0x0000000100001f80
0x102825de0: 0x0000000102825c60 0x0000000102825890
从LLDB可以看出来,内存变成了32字节。(0x102825dd0-0x102825db0=0x20)
我们再增加一个属性看下:1
2
3
4
5
6
7
8
9
10
11@interface Person : NSObject
{
	@public
	int _age;//4bytes 
}
@property (nonatomic,assign) int level; //4字节
@end
@implementation Person
@end
//InstanceSize:16 malloc_size:16
为什么新增了一个属性,内存还是和没有新增的时候一样呢?
因为property=setter+getter+ivar,method是存在类对象中的,所以实例Person占用的内存还是_age,_level和一个指向类的指针,最后结果是4+4+8=16bytes。
再看下成员变量是3个的时候是多少呢?看结果之前先猜测一下:三个int成员变量是12,一个指针是8,最后是20,由于内存是8的倍数,所以是24。
1  | @interface Person : NSObject  | 
为什么和我们猜测的不一样呢?
那么我们再探究一下:
实例对象占用多少内存,当然是在申请内存的时候创建的,则查找源码NSObject.mm 2306行得到创建对象函数调用顺序allocWithZone->_objc_rootAllocWithZone->_objc_rootAllocWithZone->class_createInstance->_class_createInstanceFromZone->_class_createInstanceFromZone最后查看下_class_createInstanceFromZone的源码,其他已省略,只留关键代码:
1  | id  | 
那么我们在看一下instanceSize中的实现:
1  | //对象指针的大小  | 
最后调用的obj = (id)calloc(1, size);传进去的值是24,但是结果是申请了32字节的内存,这又是为什么呢?
因为这是c函数,我们去苹果开源官网下载源码看下,可以找到这句代码:
1  | define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */  | 
看来NANO_MAX_SIZE在申请空间的时候做完优化就是16的倍数,并且最大是256。所以size = 24 ;obj = (id)calloc(1, size);申请的结果是32字节。
然后再看下Linux空间申请的机制是什么?
下载gnu资料,
得到:
1  | #ifndef _I386_MALLOC_ALIGNMENT_H  | 
在i386中是16,在其他系统中按照宏定义计算,__alignof__ (long double)在iOS中是16,size_t是8,则上面的代码简写为#define MALLOC_ALIGNMENT (2*8 < 16 ? 16:2*8)最终是16字节。
总结:
实例对象其实是结构体,占用的内存是16的倍数,最少是16,由于内存对齐,实际使用的内存为M,则实际分配内存为(M%16+M/16)*16。实例对象的大小不受方法影响,受实例变量影响。
- 学习资料下载
 - demo 查看
 
广告时间
