前言
在分析alloc&init&new
源码之前,我们先来看看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (void)viewDidLoad { [super viewDidLoad]; // 熟悉的入手 - 对象 // alloc 做了什么? // init 做了什么? ZJPerson *p1 = [ZJPerson alloc]; ZJPerson *p2 = [p1 init]; ZJPerson *p3 = [p1 init]; ZJNSLog(@"%@ - %p - %p",p1,p1,&p1); ZJNSLog(@"%@ - %p - %p",p2,p2,&p2); ZJNSLog(@"%@ - %p - %p",p3,p3,&p3); }
查看打印结果,分别输出3个对象的内容、内存地址、指针地址
:
1 2 3 <ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6188 <ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6180 <ZJPerson: 0x600001316130> - 0x600001316130 - 0x7ffee10e6178
由上述打印结果,我们可以知道:
结论:
说明 alloc开辟了内存空间
,而 init没有开辟内存空间
p1,p2,p3指针变量指向了同一个内存空间ZJPerson
下面用一张图说明一下:
这就是本文需要探索的内容,alloc做了什么?init做了什么?,alloc&init&new到底干了什么?
准备工作
下载 objc4-781 源码
编译源码,可参考iOS-OC底层原理02:Objc4源码编译
alloc源码探索
创建一个ZJPerson自定义类,来分析一下底层调用流程
跟踪 alloc 调用流程
通过断点调试 alloc
调用 _objc_rootAlloc
1 2 3 + (id)alloc { return _objc_rootAlloc(self); }
通过断点调试 _objc_rootAlloc
调用 callAlloc
1 2 3 4 _objc_rootAlloc(Class cls) { return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); }
通过断点调试 callAlloc
调用 _objc_rootAllocWithZone
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) // alloc 源码 第三步 { #if __OBJC2__ // 有可用的编译器优化 // checkNil 为false,!cls 也为false ,所以slowpath 为 false,假值判断不会走到if里面,即不会返回nil if (slowpath(checkNil && !cls)) return nil; // hasCustomAWZ实际意义是 hasCustomAllocWithZone —— 这里表示有没有alloc / allocWithZone的实现 if (fastpath(!cls->ISA()->hasCustomAWZ())) { // 继承自NSObject/NSProxy的类才能走到这里,在oc中基本都继承自这两个类 return _objc_rootAllocWithZone(cls, nil); } #endif // No shortcuts available. // 没有可用的编译器优化 if (allocWithZone) { return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil); } return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)); }
这里补充一下 cls->ISA()->hasCustomAWZ()
是什么意思呢?
其中fastpath中的 cls->ISA()->hasCustomAWZ()
这里表示hasCustomAWZ实际意义是hasCustomAllocWithZone——这里表示有没有alloc / allocWithZone的实现(只有不是继承NSObject/NSProxy的类才为true),这里通过断点调试,是没有自定义的实现,所以会执行到 if
里面的代码,即走到 _objc_rootAllocWithZone
这里补充一下 slowpath & fastpath
是什么?
其中关于 slowpath
和 fastpath
这里需要简要说明下,这两个都是 objc
源码中定义的宏,其定义如下:
1 2 3 4 // x很可能为真, fastpath 可以简称为 真值判断 #define fastpath(x) (__builtin_expect(bool(x), 1)) // x很可能为假,slowpath 可以简称为 假值判断 #define slowpath(x) (__builtin_expect(bool(x), 0))
其中的 __builtin_expect
指令是由 gcc
引入的,
1 2 3 4 5 6 7 8 9 10 11 目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化 作用:允许程序员将最有可能执行的分支告诉编译器。 指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N 的概率很大。 fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大 slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大 在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:`Build Setting --> Optimization Level --> Debug -->` 将None 改为 fastest 或者 smallest,即编译器优化
通过断点调试 _objc_rootAllocWithZone
调用 _class_createInstanceFromZone
1 2 3 4 5 6 _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) { // allocWithZone under __OBJC2__ ignores the zone parameter return _class_createInstanceFromZone(cls, 0, nil, OBJECT_CONSTRUCT_CALL_BADALLOC); }
通过断点调试 _class_createInstanceFromZone
内部调用 cls->instanceSize、calloc、obj->initInstanceIsa
,最后返回对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, int construct_flags = OBJECT_CONSTRUCT_NONE, bool cxxConstruct = true, size_t *outAllocatedSize = nil) { ASSERT(cls->isRealized()); // Read class's info bits all at once for performance // hasCxxCtor()是判断当前class或者superclass是否有.cxx_construct 构造方法的实现 bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor(); // hasCxxDtor()是判断判断当前class或者superclass是否有.cxx_destruct 析构方法的实现 bool hasCxxDtor = cls->hasCxxDtor(); // canAllocNonpointer()是具体标记某个类是否支持优化的isa bool fast = cls->canAllocNonpointer(); size_t size; // 1.返回开辟内存大小 size = cls->instanceSize(extraBytes); if (outAllocatedSize) *outAllocatedSize = size; id obj; if (zone) { obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size); } else { // 2.开辟内存 obj = (id)calloc(1, size); } if (slowpath(!obj)) { if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) { return _objc_callBadAllocHandler(cls); } return nil; } // 3. 初始化指针关联到相应的类 if (!zone && fast) { obj->initInstanceIsa(cls, hasCxxDtor); } else { // Use raw pointer isa on the assumption that they might be // doing something weird with the zone or RR. obj->initIsa(cls); } // 4.编译器优化返回对象 if (fastpath(!hasCxxCtor)) { return obj; } construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE; return object_cxxConstructFromClass(obj, cls, construct_flags); }
cls->instanceSize
实例对象返回开辟内存的大小?
点击 instanceSize
跟踪断点去看源码来分析:
1) 调用的是下面两个函数
1 2 3 4 size_t size = _flags & FAST_CACHE_ALLOC_MASK; // remove the FAST_CACHE_ALLOC_DELTA16 that was added // by setFastInstanceSize return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
FAST_CACHE_ALLOC_MASK 下
1 2 3 #define FAST_CACHE_ALLOC_MASK 0x1ff8 #define FAST_CACHE_ALLOC_MASK16 0x1ff0 #define FAST_CACHE_ALLOC_DELTA16 0x0008
size
得到的值是 16
extra
得到的值是 0
FAST_CACHE_ALLOC_DELTA16
得到的值为 = 16 + 0 - 8 = 8
2)然后我们计算一下 16
字节对齐 align16
到底返回多少字节?
计算一下(x + size_t(15))) & ~size_t(15)的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 8 + 15 = 23 0000 0000 0001 0111 15 0000 0000 0000 1111 ~1111 1111 1111 0000 23 & ~15 0000 0000 0001 0111 1111 1111 1111 0000 ----------------------- 0000 0000 0001 0000 最后的值为16 0000 0000 0001 0000
结论:留下了16的倍数,系统返回了16字节的内存
3)为什么是16字节的对齐?
方便快捷
16字节更加安全
看起来不像 8字节
那么紧凑,并满足以后发展
calloc
1)调用下面的方法去开辟内存
1 2 // 开辟内存 obj = (id)calloc(1, size);
2)下断点到obj = (id)calloc(1, size);在这个地方打印po:
1 2 (lldb) po obj 0x000000010241e660
打印的是指针变量的地址,没有打印出对象,说明这个地方还没有返回对象
obj->initInstanceIsa
1)断点查看下面的源码
1 2 3 4 5 6 7 8 // 3.初始化指针关联到相应的类 if (!zone && fast) { obj->initInstanceIsa(cls, hasCxxDtor); } else { // Use raw pointer isa on the assumption that they might be // doing something weird with the zone or RR. obj->initIsa(cls); }
2)下断点到 obj->initInstanceIsa(cls, hasCxxDtor);
到这行打印 po
:
1 2 3 4 (lldb) po obj <ZJPerson: 0x10241e660> (lldb)
说明在这个位置才是 将初始化的指针关联到对象
3)返回对象
1 2 3 4 // 4.编译器优化返回对象 if (fastpath(!hasCxxCtor)) { return obj; }
对象开辟内存的影响因素
给对象添加属性
ZJPerson.h文件
1 2 3 4 @interface ZJPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) int age; @end
main.h文件
1 2 3 4 5 6 7 8 9 10 11 12 int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... ZJPerson * p = [ZJPerson alloc]; p.name = @"zj"; // 8 p.age = 30; // 4 NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p))); } return 0; }
分析结果:
1 2 3 4 5 6 7 8 9 10 分析结果: 1. 成员变量应该分配内存:8字节 + 4字节 = 12字节 2. 再加上isa的8字节 = 20字节 3. 根据对象8字节对齐原则最大8字节,所以20往大走补充到24字节 4. 所以内存空间应该分配24字节 5. 注意:对象开辟空间的时候成员变量就会编译进来,所以成员变量未赋值也会分配内存 6. 系统由于是16字节对齐原则,所以系统开辟的内存大小为16的倍数 = 16 * 2 = 32,而24大于16小于32,所以为32字节 实际打印结果 : 2020-09-06 15:28:31.072150+0800 ZJObjc[15774:988136] 申请内存大小为:24——-系统开辟内存大小为:32
分析内存情况
1)打断点在 ZJPerson * p = [ZJPerson alloc];
行,终端输入:
1 2 3 4 (lldb) x p 0x1006414b0: 35 22 00 00 01 80 1d 00 00 00 00 00 00 00 00 00 5".............. 0x1006414c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ (lldb)
0x1006414b0
为首地址,在iOS中内存是 小端模式
,由于没有赋值name和age所以 35 22 00 00 01 80 1d 00
后面的 00 00 00 00 00 00 00 00
是没值的
2)当我们打断点在NSLog(@”申请内存大小为…行时,终端输入:
1 2 3 4 (lldb) x/4gx p 0x1006414b0: 0x001d800100002235 0x000000000000001e 0x1006414c0: 0x0000000100001018 0x0000000000000000 (lldb)
此时,我们看到 0x000000000000001e
和 0x0000000100001018
是有值的,分别代表age=30
和name=zj
1 2 3 4 5 (lldb) po 0x000000000000001e 30 (lldb) po 0x0000000100001018 zj
init探索
查看源码:
1 2 3 4 5 6 7 8 9 10 11 - (id)init { return _objc_rootInit(self); } _objc_rootInit(id obj) { // In practice, it will be hard to rely on this function. // Many classes do not properly chain -init calls. return obj; }
由源码可知:init什么都没做,只是调用了 _objc_rootInit
重写 init
,即构造方法,给用户提供入口去实现工厂设计。
new 查看源码
1 2 3 + (id)new { return [callAlloc(self, false/*checkNil*/) init]; }
发现和 alloc
调用 callAlloc
是一个流程,即等同于 new = alloc + init
,但是有一点区别 没有自定义的构造方法
,所以一般自定义的类会用 alloc init
,系统的类用new。
总结 本人小白,很多细节问题待补充,希望多多指点。