iOS-底层原理08 动态方法决议(找不到IMP的处理)

831

消息查询流程

前面研究了消息查询的正常流程消息查询流程:快速消息查询和慢速消息查询。如果还没有找到改消息IMP那么会进入异常流程处理。

异常流程

  • 消息动态决议方法:resolveInstanceMethodresolveClassMethod会返回方法的IMP
  • 消息转发流程forwardingTargetForSelector,methodSigntureForSelectorforwardInvocation 如下所示是消息发送的异常流程。 未命名文件(3).png 可以看出如下
  • resolveInstanceMethod是实例方法的动态决议方法
  • resolveClassMethod是类方法(在元类中)的动态决议,如果查询不到也会查询类的实例方法resolveInstanceMethod

案例

执行一个未实现的方法,如下所示。

截屏2021-07-08 下午5.54.37.png

上图所示:代码执行后报错,而且类中的动态决议方法执行了2次

//执行了2次
+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@=======",NSStringFromSelector(sel));
    return NO;
}

查看源码中的流程

方法的执行均是通过消息的发送,然后查询该消息的实现地址进行实现。如果找不到该消息的地址,系统会有一套的异常处理机制(底层实现)。如下所示,为Objc的源码中消息查询的处理。

方法决议获取IMP

这是慢速查询后没有找到IMP的处理

//查询消息的地址,并且缓存
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    // 没有找到方法,尝试一次方法解析 单利
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //动态方法决议的控制条件,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    ...
  }

