前言
作为iOS开发者,大家都知道OC的方法本质是消息发送,那么为什么是消息发送,发送流程又是怎样的呢,今天我们就来探究一下。
准备工作
OC可编译源码工程,获取方式可参考这里。
验证方法调用本质
首先我们创建一个命令行工程,在main.m里创建一个Person类,类里面包含一个对象方法,然后再main.m里创建Person对象并调用方法。
这里对象person调用了方法sayNB,我们用clang工具把main.m编译成c++文件看一下最终是如何执行的。
打开命令行工具cd到main.m所在目录执行命令clang -rewrite-objc main.m -o main.cpp,效果如下
然后我们打开main.cpp,搜索sayNB看一下,可以看到在main函数中person调用sayNB最终转换成了objc_msgSend。
我们可以看到objc_msgSend接收的参数,第一个是消息接受者person对象,第二个参数是SEL,那么objc_msgSend是如何通过消息接受者和SEL找到方法实现并执行的呢,一起来探索一下。
消息的快速查找流程
我们回到main.m内,添加符号断点objc_msgSend,运行程序,断点进入objc_msgSend,我们可以看到 objc_msgSend的实现在libobjc.A.dylib内。
接下来我们打开准备好的源码工程,搜索objc_msgSend,好家伙,搜出来一大堆,一个一个去找太麻烦了,文件太多了,那么我们简单分析一下,消息发送在整个OC当中应当是常用的东西,他的执行效率应该非常高,所以我们可以大胆推测objc_msgSend就是用汇编实现的,而且搜索结果中有很多不同架构的msg文件,根据文件名我们也可以猜测这些文件是不同架构下,消息的实现文件。
那我们进入objc-msg-arm64.s文件,这里边是一大堆的汇编,非常的晦涩。好在苹果程序员注释写的还不错😁,我们先来看下注释写了些什么东西,
我们看到了一句有用的,证明文件没有找错,这个汇编文件就是消息发送的实现。接下来我们要找到程序的入口,好在这个汇编文件代码量不是很多,我们往下找,找到这样一段代码
这里定义了一个入口的宏,那我们搜一下这个宏应该就可以找到入口。
我们可以找到如上这样一段代码,这段代码可以看出方法查找的大体流程。这里看到在获取到isa和class之后,执行了CacheLookUp,说明方法是有缓存的,那么我们就先开看一下这个缓存具体是什么样的。
- 方法的缓存
现在在源码中全局搜索一下struct objc_class查看一下类的实现方式,找到如下代码
我们看到在类的结构体内有cache这样一个成员变量,我们点进去看一下这个结构体是不是缓存方法的,cache_t结构如下
这个结构体代码很长而且也不是很容易理解,但是我们在这个结构体上面看到了cache_getImp函数声明,并且这个函数在构体内部调用过,那么说明这个cache_t结构体应该就是缓存方法用的,那我们来调试验证一下。
似乎也看不到什么有用的信息,那么我们回到cache_t结构再看一下,发现很多关于bucket和mask什么的鬼东西,那我们就找一下看看这个bucket到底是什么,找到bucket_t结构体,进入发现如下结构
可以看到这里边有一些sel和imp的东西了,应该就是存储方法的地方,那我们再来验证一下,在lldb环境调用一下cache_t提供的buckets()函数,看看获取到的bucket_t里边有什么东西
可以看到sel和imp都是空的,我们可以猜测既然是缓存,那么至少要调用一次方法这个方法才会被缓存起来吧,那么我们就调用一次方法看看。
哇靠,还是没有,我们指针平移一下再试试
这里我们平移了两个指针位置才发现有值,通过bucket_t结构体内提供的方法查看一下,找到了我们的方法缓存的sel和imp。
- 回到objc_msgSend汇编源码
我们看下GetClassFromIsa_p16这里是如何处理的,找到这个宏定义实现,如下
在arm64架构下执行130行代码,全局搜索ExtractISA,找到如下定义
这里通过isa&ISA_MASK运算获取到类赋值给p16
继续往下执行,进入CacheLookup宏,搜一下看看这个宏里面做了什么,可以搜到一段注释解释缓存查找流程
找到实现代码如下
我们可以看到这部分操作就是通过class取出cache,然后从cache中获取到buckets,然后再通过内存平移取出bucket,这个bucket内保存的就是sel和imp。继续执行接下来的代码
我们可以看到这里执行了一个循环遍历buckets取出bucket的sel对比入参sel的过程,如果入参sel和取出的sel相同执行缓存命中代码,我们看一下缓存命中后做了哪些事
这里通过判断直接执行TailCallCachedImp,我们再来看下这里边是什么鬼东西
这里解码了一下imp,然后直接调用。到这里快速查找流程结束,说白了消息的快速查找流程其实就是在类的方法缓存里查找sel的实现然后执行查找结果imp的过程。
如果缓存没有命中时是怎么查找imp的呢,我们继续来探索一下
消息的慢速查找流程
我们看到如果缓存没有命中,执行的是MissLabelDynamic,这个鬼东西是什么,他是执行CacheLookup时传过来的参数,我看一下调用的时候传是什么,这里传入的是__objc_msgSend_uncached,,那我们找一下这个东西,最终找到实现如下
这里我们看到主要就是调用了方法查找过程,然后调用了执行过程,那我们继续搜一下看看MethodTableLookup做了什么
这里看到查找流程又跳转到_lookUpImpOrForward这里,继续搜索
找来找去也找不到实现,可能这个函数不是汇编实现的,那就去掉下划线在搜索一下看看,最终找到了这里
在这个函数末尾我们看到最终返回imp,而且这部分代码有个done标记,我们找一下看看在什么位置给imp赋值就可以了,最终找到这里
这里通过调用getMethodNoSuper_nolock函数传入当前类和要查找的sel进行查找,那再来看下这个东西的内部实现
这里是一个二维数组遍历查找的过程,将取出的一维数组传递给下一步,由search_method_list_inline找到method_t然后返回,那再来看下这个东西的实现。
这里做了一个列表有序和无序的判断,分别对应两种查找方式,我们先来看下有序查找是如何查找的,进入findMethodInSortedMethodList
这里有个M1架构电脑的对应处理,我们直接进入下一步
我们看到这里根据列表长度和传入的sel做了一个二分法查找,返回查找结果。我们再来看下无序列表是怎么查找的,进入findMethodInUnsortedMethodList
同样是一个cpu判断,继续下一步
这里就比较简单了,遍历数组对比查找过程
那我们再回到lookUpImpOrForward,看一下getMethodNoSuper_nolock执行过后做了什么
我们看到如果找到了就执行goto done跳出for循环,在done的位置执行插入缓存和返回imp,那如果getMethodNoSuper_nolock没有找到返回的是nil怎么办
这里看到如果找不到就把curClass赋值为父类继续循环查找,在父类同样优先从缓存查找,直到找不到跳出循环,将forward_imp赋值给imp
如果找不到跳出循环后在done之前执行消息动态决议过程。
到这里消息完整的查找流程执行完毕。
总结
当一个OC方法被调用,在底层会将方法调用转换成objc_msgSend,在objc_msgSend方法内部,首先通过receiver(也就是调用方法的对象)的isa指针找到这个对象的类,然后在类的方法缓存查找对应的sel,这部分使用汇编实现,大大提高了方法的执行效率,汇编查找缓存的过程其实就是根据类的缓存的内部结构进行内存的平移查找等操作,最终如果找到就调用对应的imp函数执行方法体,如过缓存找不到就调用lookUpImpOrForward进入消息的快速查找流程。
消息的快速查找流程是通过c++函数lookUpImpOrForward实现的,在这个函数内部主要是根据传入的class对象取出methodlist,然后遍历methodlist对比传入的sel,找到则返回对应的imp去执行,否则查找其父类,在查找父类时同样优先查找缓存,然后然后方法列表,不断循环查找父类,最终如果根类NSObject夜查找不到的则进入消息的动态决议流程或转发流程。