第2课-OC对象原理上-1
[TOC]
1.1 alloc对象的指针地址和内存
首先我们看下面代码的执行
ZBPerson *p1 = [ZBPerson alloc];
ZBPerson *p2 = [p1 init];
ZBPerson *p3 = [p1 init];
NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);
打印如下:
2022-03-18 19:49:27.611250+0800 001-alloc&init探索-Demo[86438:3286512] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e58
2022-03-18 19:49:27.611430+0800 001-alloc&init探索-Demo[86438:3286512] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e50
2022-03-18 19:49:27.611564+0800 001-alloc&init探索-Demo[86438:3286512] <ZBPerson: 0x6000027c0360>-0x6000027c0360-0x7ff7b43f2e48
通过以上代码可以得到:
- p1执行alloc之后在堆上分配了一块内存,0x6000027c0360,p1指向该内存地址的对象
- p1,p2,p3指针相同,指向同一个对象
- 在栈上连续分配了3块内存用来保存变量p1,p2,p3
那么接下来我们探索alloc函数底层具体执行了什么?那我们怎么探索底层源码呢?
1.2 底层探索的三种方法
1.2.1 符号断点
在探索之前,我们先了解一下Xcode的符号断点符号断点(Symbolic Breakpoint)
Symbolic Breakpoint为符号断点,可以针对某一个方法(函数)设置断点并暂停执行;有时候,我们并不清楚会在什么情况下调用某一个函数,那我们可以通过符号断点来跟踪获取调用该函数的程序堆栈
Xcode中添加符号断点如下
添加之后,会弹出如下设置页面
各字段含义如下: Symbol:填入你想设置断点的方法 (例如:-[NSException raise],-号是实例方法,+号是类方法)。 你也可以输入:objc_exception_throw malloc_error_break //跟踪调试释放了2次的对象 -[NSObject doesNotRecognizeSelector:] //向某个object发送没有的方法
Module:填入要设置断点的方法或函数是否在位于dylib中,默认不填。
Conditon:填入条件,例如:(BOOL)[item isEqualToString:@“test”]前面的(BOOL)是必须的。否则console会提示类型不符合,导致条件不能生效。意思是item(NSString)是test时停下。 同样可以写一下判断的方法比如用来确定类类型的isKindOfClass:,确定对象在继承体系中的位置的isMemberOfClass:,判断一个对象是否能接收某个特定消息的respondsToSelector:,判断一个对象是否遵循某个协议的conformsToProtocol:,以及提供方法实现地址的methodForSelector:等。
Ignore:忽略几次。
Action:可在程序断点执行后增加额外动作(Applescript,捕捉动画帧速,调试器命令(lldb),输入log记录,终端命令(shell),播放声音) 例如:Debugger Commond中可填入 po item 输出 item变量的值 bt 表示输出 方法调用堆栈信息
1.2.2 方式1:使用符号断点探索底层函数的执行
首先在alloc函数地方打个断点,执行alloc函数时按住ctrl点击调试小箭头,即可进入到底层函数
如果我们想要进一步了解objc_alloc函数的底层代码,那么我们打一个符号断点,查看底层代码的具体执行逻辑
接着再次执行断点我们就可以看到
objc_alloc实际会继续向下执行
_objc_rootAllocWithZone
和objc_msgSend
函数 其中上图中libobjc.a.dylib
就是OC底层的动态库,所以我们得出如下结论:
1. alloc函数的执行最后是执行libobjc.a.dylib动态库里面objc_alloc函数
如果我们还想继续查看_objc_rootAllocWithZone
和objc_msgSend
函数的底层逻辑,那么可以再将这两个函数设置符号断点,进一步查看底层代码逻辑。
1.2.3 方式2:通过汇编探索底层函数的执行
首先我们需要在断点处,打开汇编设置
接下来我们就可以看到汇编源码了
从汇编结果中我们可以看到,断点下一行 0x10b54fe0a <+58>: callq 0x10b5503c2 ; symbol stub for: objc_alloc
虽然我们看不懂汇编,但是通过call->symbol stub for: objc_alloc我们也可以大致理解,接下来函数会调用objc_alloc这个符号所代表的的函数。
接着ctrl一步步执行,最后还是会执行到OC的动态库
1.2.4 方式3:直接通过符号断点确定底层函数的执行
例如我们直接对alloc打上符号断点
执行之后可以直接得到libobjc.A.dylib
+[NSObject alloc]:`
1.2.5 混合方式:汇编结合源码调试分析
我们到苹果开源官网下载底层源码。 https://opensource.apple.com/ https://opensource.apple.com/tarballs/
我们可以搜索objc
或者通过另一个网站下载
我们这里下载objc4-818.2这个库
下载成功之后打开这个库文件
然后在源码中搜索 alloc {
可以得到
首先alloc定位到了NSObjec.mm文件,这里是一个Objective-c++汇编文件
执行alloc会执行_objc_rootAlloc
_objc_rootAlloc(Class cls) { return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/); }
接着执行callAlloc
这里OBJC2是指的OC底层2.0版本的源码,当然还有1.0版本,这里做了版本控制。
接下来的代码执行,如果不清楚到底走哪个分支,那么可以通过将callAlloc函数设置符号断点来调试查看函数接下来具体执行的内容。
// 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));
那么回到我们之前的ZBPerson断点处,我们添加一个
_objc_rootAlloc
的符号断点,再次执行其中我们使用register read指令读取寄存器的内容。
regsiter read: 读取寄存器的内容
register read x0: 读取某个寄存器的内容
通过上面的汇编代码我们可以知道函数的执行顺序为, _objc_rootAlloc
==》_objc_rootAllocWithZone
==》objc_msgSend
所以我们回过头来在看源码:
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
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));
}
所以#if __OBJC2__
分支的代码一定运行了。
1.3 源代码调试&编译优化
1.3.1 源代码编译调试
文章参照:https://juejin.cn/post/6844903959161733133
虽然通过上面的手段,我们能够检测到底层的执行函数,但是具体的实现逻辑还是不清楚。如果能够直接编译调试,像我们自己的代码直接 LLDB 调试,流程跟踪。那简直不要太爽,于是我编译了现在最新的iOS_objc4-838.1
首先下载源码库,源码库的下载上面已经讲过,这里不再重复。 下载链接:https://opensource.apple.com/releases/
编译源码库之前,你需要准备: Mac OS 12.2 Xcode 13.2 objc4-838.1
开始配置
配置好的源代码推荐: https://github.com/LGCooci/KCCbjc4_debug
问题:编译objc源码的时候遇到Xcode error: unable to find sdk 'macosx.internal'
解决方案: Build Settings -> Base SDK -> macOS
问题:文件查漏补缺
解决方案:
- 大家可以通过 Apple source 在 xnu-8019.61.5/bsd/sys/reason.h 路径自行下载
- 还可以通过谷歌中输入reason.h site:opensource.apple.com 定向检索
把找到的文件加入到工程里面。例如:
- 我在根目录创建了一个 ZBCommon 文件
- 创建 sys 文件
- 把 reason.h 文件加入进去
目前还不行,一定给我们的工程设置文件检索路径
这里之所以这么配置是因为,源代码中通过# include <sys/reason.h>
进行引用,所以我们需要在ZBC