已过而立之年,新的里程碑,新的起点。决定再拼一把,在专业深度上再上一层楼。
一、定位源码位置
我们要探究一个对象,那么就要找到其属性或者方法等所对应的源码。首先,我来介绍三种探索源码(即定位源码位置)的方式。
示例代码:
代码语言:javascript复制 LaVieLeader *leader1 = [LaVieLeader alloc];
LaVieLeader *leader2 = [leader1 init];
LaVieLeader *leader3 = [leader1 init];
NSLog(@"n%@, %@, %@", leader1, leader2, leader3);
NSLog(@"n%p, %p, %p", leader1, leader2, leader3);
NSLog(@"n%p, %p, %p", &leader1, &leader2, &leader3);
其打印结果:
<LaVieLeader: 0x600001d80520>, <LaVieLeader: 0x600001d80520>, <LaVieLeader: 0x600001d80520>
0x600001d80520, 0x600001d80520, 0x600001d80520
0x7ffee7cd40c8, 0x7ffee7cd40c0, 0x7ffee7cd40b8
我们发现,leader1、leader2和leader3的对象和对象地址的打印结果都相同,但是&leader1、&leader2和&leader3却不同。要知道为什么,就要看其源码。接下来我们就来探索一下alloc的源码。
1,直接代码中下普通断点
(1)
(2)Step Into Instruction
即摁住Control键,然后点击如下符号
(3)最终会定位到libobjc.A.dylib`objc_alloc:
需要注意的是,我们要使用真机调试才会定位到libobjc.A.dylib`objc_alloc,而使用模拟机是不能进入到libobjc.A.dylib`objc_alloc的,因为真机是arm64,而模拟机是x86,二者是不一样的。
2,下符号断点
(1)
(2)Symbolic Breakpoint
下符号断点
在Symbol里面填写符号标识,要定位哪个方法就填写哪个方法名:
(3)下完符号断点,在第一步的断点处,直接点击下一步,就会定位到libobjc.A.dylib` [NSObject alloc]:
需要注意的是,第一步的断点很重要,如果我们不在对应的位置加上普通断点,而是直接加上第二步的符号断点,那么我们就不知道定位的是哪一个对象的alloc方法。
3,通过汇编
OC是一门高级语言,最终还是会转换成能被机器识别的汇编语言。所以我们可以直接查看汇编源码来定位。
(1)允许展示汇编源码
(2)打一个普通断点
(3)运行,就会定位到第二步所打断点的汇编源码处:
(4)找到打断点方法(alloc)所对应的C方法(objc_alloc),然后打个断点:
(5)Step Into Instruction(Control Step Into)
最后会定位到libobjc.A.dylib`objc_alloc:
以上就是定位源码位置的三种方式。
二、汇编源码
前面,我们已经定位到了alloc的方法源码是在libobjc.A.dylib中,接下来我们就要找到libobjc.A.dylib这个库的源码。
苹果爸爸 开源了部分源码,我们访问如下网址就可以找到苹果所有的开源代码:
代码语言:javascript复制https://opensource.apple.com/tarballs/
我们找到objc4/文件夹,然后下载最新的objc4-756.2即可。
后面的分析,都是基于objc4-756.2源码。编译运行之后,截图如下:
由于我们是要找alloc方法的源码,而每一个OC方法都是通过大括号来实现的,所以我们全局搜索alloc {,结果如下:
然后我们依次点击对应的方法和函数,就会找到alloc方法的调用线:
alloc->_objc_rootAlloc->callAlloc,最后会找到callAlloc函数:
代码语言:javascript复制callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
alloc的流程图如下:
接下来我们按照上述方法函数调用线,来添加符号断点,如下:
然后运行程序,就会依次定位到对应的符号断点处。需要注意的是,我们先将排在后面的符号断点给关掉,然后定位到前面的符号断点处,再打开接下来的符号断点,这样的话才可以定位到我们所要研究的对象所调用的方法。
alloc方法的源码实现如下:
代码语言:javascript复制 (id)alloc {
return _objc_rootAlloc(self);
}
所以我们将断点定位到_objc_rootAlloc函数进行研究:
然后在控制台读取寄存器(register read):
所谓的寄存器,就是存储指针的容器。这里的x0、x1、x2......等,是用于程序调用的参数传递。
需要特别注意一下x0。x0在寄存器中排在第一个,所以x0是第一个参数的传递者,但同时在返回的时候也是返回值的存储地方。
我们都知道,alloc的作用是给对象申请内存,那么是如何实现的呢?使用汇编来分析确实是可以分析的,但是很难跟踪,所以并不推荐大家使用。接下来我将给大家介绍一个简洁的方法。
三、直接源码编译来分析
这个简洁的方法就是直接编译objc4源码。这里可以参考文章《iOS_objc4-756.2 最新源码编译调试》进行配置:
代码语言:javascript复制https://juejin.im/post/5d9c829df265da5ba46f49c9
好,配置完了之后,我们运行最新的objc4-756.2代码,我们定位alloc,最终会定位到如下代码:
代码语言:javascript复制if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
下面对该代码一一解说。
calloc是开辟一块内存,该内存就是一个实例对象,但是此时该实例对象的内存空间还不能和任何的类对象产生关联。
initInstanceIsa是初始化上面开辟出来的内存空间的isa指针,也就是将实例对象内存空间与其Class关联起来。
size指示应该为对象开辟多少大小的内存。
关于这个size,也就是给对象开辟的内存空间大小,有如下两个结论:
1,size必须是8字节的倍数,也就是8字节对齐。
之所以必须是8字节的倍数,主要是为了方便CPU进行内容的读取。我们设置了必须是8字节的倍数的这个规定,那么CPU就会以8字节一个单位进行读取操作,不然的话,它就不知道下一次读取该读取多大的内存,这势必将影响CPU的读取效率。但是这样做有一个弊端,也就是会浪费部分内存空间。也就是说,我们是用空间换取时间。
2,size最少是16字节。这是为了预留出一些内存空间以应对特殊情况。
四、查看内存段的存储
前面我们知道了,一个对象的内存大小是8字节的倍数,我们接下来就来看看如何读取对象的内存段。
在某处打好断点,程序跑到该断点处的时候,在编译器输出栏,进行如下输入:
x leader1 的作用是以16进制打印leader1对象的地址空间。
需要注意的一点是,这里的0x282424470是栈顶指针(即isa)的起始位置,所以 po 0x282424470 的结果就是leader1所对应的类对象LaVieLeader。
还需要注意的一点是,直接通过x leader1打印出来的地址空间是iOS小端模式,也就是说,其地址空间是反的。
除了x leader1,其实我们还可以通过x/4xg leader2来读取对象的内存空间:
x/4xg leader2的作用是:读取leader2对象的前四个单位(每一个单位是8字节)的内存空间。
五、init的作用
上面我们知道了,alloc开辟内存空间;那么init做了什么呢?
代码语言:javascript复制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有什么用呢?
init的作用主要是系统提供给我们一个接口,我们可以通过重写该方法来初始化自定义的一些属性。这其实是工厂模式的一种体现。
六、LLDB调试
控制台调试是一名高级开发工程师必须掌握的技能,我们可以通过在l'ldb控制台中输入 help 来查看lldb调试的文档:
接下来我们再看看po这个命令是干啥的:
代码语言:javascript复制(lldb) help po
Evaluate an expression on the current thread. Displays any returned value with
formatting controlled by the type's author. Expects 'raw' input (see 'help
raw-input'.)
Syntax: po <expr>
Command Options Usage:
po <expr>
'po' is an abbreviation for 'expression -O --'
可以看到,po是以当前对象的expression方法进行返回的,而有些变量的值是无法po出来的,此时就可以通过如下指令读取出来:
代码语言:javascript复制(lldb) e -f f -- 0x4050800000000000
(long) $3 = 66
lldb有很多的调试指令,熟练掌握的话,将大大提高debug效率。
以上。