iOS八股文(六)objc_msgSend之方法查找源码解析

627 阅读4分钟

Object-C动态性

说到Objcet-C的动态性,我们可以从两点来聊。首先是类的结构的动态性,静态语音类的结构确定是在编译时候,而Objcet-C从编译推迟到了运行时。另一方面就不得不提到消息发送机制了,Objcet-C中大多数函数的调用都是通过objc_msgSend来调用的。那么本文就消息解读objc_msgSend过程中最重要的一步--方法查找。

objc_msgSend参数

objc_msgSend在调用的时候有两个默认参数,第一个参数是消息的接收者,第二个参数是方法名。

这一点可以通过oc代码重写成cpp代码来证明。

int object_c_source_m() {
    OSTestObject1 *obj1 = [[OSTestObject1 alloc] init];
    [obj1 print];
    return 0;
}

重写后:

int object_c_source_m() {
    OSTestObject1 *obj1 = ((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)((OSTestObject1 *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("OSTestObject1"), sel_registerName("alloc")), sel_registerName("init"));

    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj1, sel_registerName("print"));

    return 0;
}

可以看到print的调用转化成了objc_msgSend调用并传入 objc1 print。如果方法本身有参数,会把本身的参数拼接到这两个参数后面。

在cache中快速查找

objc4中搜索 _objc_msgSend

image.png 可以看到各个架构版本的objc_msgSend的源码。我们主要以arm64为主,所有直接看objc_msg_arm64中的源码,可以看到都是汇编代码。这里我们可以思考一个问题,苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单--效率。汇编的效率是比c/c++更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高(个人愚见,欢迎评论区互喷)。另外苹果在所有的汇编方法命值钱都会用下划线开头,目的是为了防止符号冲突。

言归正传,我们先看汇编源码。

image.png 其实我们不需要看源码,只需要看注释就能知道大概流程。 这部分其实是objc_msgSend 开始到找类对像cache方法结束的流程。 首先判断receiver是否存在,以及是否是taggedPointer类型的指针,如果不是我们取出对象的isa指针(x13寄存器中),通过isa指针找到类对象(x16寄存器),然后通过CacheLookup,在类对象的cache中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached分支。

其中CacheLookup的源码如下

image.png 可以大致看看注释,不用深究汇编代码逻辑,大概应该是通过类对象内存平移找到cache,然后再获取buckets,然后再查找方法。 注释解释:如果没有找到返回NULL,查找的时候x0存放方法接收者,x1存放方法名,x16存放isa指针。

我们可以通过汇编断点来证实以上流程。 代码:

- (void)viewDidLoad {

    [super viewDidLoad];
    [self test]; //断点在这!
}

调试过程以及结果:

image.png 马上要进入objc_msgSend流程。调用objc_msgSend方法。x0中存放的是objc_msgSend的第一个参数,x1中存放的是objc_msgSend的第二个参数。

image.png 对象的首地址其实就是isa地址,把isa指针放到x13寄存器中。x13的值就是isa的指针。

image.png

isa_mask做与运算,获取类对象的指针。先在x16中,再放到x15中。

image.png 如果没有找到,直接走_objc_msgSend_uncached流程

方法类表中查找

image.png

image.png 在全局搜索loolUpImpOrForward

image.png 定位到关键代码

image.png 这一段代码可以说是非常厉害了,是我们程序员代码的楷模。第一个红框是在cache中查找,第二个红框是在方法列表中查处。这里肯定有一个疑问?之前不是站在cache中查找过了么?为什么还要再查找一次,首先是因为多线程同步问题,还有就是注意在这个循环里面curClass要继续从superClass中去找,这样逻辑统一,也需要在superClasscache中查找。

我们这一小节主要看看如何在方法列表中寻找的。 找到getMethodNoSuper_nolock的实现

image.png 继续到search_method_list_inline的实现

image.png 这里可以看fastpath代表的是大概率走这边,然后再看findMethodInSortedMethodList的这个方法名,关键字InSorted!!这里面可以大胆从字面翻译一下--从排过序的方法类表中查找方法

image.png 这里面有2个方法实现,是因为C/C++可以通过参数来区分方法,这两个方法一个是两个参数,一个是三个参数,两个参数的这个方法最终也是调用三个参数的方法的。所以直接看三个参数的实现就可以。

image.png 注意看for循环中的count的变化。count>>1 相当与 count = count / 2。再结合之前的InSorted,可以看到这是明显的二分法查找

缓存找到的方法

接下来我们回到我们loolUpImpOrForward的实现中。

image.png 找到方法然后去了done

image.png 再看log_and_fill_cache的实现。

image.png 这里cache的insert是不是上一遍详细讲过。要注意的是cls传的的是msgSend receiver 的isa指向的类对象/元类对象。也就是说,即使方法是从父类的类对象/元类对象中找到的,这个方法缓存也是存再自己的cache中的。

总结

之前忘了再哪收藏了一个不错的流程图,对这一过程总结的十分到位。

WechatIMG6404.png