对象原理探究(一)

2019-12-27 21:27:40 浏览数 (1)

一、定位源码位置

我们要探究一个对象,那么就要找到其属性或者方法等所对应的源码。首先,我来介绍三种探索源码(即定位源码位置)的方式。

示例代码:

代码语言:javascript复制
LaVieLeader *leader1 = [LaVieLeader alloc];LaVieLeader *leader2 = [leader1 init];LaVieLeader *leader3 = [leader1 init];NSLog(@"%@, %@, %@", leader1, leader2, leader3);NSLog(@"%p, %p, %p", &leader1, &leader2, &leader3);

其打印结果:

2019-12-15 08:58:07.799176 0800 msg_send[1184:250725] <LaVieLeader: 0x281e403b0>, <LaVieLeader: 0x281e403b0>, <LaVieLeader: 0x281e403b0>

2019-12-15 08:58:07.799305 0800 msg_send[1184:250725] 0x16bdf0ee8, 0x16bdf0ee0, 0x16bdf0ed8

我们发现,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的作用主要是系统提供给我们一个接口,我们可以通过重写该方法来初始化自定义的一些属性。这其实是工厂模式的一种体现。

以上。

0 人点赞