上一篇文章中,我们知道了一个NSObject对象,因为只有一个isa指针,在64位环境下系统分配了16个字节,但是实际只占用8个字节。这个可以分别通过malloc_size() 和 class_getInstanceSize()方法来获取。具体可以参看:一个NSObject对象在内存中占用多少个字节?
在实际开发过程中,我们用到的类都是继承自NSObject对象,那么一个继承自NSObject对象的实例对象究竟占用多少个字节呢?我们一起来探讨一下。
首先我们先创建一个命令行函数,构建一个继承体系,Student --> Person --> NSObject,如图所示:
思考:现在Person的实例对象和Student的实例对象在64位环境下,分别占多少字节?
按照同样的方法,我们通过clang编译器将main.m函数转换成C++函数,在命令行中输入:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
得到main.cpp函数,打开后,依旧搜索Person_IMPL和Student_IMPL,得到如下结果:
通过编译后的代码,我们也看到继承是如何实现的,Person类首先将其父类NSObject的成员变量继承过来,然后加上自己的成员变量。同样的道理,Student类先将其父类Person的成员变量继承过来,然后加上自己的成员变量。
首先我们来思考Person类,因为Person类中包含两部分,第一部分是NSObject的成员变量,也就是isa指针,第二部分是一个整形变量age。系统为NSObject分配了16个字节,isa指针只占用8个字节,age变量占用4个字节。那么age变量会不会在NSObject分配的16个字节的后8个字节中呢?如果是这样子的话,Person类的实例变量也就是占用16个字节内容。
同样的道理,Student类继承自Person类,按照刚才的假设,Person类分配了16个字节大小,实际占有12个字节,还有4个字节,刚好够整形score进行存储,实际是否是这样呢?
我们通过代码来验证一下:
结果和我们猜测的一样,Person类和Student类的实例都是分配了了16个字节,实际占用16个字节大小,也就是把NSObject剩余的8个字节充分利用了起来。
当然我们也可以像上篇文章中所讲的,通过探测内存布局,从侧面进行二次验证:
从内存布局中我们可以看到,系统分配了16个字节的连续空间,其中前8个字节是isa指针指向的地方,紧接着十六进制 0A 就是我们进行的age的值,十六进制96就是score的值。
这里牵涉一个大小端的问题,在iOS开发中所用的是小端模式,所谓的小端模式就是高位放在高地址,低位放在低地址,简称大大小小。大端模式刚好相反,高位放在低地址,低位放在高地址,简称大小大小。
由于地址从左至右依次增大,age的实际数值就是 0x0000000A = 10,score的值是0x00000096 = 150。
想进一步验证0x00000096是否是score的地址,我们也可以通过lldb调试器,来判断:
首先 student的地址是0x10040f430,向后偏移12个字节,也就是0x10040f43C,使用memory write 0x10040f430 10 将0x10040f43C的值改为0x10,修改完成后,查看score的值,变成了16,完美验证了我们的猜想。
到这里,关于继承体系中实例对象的内存布局,我们已经清楚了。总结一下:
1.子类会继承父类的成员变量,然后将自己的成员变量紧挨其后;
2.当父类中有剩余空间,能够存放子类的成员变量时,将子类的成员变量放在父类所开辟的空间后面,并没有分配新的内存空间。假如父类分配的内存空间已经占用满了呢?会发生什么情况,这个问题我们留到下篇文章进行分析,会牵涉到内存对齐的知识。
3.大小端模式,iOS开发中用到的都是小端模式,即大大小小,高位放在高地址,低位放在低地址。