OC对象原理(一)

2021-10-08 15:20:03 浏览数 (1)

已过而立之年,新的里程碑,新的起点。决定再拼一把,在专业深度上再上一层楼。


一、定位源码位置

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

示例代码:

代码语言: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效率。

以上。

0 人点赞