阅读 238

OC底层原理8之objc_msgSend(消息)的慢速查找,消息的动态协议

本章内容

  1. 本章的目的是什么
  2. lookUpImpOrForward的源码
  3. 消息的慢速查找流程
  4. 消息的动态协议,实例方法的动态协议,类方法的动态协议

本章的目的

在消息的汇编流程中,也就是消息缓存查找流程中。如果说消息并没有在缓存中命中需要查找的方法就会走 _objc_msgSend_uncached流程

大致流程是:_objc_msgSend_uncached(汇编) -> MethodTableLookup(汇编) -> lookUpImpOrForward(C/C++) -> TailCallFunctionPointer(汇编) -> 执行方法

目的:我们看这个方法的目的就是理清楚消息的查找机制中,如果说缓存没有查找到,那么他慢速的查找流程是如何的。而这个方法也包含了消息的动态协议流程(苹果在正常查找流程中给我们一次修复方法的机会)。但是如果这个流程也没找到呢?

lookUpImpOrForward

大致流程就是:

1.前期准备(如果要查找的类没有被初始化去初始化也就是注册类以及他的元类,父类和父类的元类)

2.死循环遍历类以及其父类所有的方法列表(如果没被找到就走3,找到就走4)

3.方法遍历完没被找到就给一次动态协议修复机会(找到就走返回imp,没找到就返回nil)

4.插入到消息接收者自己的类缓存里面(3流程不走4)

源码

贴出源码,尽可能备注,可以不看。苹果备注太长所以删掉

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //我们在消息未实现时候经常报一个错就是下面这个forward_imp的原因,就是提醒未找到方法的错误
    //等于说imp 指向去执行内置报错的函数。
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();
    //看看类是不是已经初始化,也就是被加载
    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }

    runtimeLock.lock();
    //看看cls是不是已经知道的类。防止程序员去做一个二进制blob,看起来像一个类但不是真正的类。
    //做了CFI攻击,希望通过合法注册。苹果的防护机制,不重要
    checkIsKnownClass(cls);
    
    //1.该方法就是 类没实现去实现,没初始化去初始化,并且包含它所有的父类,父类元类。
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    //2.死循环查找其类的方法,父类的方法列表。如果说有序的话会用二分查找算法
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
        //共享缓存,一般不走
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            // 方法查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            //如果方法被找到走 done流程
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            //当方法没查到,就看看有父类没,如果没有,imp为报错imp。跳出循环
            //这里的curClss是cls的父类了(这么说其实也不准确。因为curClss是它本身的父类了)
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }
        
        //如果父类查找流程有循环,毕竟万物NSObject。就报错停止。不重要
        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 走父类的快速查找流程
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        // 找到的话走done,将方法插入自己类缓存
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
    // 3.如果说上面循环结束也没找到,走一次消息动态协议 。
    // behavior为3,这是从汇编传过来的值 ,LOOKUP_RESOLVER为2
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

// 4.done流程,将方法插入缓存,如果共享缓存不重要,
 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
复制代码

消息的慢速查找流程

我们通过lookUpImpOrForward那一段差不多已经知道流程了。但是无非就是有一个getMethodNoSuper_nolock方法里面执行不太知道而已,其实这个也不是很重要。我们知道流程已经足以应付大部分面试了。感兴趣可以看

源码 getMethodNoSuper_nolock

源码很简单,无非就是一个遍历类的方法列表而已。而且还很短,但是我们需要注意的是methods是一个二维数组。至于为什么要这么设计,我也不晓得

static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
复制代码

源码 search_method_list_inline

这就是上面的方法进来去遍历的。看看上面获得的方法列表是无序的还是有序的。有序的就去二分查找,无序的话就只能线性搜索,可以看他的备注。(方法的获取也有点特殊,他分M1和咱们正常的系统。M1的话就是在smallList里面)

ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}
复制代码

消息的动态协议

我们如果说在以上流程都没有找到方法怎么办。苹果给了一个修复的机会就是消息动态协议(看上面lookUpImpOrForward源码的resolveMethod_locked)。但是消息的快慢转发呢?其实消息的快慢转发是在动态协议之后的。

大致流程:动态协议未找到 -> 消息快速转发 未找到 -> 消息慢速转发 未找到 -> 报错

消息动态协议最重要的两个方法分别为,实例对象的动态协议resolveInstanceMethod,和类方法的动态协议resolveClassMethod

源码 resolveMethod_locked

苹果会走这个方法去判断,消息的接收者的类是类还是元类,因为我们知道,对象方法是在类的中,类方法是在元类中存储,具体存储看这里类和元类本质文章中的补充了解。本文章不展示其具体resolveInstanceMethodresolveClassMethod可以自己查看,里面就是看看类有没有实现这两个方法,实现的话就通过objc_msgSend去调起这个方法

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // 如果cls 不是元类,就走实例动态协议方法
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 走类动态协议方法
        resolveClassMethod(inst, sel, cls);
        //这里为什么会再次执行resolveInstanceMethod?
        //因为根元类是继承NSObject的(NSProxy除外),浪费性能圆类方法和实例方法的谎
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    // 再次去尝试查找一遍,看看缓存中是不是又已经处理了
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码

示例与问题

描述:我创建一个类Person,没有去实现任何方法,创建对象p,让p去执行一个不存在的方法,毫无疑问报错。在Person类中实现实例动态协议方法,然后在动态协议方法中进行打印。

image.png

问题1:resolveInstanceMethod为什么走了两遍?

1.正常查找,2系统在经过消息转发后仍未找到就回调回来再找一遍。

(本回答不详细,实质是底层在第一遍动态协议,快慢转发都走完后没找到,会经过回调流程___forwarding___(CF库的函数)进行回调。然后在libsystem库和CF之间有一个叫_forwardStackInvocation函数进行了操作回调。具体流程需要自己先反汇编CoreFoundation库,再找到最终调用结果后可以回项目断点慢速转发方法后汇编查看)

第一遍是正常查找流程,如果说Person类里面实现了动态协议方法后就走,并且去调用。 那么第二遍呢?这时候我们需要断点然后借助方法调用栈去查看,如下图

image.png

问题2:报错信息unrecognized selector sent to instance 0x100552570...是如何来的?

看lookUpImpOrForward方法中会给forward_imp一个默认值_objc_msgForward_impcache,它其实是汇编函数。流程是:_objc_msgForward_impcache -> __objc_msgForward 获取报错地址 -> _objc_forward_handler C/C++函数,其实为objc_defaultForwardHandler -> 报错。如果说有人问起,就直接说没找到方法的话底层会给一个默认imp的报错方法指向

问题3:类动态协议为什么又要去resolveInstanceMethod中查找

看源码备注

补充

消息的动态协议,以及消息快慢转发,优点都是无侵入,对业务代码没什么损害。

其实消息的动态协议有很多致命问题,如:会记录系统很多方法(不是我们需要修复的),而且代码冗余。所以如果做AOP(面相切面编程)一定不要在此机制中进行消息转发。

文章分类
iOS
文章标签