Objective-C · 2022年5月3日 0

iOS alloc 底层探索

前言

作为一名 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 方法
    没有开辟内存的功能,仅作返回操作,提供了工厂模式给用户,让用户在其内部进行操作