OC底层原理之-objc_msgSend方法查找(中)

756 阅读9分钟

前言

我们上篇文章讲述的是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位的。(这是分类同方法,优先拿执行分类的缘故)
  • 上面的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查找时,我们又详细的说了下二分查找。找不到就开始进行消息转发,消息转发我们这篇文章讲的比较少,后面的文章会详细介绍消息转发过程。文章内容多,有不对的地方,希望能够指正。 最后我们补一张方法查找流程图,来结束这篇文章。