前言
我们上篇文章讲述的是objc_msgSend在缓存中的查找(传送门),在文章最后,我们知道当消息在cache_t中找不到时,会调用_objc_msgSend_uncached方法,这个时候就会进入慢速查找。这边我们就探究下_objc_msgSend_uncached方法究竟做了些什么。
探究消息发送流程
源码查看_objc_msgSend_uncached调用过程
在源码中搜寻_objc_msgSend_uncached,我们在objc-msg-arm64.s中发现如下代码
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
发现方法MethodTableLookup方法,在汇编中发现调用了_lookUpImpOrForward方法,我们查看下_lookUpImpOrForward方法
我们发现并没有搜到想要的结果,此时说明汇编中没有该方法实现,此时搜lookUpImpOrForward看下
找到我们要的方法了,此时说明方法从汇编进入到了c++
源码查看lookUpImpOrForward过程
总览整个方法
我们看到方法比较长,牵扯的内容也很多,下面我们来理解这个方法
初始化赋值,其中给
forward_imp赋值,在汇编中调用了__objc_msgForward方法,imp初始化nil,创建curClass。我们再看下__objc_msgForward调用,直接搜搜不到想要的,所以再搜_objc_msgForward,我们在汇编中发现下面代码
我们再看下__objc_forward_handler(
当我们搜--方法搜不到合适的时候,考虑搜_方法,因为 _ _ _ 一般搜的是汇编,而_ _一般是C++).
再去看下objc_defaultForwardHandler方法
发现失败的输出文字很熟悉,它就是我们方法找不到的报错信息。
里面有+跟-的区别,说明对象方法和类方法都会走到这里。我们继续往下看
此处是无锁的缓存查找,如果找到了imp,就调用done_nolock。
这个方法就是加锁
-
锁的作用,防止多线程发生资源抢夺
runtimeLock在isrealize和isInitialized检查过程中被持有,以防止对并发实现的竞争
-
之所以添加运行时锁是因为当在缓存中搜索方法的时候(读操作),分类可以刷新缓存(写操作),同时对一块内存进行读写操作,会出问题的。
runtimeLock在方法搜索过程中保持,使方法查找+缓存填充保持原子性相对于方法添加。否则,可以添加一个类别,但是会无限期地忽略它,因为代表类别的缓存刷新之后,缓存会用旧值重新填充。
检查这个类是否被加载过,是否在缓存中
- 上面的方法是isRealized是否实现,如果未实现,就去实现。
realizeClassMaybeSwiftAndLeaveLocked方法调用会到realizeClassWithoutSwift,内部有如下代码
cls->superclass = supercls;告诉编译期cls的父类是supercls,addSubclass(supercls, cls);告诉编译期这个supercls类的子类是cls。所以这里面在进行isRealized时,将这个接受者父类一一找出,并进行了双向绑定。 - 下面的方法是isInitialized是否实现,如果没有实现,去实现。
- 让锁继续保持
- 将接受的类cls赋值给curClass,此时的cls是已经初始化过的。
下面就是一个for方法(这其实是一个for循环方法,截图将其他方法删除了,方便看)
下面我们写个简单的方法来证明这是个for循环
看到如果if的条件满足就会跳出循环。
从当前curClass(
上面将接受者cls赋值给curClass)的方法列表中找到sel方法,并返回。我们看下getMethodNoSuper_nolock具体实现,截图为主要方法
- cls->data()->methods()是通过data()方法拿到bits再通过methods()拿到方法列表。
- search_method_list_inline是查找方法,里面包含著名的二分查找算法
下面我们来看下算法怎么实现的,算法在findMethodInSortedMethodList中,我们总览整个方法
红框里就体现着二分算法思想
我们来分析下这个方法,怎么体现出来二分思想的,我们假设方法list包含0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,第一次我们找0x03,第二次我们找0x06。
- 上面我们拿到的bsae为list的第一位,也就是0x01的地址。(在高位)
- for (count = list->count; count != 0; count >>= 1) { count为8,count不为0,count大于等于1
- probe = base + (count >> 1);
此时的count为8,让count右移1位(这个右移1位就是除以2,2进制右移一位/2,左移一位*2),上面base为第一个地址位,此时probe为list首地址偏移4位。也就是0x05。 - uintptr_t probeValue = (uintptr_t)probe->name;取出的probe的name
- if (keyValue == probeValue) {意思就是如果取出来的方法名跟传进来的key(方法名sel)一样,进入。这里实不相等的,所以跳下去,我们解释下进入的方法
- 上面意思就是如果probe不是第一位,且传进来的方法名等于probe在向前偏移1位取出的值,那么就返回偏移1位的。(
这是分类同方法,优先拿执行分类的缘故)
- 上面意思就是如果probe不是第一位,且传进来的方法名等于probe在向前偏移1位取出的值,那么就返回偏移1位的。(
- 上面的0x04不是要找的,if (keyValue > probeValue) {这个要获取的sel位数是否比取到的值高,我们这是低,所以进下次循环。
上面讲完一个循环,我们继续循环,循环还是满足条件,此时的count为4,再左移一位就是2,此时base还是第一个,probe就等于首地址偏移两位,就到0x02了,不对再循环后面比对发现一致,而它下一位为为0x02,不满足,所以返回当前的probe。下面我们来查找下0x06
前面过程一样,第一次循环probe取的0x05,不相等,到if (keyValue > probeValue),发现要取的0x06是大于当前得到的0x05,所以进入这个判断,判断力让base = probe + 1;count--;过了这个判断,base就等于0x60,count变为7。再循环再/2为3,但是后面它不会偏移3,3不是偶数应该按2去2分,就会偏移1,到0x70,不满足条件了,这个时候再循环,就不会偏移直接拿到0x60了。
上面的查找过程说完了,继续下面的方法。
如果这个方法存在(
找到这个sel),这个方法找到了就会执行done方法,done方法的内调用了log_and_fill_cache,而log_and_fill_cache调用cache_fill,cache_fill调用cache->insert,将方法写入cache_t缓存。这是不在cache_t的方法写入缓存的地方。下面的父类找到方法也会执行done方法。
- 将curClass等于当前的curClass的父类,如果curClass等于nil,则进来,让imp等于forward_imp,结束
翻译意思:当attempts做--操作等于0时,就进入,提示报错。如果超类链中存在循环,则暂停。
这个方法调是查找父类的方法列表是否存在sel(
if (slowpath((curClass = curClass->superclass) == nil)) {这个判断就是将curClass指向之前curClass,所以此时的curClass就是父类) 我们进入cache_getImp看下实现,在汇编中看到如下代码
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP, _cache_getImp
LGetImpMiss:
mov p0, #0
ret
END_ENTRY _cache_getImp
- 获取isa指针后,
再次调用了CacheLookup但是参数是GETIMP,上面文章我们知道了,找不到方法会调用CheckMiss,CheckMiss在GETIMP情况下是调用LGetImpMiss。返回的0x0.打印也证实了这一点
这两个方法都不满足,所以进入下次循环,
if (slowpath((curClass = curClass->superclass) == nil))再次找父类,知道找到nil,也就是NSObject的父类,此时进入判断,让imp等于forward_imp,而forward_imp等于_objc_msgForward_impcache,跳出循环。
下面就开始进入动态方法决议
进入这个判断,sel未变,还是传入的方法名。cls也没变,还是传入的接收者。当前的cls不是元类,所以方法会进入resolveMethod_locked方法
该方法只走一次,我们看下该方法内容
探究resolveMethod_locked方法
这个方法会给最后一次机会,它会调用resolveInstanceMethod,我们看下resolveInstanceMethod方法。
总览下方法
其中有个判断:
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {这个判断是再去调用lookUpImpOrForward,这次传入的方法是resolveInstanceMethod:,cls还是接收者不变。
也就是说,给的
最后一次机会就是本类或者父类只要实现resolveInstanceMethod:这个方法就可以了,如果还没有实现,它也会走log_and_fill_cache
我们看下log_and_fill_cache方法
它会调用cache_fill,而cache_fill是会调用cache->insert,将方法resolveInstanceMethod写入cache_t,然后刷新cache_t。(这个分析,我后面再去验证下,lookUpImpOrForward方法循环很多次,有6次之多,具体还要理清,消息转发这部分很重要)
此时cls还是接收者不变,sel还是resolveInstanceMethod:,
imp是resolveInstanceMethod,receiver还是接收者。
如果未实现resolveInstanceMethod方法,它会再次通过调用lookUpImpOrNil来调用lookUpImpOrForward,传参还是之前的sel(不是resolveInstanceMethod)在进行查找一次
此时会走无锁缓存查找。
imp结果还是未找到。但还是会调用log_and_fill_cache将未实现的方法写入cache_t,进行缓存,刷新cache_t。但是imp为0x0。
再往下走就会返回nil,imp不存在,都未实现。
后面我们再次打断点,
发现sel还是之前的方法,但是对应的imp变为_objc_msgForward_impcache,也就是返回的imp为_objc_msgForward_impcache。(推断当imp返回nil的时候,方法会再次调用lookUpImpOrForward,此时的取出的imp为_objc_msgForward_impcache并返回)后续验证
验证方法流程的正确性
写如下代码:
@interface Person : NSObject
- (void)sayHello;
@end
@implementation Person
- (void)sayGood {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
[person sayHello];
}
return 0;
}
通过代码我们发现Person并未实现sayHello,所以调用肯定会报错。
提示该方法未找到。我们上面分析了,系统找不到方法会给一次机会resolveInstanceMethod:,也就是只要实现就可以,下面我们验证下。
我们运行下代码
所以我们的分析是对的。
后面我们发现lookUpImpOrForward方法sel还会有forwardingTargetForSelector以及methodSignatureForSelector这个我们下篇文章在分析。
最后
这篇文章就是消息发送查找过程,查找过程中牵扯到自己cache_t查找,methodList查找;父类的cache_t查找,methodList查找;知道找到NSObject的父类为nil位置。在说methodList查找时,我们又详细的说了下二分查找。找不到就开始进行消息转发,消息转发我们这篇文章讲的比较少,后面的文章会详细介绍消息转发过程。文章内容多,有不对的地方,希望能够指正。
最后我们补一张方法查找流程图,来结束这篇文章。