前言
作为一名 iOS 开发人员对于对象初始化 alloc 方法是非常熟悉的。
今天来探索一下 alloc 底层到底做了什么?
通过汇编 探索 alloc 底层调用流程
新建一个 Student 类 断点到下面这行代码 Student *stu = [Student alloc];
注意我这边是新建的 iOS APP 项目,用的真机调试,读者也可使用 M1 电脑用模拟器调试
在此之前 需要先了解几个简单的 ARM64 汇编指令 以及 断点调试
b、bl : 跳转指令,函数调用 ret : 函数返回 ;分号 代表注释
断点调试
打开 Xcode 汇编调试 (Debug - Debug Workflow - Always Show Disassembly) 运行项目
-> 0x104925e54 <+60>: ldr x0, [x8, #0x518] 0x104925e58 <+64>: bl 0x104926468 ; symbol stub for: objc_alloc
通过上面的 bl 汇编指令发现 Student 调用 alloc 方法后实际上是执行了 objc_alloc 方法。
接下来 为 objc_alloc 方法添加符号断点(Symbolic Breakpoint --> Symbol 设为 objc_alloc) 继续调试
libobjc.A.dylib`objc_alloc: -> 0x1b3880038 <+0>: cbz x0, 0x1b3880050 ; <+24> 0x1b388003c <+4>: ldr x8, [x0] 0x1b3880040 <+8>: and x8, x8, #0xffffffff8 0x1b3880044 <+12>: ldrb w8, [x8, #0x1d] 0x1b3880048 <+16>: tbz w8, #0x6, 0x1b3880054 ; <+28> 0x1b388004c <+20>: b 0x1b3876548 ; _objc_rootAllocWithZone 0x1b3880050 <+24>: ret 0x1b3880054 <+28>: adrp x8, 211229 0x1b3880058 <+32>: add x1, x8, #0x469 ; =0x469 0x1b388005c <+36>: b 0x1b385d5c0 ; objc_msgSend
在 objc_alloc 里面有两个方法 分别是 _objc_rootAllocWithZone、objc_msgSend 通过 Step Over 一步一步往下走 我们发现断点会跳过 _objc_rootAllocWithZone 来到 objc_msgSend 这个方法。
-> 0x1b388005c <+36>: b 0x1b385d5c0 ; objc_msgSend
当断点来到 objc_msgSend 我们通过寄存器读取指令
(lldb) register read x0 //x0代表方法返回值 objc_msgSend 返回的是 Student实例 x0 = 0x0000000100c25568 (void *)0x0000000100c25540: Student (lldb) register read x1// x1返回方法名 x1 = 0x00000001e719d469 (lldb) po 0x00000001e719d469//po x1信息 8172196969 (lldb) po (char*)0x00000001e719d469//强转后发现 objc_msgSend 方法名是 alloc "alloc"
通过上面分析 我们得出 objc_msgSend 调用 alloc 方法 返回 Student对象,并且Student 继承自 NSObject ,我们继续添加符号断点 [NSObject alloc]
libobjc.A.dylib`+[NSObject alloc]: -> 0x1b385cef4 <+0>: b 0x1b387d5a8 ; _objc_rootAlloc 0x1b385cef8 <+4>: udf #0x0 0x1b385cefc <+8>: udf #0x0
按住 Control 点击 Step into 跳转到 _objc_rootAlloc 内部 点击 Step Over 一步一步往下走 断点到 _objc_rootAllocWithZone 方法
libobjc.A.dylib`_objc_rootAlloc: 0x1b387d5a8 <+0>: ldr x8, [x0] -> 0x1b387d5b8 <+16>: b 0x1b3876548 ; _objc_rootAllocWithZone 0x1b387d5bc <+20>: adrp x8, 211328 0x1b387d5c8 <+32>: b 0x1b385d5c0 ; objc_msgSend 0x1b387d5cc <+36>: udf #0x0
再按住 Control 点击 Step into 跳转到 _objc_rootAllocWithZone 内部,通过前面的汇编指令 我们找到 ret这行汇编(代表函数返回) 打上断点(点击 ret 左侧代码列数 即可) 断到这行汇编
libobjc.A.dylib`_objc_rootAllocWithZone: 0x1b3876548 <+0>: stp x20, x19, [sp, #-0x20]! 0x1b38765a0 <+88>: ldp x20, x19, [sp], #0x20 ->0x1b38765a4 <+92>: ret 0x1b38765a8 <+96>: ldr x8, [x19, #0x20]
通过寄存器读取 po xo 寄存器 我们发现 返回的是 Student 实例
(lldb) register read x0 x0 = 0x00000002807fc740 (lldb) po 0x00000002807fc740 <Student: 0x2807fc740>
通过汇编我们得出 alloc 底层大致流程:
alloc ---> objc_alloc ---> objc_msgSend ---> [NSObject alloc] ---> _objc_rootAlloc ---> _objc_rootAllocWithZone
为了验证上述流程是否准确 下面通过 objc4-838 源码来分析 alloc 流程
断点到这行代码 Student *stu = [Student alloc];
进入到 objc_alloc
id objc_alloc(Class cls) { return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/); }
然后到 callAlloc 中的 objc_msgSend方法
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { --> return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)); }
断点进入到 [NSObject alloc]
+ (id)alloc { return _objc_rootAlloc(self); }
断点到 _objc_rootAlloc
id _objc_rootAlloc(Class cls) { return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); }
再次断点到 callAlloc 并且执行到 _objc_rootAllocWithZone
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false) { if (fastpath(!cls->ISA()->hasCustomAWZ())) { --> return _objc_rootAllocWithZone(cls, nil); } }
最终 源码定位到 _class_createInstanceFromZone 方法
id _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); }
alloc 底层流程
alloc --> objc_alloc --> callAlloc --> objc_msgSend --> [NSObject alloc] --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone
思考:为什么通过汇编调试和源码调试出来的 alloc 流程有些不一样呢?
通过搜索相关资料 汇编调试的时候 编译器进行了优化 所以汇编看不到 callAlloc、_class_createInstanceFromZone 方法
关于 _class_createInstanceFromZone 方法分析 由于篇幅有限 下篇文章进行分析
接下来分析 init 方法 和 new 方法
断点进入到 [stu init] Student *stu = [Student alloc]; [stu init];
为[NSObject init] 添加符号断点后
libobjc.A.dylib`-[NSObject init]: -> 0x1b385d124 <+0>: ret 0x1b385d128 <+4>: udf #0x0
我们发现 [NSObject init] 直接 返回了
通过 寄存器 x0 x1 指令 init 内部仅做了返回操作
(lldb) register read x0 x0 = 0x0000000280074bf0 (lldb) po 0x0000000280074bf0 <Student: 0x280074bf0> (lldb) register read x1 x1 = 0x00000001e7dc0272 (lldb) po (char*)0x00000001e7dc0272 "init"
同理 查看 objc 源码 啥也没干
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
id
_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 方法?
这是一种工厂模式 可以重写 init 方法,在其内部进行相关操作 进行初始化赋值
关于 new 方法
+ (id)new { return [callAlloc(self, false/*checkNil*/) init]; }
通过源码显示 new 走了 callAlloc 方法流程,然后走了 init 方法,所以 new = alloc + init
再看一段代码
Student *stu1 = [Student alloc]; Student *stu2 = [stu1 init]; Student *stu3 = [stu1 init]; Student *stuNew = [Student alloc]; NSLog(@"%@ %p",stu1,stu1); NSLog(@"%@ %p",stu2,stu2); NSLog(@"%@ %p",stu3,stu3); NSLog(@"%@ %p",stuNew,stuNew);
输出结果
<Student: 0x283d74980> 0x283d74980 <Student: 0x283d74980> 0x283d74980 <Student: 0x283d74980> 0x283d74980 <Student: 0x283d74970> 0x283d74970
不难发现 stu1 、 stu2 和 stu3 指向同一块内存区域
总结
我们通过汇编调试和 objc4 底层源码调试 得出以下结论:
- alloc 具有开辟内存空间的作用,alloc 方法底层流程如下
alloc --> objc_alloc --> callAlloc --> objc_msgSend --> [NSObject alloc] --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone
最终调用 _class_createInstanceFromZone 方法为对象开辟内存
- init 方法
没有开辟内存的功能,仅作返回操作,提供了工厂模式给用户,让用户在其内部进行操作