上一节中我们探索一下Objc
和LLVM
源码底层关于alloc的实现过程,并了解了如何来标识记录类的各种状态,总结并解释了提出的相关问题,这节我们继续看下alloc
后续关于内存开辟、内存对齐以及类绑定的问题。
一、影响对象内存大小的因素
在alloc执行后,代码会调用_objc_rootAllocWithZone
->_class_createInstanceFromZone
来进行内存空间的开辟,我们在源码中先输出一下对象的内存大小
和对象在系统中实际开辟的内存大小
;
1、LGPerson类没有属性和方法时
初始情况下内存大小的情况
初始时,默认只有isa占用8个字节
;(对象实际大小时按照16字节对齐,下面讲解),所以输出 8 、8、16;
2、LGPerson类声明四个属性时
属性占用字节分别为1、4、8、8,还有默认的isa 8个字节,根据结构体内存对齐原则(基础知识应该很熟吧),输出分别为8、32、32;
添加属性时,成员变量的大小和系统开辟的大小都增加了
3、LGPerson类属性换成成员变量时
和属性输出是一样的,如果把成员变量的顺序换一下
4、LGPerson类中成员变量的顺序调换
神奇的一幕出现了,调换成员变量的顺序后,成员变量的大小
和系统开辟的大小
都发生了变化,输出变成了8、40、48;
这是因为根据结构体中顺序决定了内存对齐
,(而实际的对象大小是以16字节对齐返回)。
5、LGerson类调换属性的顺序并添加两个方法
改变属性顺序和添加方法,都没有对内存的大小产生影响
我们从上面5种情况我们可以推断出影响对象内存大小的因素
是: 属性(或者成员变量)的个数
和成员变量的顺序
;那接下来我们分析一下源码是如何实现的。
函数说明
sizeof
计算数据类型或者指针大小,p
指针为8
字节;class_getInstanceSize
类中成员变量的大小,在类结构体class_ro_t中instanceSize
字段来记录(后面详细分析);malloc_size
计算实际向系统申请开辟的内存空间大小,遵循16
字节对齐原则;
小结: 系统底层针对
属性
做了内存对齐的优化,无论属性顺序如何变化,都会按照最优的对齐方式对齐; 而成员变量的对齐并不会进行优化处理;所以日常开发中我们尽量使用属性,不直接使用成员变量,节约内存;
二、_class_createInstanceFromZone源码分析
源码如下:
仔细分析上面源码,总结就是干了3件事:
instanceSize
计算成员变量的内存大小;calloc/malloc_zone_calloc
开辟内存空间大小(这个空间就是实例对象的空间大小)(默认执行calloc,因为参数默认传nil);initIsa/initInstanceIsa
关联isa和class;(默认执行initInstanceIsa,fast标识是否需要开启nonpointter,实例对象是isa_t联合体,类对象是isa指针) 接下来我们来看具体实现:
三、instanceSize内存计算
1、instanceSize
方法
底层代码会读取cache(缓存)中的fastInstaceSize
大小(存储在cache的_flags中
,在上节中我们说过cache中的_flags
第3-12位来存储instanceSize大小);
因为在编译阶段成员变量大小已经确定,存放在MachO文件中;当类加载时,会在realizeClassWithoutSwift
->reconcileInstanceVariables
计算父类的成员变量
的大小,并修改class_ro_t中的起始大小(默认isa指针大小)和instanceSize大小;
在老版本的源代码中,是没有缓存的,在instanceSize方法中执行的是 读取缓存下面的代码。
size_t size = alignedInstanceSize() + extraBytes;
if (size < 16) size = 16;
return size; alignedInstanceSize()方法中
word_align
方法是8字节对齐算法(如上图标注部分);
2、cache.fastInstaceSize
方法
读取_flags
中缓存的成员变量的大小,【 在上节中我们说过cache中的_flags
第3-12位来存储instanceSize大小,最小可存储8字节(1000),最大存储4M(1 0000 0000 0000, 1024*1024 *4字节)】,align16
方法就是16字节对齐算法,所以对象的实际大小,以16字节对齐返回
。
下图 是类加载时如何缓存instanceSize大小:
在realizeClassWithoutSwift方法中
加上父类的instanceSize大小后,调用setinstanceSize
方法缓存记录;
在reconcileInstanceVariables方法中: 如果父类的成员变量大小 大于 起始的8个字节(所有对象都有默认的isa起始8个字节),则说明父类有其它的成员变量,调用moveIvars修改成员变量内存大小;
计算父类的成员变量大小,首先要减去起始默认的isa的字节(所有对象都有默认的isa起始8个字节);然后再8字节对齐;最后修改起始字节数和instanceSize大小;
小结:
在获取instanceSize大小时,成员变量大小(排序有关)都是以8字节对齐的;
而实际返回的对象大小是以16字节对齐的;
一个对象的占用内存大小 最小为8字节,最大为4M;
关于字节对齐word_align和align16算法???
四、calloc/malloc_zone_calloc内存开辟
id obj;
if (zone) {
obj = (id)
malloc_zone_calloc
((malloc_zone_t *)zone, 1,size
);} else {
obj = (id)
calloc
(1,size
);}
在开辟内存空间时,size大小就是返回对象的实际大小(以16字节对齐),我们通过跳转定义
看到calloc/malloc_zone_calloc
源码在malloc库中
我们在苹果的开发社区看到(malloc下载地址)
最新的源码版本为317.40.8,我们下载探究一下:
在源码中calloc 其实就是调用的是malloc_zone_calloc
方法,传入了一个默认的default_zone参数(初始化的一个malloc_zone_t结构体);
中间查找过程省略。。。
直接贴结果函数_nano_malloc_check_clear
在segregated_size_to_fit
对开辟的空间大小再次用16字节对齐验证;代码如下:
宏定义NANO_REGIME_QUANTA_SIZE
为16,SHIFT_NANO_QUANTUM
为4,算法公式为(32+(16-1))>> 4 << 4
, 此公式为16字节对齐计算公式。
五、initIsa/initInstanceIsa关联isa
分别看下initInstanceIsa
和initIsa
底层代码
可以看到底层都是调用了initIsa函数
只不过参数不同而已;第一个参数为当前类,第二个参数为isa是否开启nonpointer(类对象的isa为指针,实例对象的isa为isa_t联合体),第三个参数为是否有默认的.cxx_destruct 析构方法的实现。
初始化isa,当nonpointer为flase时,说明不需要开启nonpointer优化,那么isa指针直接指向了class类;
当nonpointer为true时,说明需要开启nonpointer优化,那么给isa_t位域赋值并指向class类;然后给objc_object(对象)中的isa赋值绑定;
最后开辟的空间对象obj指向了初始化的对象,完成和类的关联;
六、疑问解答
1、class_getInstanceSize函数
源码如下:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
看到这个 alignedInstanceSize
方法有没有很熟悉,方法中word_align
方法是对获取的instanceSize大小以8字节对齐;获取的是对象中成员变量占用的内存大小
,如果此时内存大小恰好也是16字节对齐,那么获取的instanceSize也是对象的大小。
2、字节对齐
word_align
和align16
字节对齐算法,其中以8字节对齐为例,用两种算法公式来说明:
(size + 7) >> 3 << 3
假设
size
等于6,6 + 7 = 13
==>二进制为0000 1101
>>3
==> 二进制为0000 0001
;<<3
==> 二进制为0000 1000
转换为
10机制
就是8
算法核心是
把二进制的最后三位置为0
。
(size + 7) & ~7
假设
size
等于10
,10 + 7 = 17
,0001 0001
~7
:7
取反,1111 1000
0001 0001
&1111 1000
=0001 0000
转换
10进制
为16
算法核心同样是
把二进制的最后三位置为0
。
总结:
- 对象中成员变量(结构体内部)采用
8
字节对齐;- 对象与对象在堆内存中采用
16
字节对齐;- 系统会对属性进行内存对齐优化,尽量不直接使用成员变量;
- malloc_size获取对象实际开辟了内存空间大小;