对象的本质
探寻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 查看
广告时间