方法的本质3_消息查找流程


本页所使用的objc runtime 756.2,来自GITHUB

1. 概念

在前文中,已经总结了方法查找的流程,今天从代码层面上继续阐述。

isa 的指向图如下所示:

消息的查找流程图

2. 方法查找流程

2.1 从业务代码分析

配置代码环境:,先从子类父类方法查找说起,先创建几个类,有:

  • 父类 Person 继承自NSObject,有方法talk

  • 子类 Student 继承自 Person,有方法 sayEnglish

  • 扩展 NSObject + sayGerman,有方法 sayGerman

具体代码如下所示

业务代码类结构

主业务代码图:

业务执行方法图

由上图可以看出,我们生成的子类Student 对象,既可以执行自己的对象方法sayEnglish,也可以执行父类方法talk,当需要执行的方法——子类和父类都没有之后,也可以执行根类扩展方法sayGerman

查找逻辑图可以初步理解为:子类——> 父类——> 父类的父类 ——>根元类(NSObject)

类方法也是类似的,就不赘述

2.2 源码分析

2.2.1 class_lookupMethodAndLoadCache3 - 慢速查找开启

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

原来上文留下的class_lookupMethodAndLoadCache3 方法,最终执行的方法是lookUpImpOrForward,继续探究一下!

lookUpImpOrForward - 查找Imp 或转发消息

乐观检查是否有缓存
// Optimistic cache lookup
if (cache) {
   imp = cache_getImp(cls, sel);
   if (imp) return imp;
}
查找类是否缓存过
    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

这里通过runtimeLock 锁住该部分内存,进行查找,执行了isKnownClass的方法,具体实现在这里

/***********************************************************************
* isKnownClass
* Return true if the class is known to the runtime (located within the
* shared cache, within the data segment of a loaded image, or has been
* allocated with obj_allocateClassPair).
**********************************************************************/
static bool isKnownClass(Class cls) {
    // The order of conditionals here is important for speed. We want to
    // put the most common cases first, but also the fastest cases
    // first. Checking the shared region is both fast and common.
    // Checking allocatedClasses is fast, but may not be common,
    // depending on what the program is doing. Checking if data segments
    // contain the address is slow, so do it last.
    return (sharedRegionContains(cls) ||
            NXHashMember(allocatedClasses, cls) ||
            dataSegmentsContain(cls));
}
在类的缓存(cache_t)找
   // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;
在类的方法列表里寻找
    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }
在父类的缓存和方法列表寻找
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
  1. 父类缓存(Cache)找

               // Superclass cache.
                imp = cache_getImp(curClass, sel);
                if (imp) {
                    if (imp != (IMP)_objc_msgForward_impcache) {
                        // Found the method in a superclass. Cache it in this class.
                        log_and_fill_cache(cls, imp, sel, inst, curClass);
                        goto done;
                    }
                    else {
                        // Found a forward:: entry in a superclass.
                        // Stop searching, but don't cache yet; call method 
                        // resolver for this class first.
                        break;
                    }
                }
    
  2. 父类方法列表找

                // Superclass method list.
                Method meth = getMethodNoSuper_nolock(curClass, sel);
                if (meth) {
                    log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                    imp = meth->imp;
                    goto done;
                }
有可能内存覆盖,再给一次查找

这里的核心方法是

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
父类也没有——查找失败——报错
    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;

但是这个_objc_msgForward_impcache 的实现可不好找,在源码里只看到了这些:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
extern void _objc_msgForward_impcache(void);

最终通过搜索查找_objc_msgForward_impcache,在汇编源码找到了类似的代码消息转发流程
得到这个叫做__objc_forward_handler 的代码块,搜索得知它继承自objc_defaultForwardHandler,

继续查找,得到最终的源码如下:

消息查找失败报错

OH MY GOD! 原来这就是传说中——让程序员捶胸顿足的方法查找失败的代码

消息查找失败报错

至此,方法查找的流程已经捋完。

3. 小结

方法查找的流程,就是在类里查找缓存与方法列表里挖掘的过程。

如上文阐述:

方法查找:缓存查找 —— 的方法列表 —— 父类的方法列表 (递归)—— 动态方法解析 —— 结束

消息转发流程


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【休闲】西湖一瞥 【休闲】西湖一瞥
逛次逛次的西湖今天过来平海路的Apple 直营店给电脑做大保健,顺便看了一眼西湖。 湖边景色依旧宜人,可惜游人寥寥。 期待一切顺利,然后恢复到游人如织的景色。
2020-03-09 李佳
下一篇 
方法的本质2_从objc_msgSend谈起 方法的本质2_从objc_msgSend谈起
方法的本质,就是消息传递… 本页所使用的objc runtime 756.2,来自GITHUB 一、引子:Runtime概念我们都知道,在运行OC代码时,类或者对象在调用方法时会用到runtime,那么,到底什么是运行时呢? 寻找一
2020-02-24 李佳
  目录