实例方法和类方法的决议入口,

  • 判断cls是否是元类
  • 不是元类,说明调用的是实例方法,会走resolveInstanceMethod
  • 如果是元类,说明调用的方法是类方法,会先调用resolveClassMethod查询元类中的类方法,如果找到了会缓存到cache。如果缓存中没有,会查询元类的实例方法,走resolveInstanceMethod
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    
    runtimeLock.unlock();
    
    if (! cls->isMetaClass()) {//实例方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    }
    else {//类方法
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //如果没有找到,在元类的对象方法中查找,类方法相当于在元类中的对象方法
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    // 快速查找和慢速查找sel对应的imp返回imp 实际上就是从缓存中取,因为前面已经缓存过了
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

实例方法查询流程

  • 查询元类中是否有resolveInstanceMethod方法,没有的话直接返回
  • 如果会发送消息调用该方法第一次执行,调用后会缓存起来(这里可以做方法交换等)
  • 再次查询是否有sel的方法缓存(lookUpImpOrNilTryCache)。经快速查询->`慢速查询。
  • 如果实现了,缓存中没有。进入lookUpImpOrForward查找到sel对应imp插入缓存,调用imp查找流程结束
  • 如果没有实现,缓存中没有。进入lookUpImpOrForward查找,sel没有查找到对应的imp,此时imp = forward_imp,动态方法决议只调用一次,此时会走done_unlock和done流程,既sel和forward_imp插入缓存,进行消息转发后还会调用一次
  • lookUpImpOrNilTryCache第三次进入lookUpImpOrForward查找,会再次进入到方法决议中(第二次执行
  • 之所以有时可以第一次和第三次执行lookUpImpOrForward可以进入动态方法决议,是因为if (slowpath(behavior & LOOKUP_RESOLVER))决定
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //只要cls的元类初始化 resolve_sel就一定实现了,因为NSObject默认实现了resolveInstanceMethod方法
    //目前是将resolveInstanceMethod方法缓存到cls的元类中
    //通过lookUpImpOrNilTryCache的数我们知道`resolve_sel`是类方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    //发送消息调用resolveInstanceMethod方法
    //通过 objc_msgSend 发送消息 接收者是cls说明是类方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //判断 resolve_sel 方法有没有实现,注意是`resolve_sel`方法
    bool resolved = msg(cls, resolve_sel, sel);//
    
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //为什么还有调用 lookUpImpOrNilTryCache 查询缓存和慢速查找呢
    //虽然 resolveInstanceMethod 方法调用了。但是里面不一定实现了sel的方法
    //所以还是再次要去查找sel对应的imp,如果没有实现就会把imp = forward_imp 插入缓存中
    //因为慢速查找流程动态决议方法已经走过了,此时imp = forward_imp走down和down_unlock
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

类方法查询

static void resolveClassMethod(id inst, SEL sel, Class cls){
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    
    //当你为实现resolveInstanceMethod的时候,此处也不会进入return
    //因为系统给resolveInstanceMethod函数默认返回NO,即默认实现了
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
    
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    //向类中发送消息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    //类方法相当于元类中的实例方法,同样去快速和慢速的查找
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    
    if (resolved  &&  PrintResolving) {
    ...
    }
}

lookUpImpOrNilTryCache方法

  • 主要作用通过behavior来控制插入缓存,不管sel对应的imp有没有实现,还有就是如果imp返回了有值那么一定是在动态方法决议中动态实现了imp
  • 最终会再次执行一次动态决议方法
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // LOOKUP_NIL = 4  没有传参数behavior = 0   0 | 4 = 4
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

首先最后一个参数默认是behavior = 0,LOOKUP_NIL = 4, behavior | LOOKUP_NIL 大于等于LOOKUP_NIL

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    //cls 是否初始化
    if (slowpath(!cls->isInitialized())) {
        // 没有初始化就去查找 lookUpImpOrForward 查找时可以初始化
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //在缓存中查找sel对应的imp
    IMP imp = cache_getImp(cls, sel);
    // imp有值 进入done流程
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    //是否有共享缓存
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.`preoptFallbackClass(), sel);
    }`
#endif
    // 缓存中没有查询到imp 进入慢速查找流程
    // behavior = 4 ,4 & 2 = 0 不会进入动态方法决议,所以不会一直循环
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    //(behavior & LOOKUP_NIL) = 4 & 4 = 1
    //LOOKUP_NIL 只是配合_objc_msgForward_impcache 写入缓存
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

动态添加方法

  • 动态添加方法后,会执行sayNB的方法。
  • 执行了一次resolveClassMethod方法,没有执行resolveInstanceMethod方法。是查询的是元类中的实例方法。而resolveInstanceMethod是类方法。
  • 实例方法查询流程:对象 -> 类 -> 父类 -> NSObject -> nil
  • 类方法查询流程:类 -> 元类 -> 根元类 -> NSObject -> nil
  • 所以,可以在NSObject的分类中的resolveInstanceMethod方法中统一进行处理,但是很影响性能。
+ (BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"%@=======",NSStringFromSelector(sel));
    if(sel == @selector(sayHappy)){
        IMP sayNBImp = class_getMethodImplementation(objc_getMetaClass("ZMTeacher"), @selector(sayKC));
        Method method = class_getInstanceMethod(objc_getMetaClass("ZMTeacher"), @selector(sayKC));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("ZMTeacher"), sel, sayNBImp, type);
    }
    return NO;
}

OOP(面向对象编程) 和 AOP (面向切面编程)的区别

如果在NSObject中统一处理resolveInstanceMethod的方法就是属于面向切面编程。

  • OOP:面向对象编程,什么人做什么什么事情,分工非常明确。对于特定的事件耦合度低,对于功能相同的地方,需要提取到特定的类,对于该类属于强依赖,耦合度高。
  • AOP:动态切入到特定类的方法内,对代码侵入性比较小。

总结

  • 方法执行,就是查询方法IMP的过程。走的是继承链。
  • 在动态方法决议中,会执行resolveInstanceMethod或者resolveClassMethod可以进行一次动态添加方法,避免崩溃。
  • 方法查询经过了快速查找,慢速查找和动态方法决议。如果还是没有找到会进入消息转发。
  • 消息转发中还会执行避免崩溃的方法forwardingTargetSelector等(在下一章)。

补充

位运算

  • &:按位与。参与运算的2数,二进制对应的位置相同为1,否则为0
  • ^:按位异或。参与运算的2数,二进制对应的位置相同为0,否则为1

找不到IMP时进入lookUpImpOrForward调用方法决议入口(下面方法)的变化。二次进入方法决议的原因

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //动态方法决议的控制条件,表示流程只走一次
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

从快速查询没有找到IMP,第一次进入lookUpImpOrForward

  • behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER = 1 | 2 = 0x01 | 0x10 = 0x11 = 3
  • behavior & LOOKUP_RESOLVER = 3 & 2 = 0x11 & 0x11 = 0x10 = 2
  • slowpath(behavior & LOOKUP_RESOLVER) = slowpath(2) = 1
  • behavior = behavior ^ LOOKUP_RESOLVER = 3^2 = 0x11^0x10 = 1

从方法决议中lookUpImpOrNilTryCache,第二次进入lookUpImpOrForward

  • behavior = 0 | 4 = 0x100 | 0x000 = 0x100 = 4
  • behavior & LOOKUP_RESOLVER = 4 & 2 = 0
  • 无法进入方法决议

方法决议结束后lookUpImpOrForwardTryCache,第三次进入lookUpImpOrForward

  • 此时behavior的值来自于第一次进入的值。behavior = 1
  • behavior & LOOKUP_RESOLVER = 1 & 2 = 0x01 & 0x11 = 0x01 = 1
  • behavior = behavior ^ LOOKUP_RESOLVER = 1^2 = 0x01^0x10 = 0
  • 再次进入方法决议