消息的动态决议和消息转发

在上篇文章objc_msgSend探索,我们探索了正常的方法的快速查找方法的慢速查找流程,同时我们也知道未找到imp时,会将最初定义的消息转发的imp赋值给当前imp,进行消息转发流程.今天我们就来探索一下这个消息转发流程.

_objc_msgForward

我们在上篇文章,知道如果找不到方法,会将imp赋值成_objc_msgForward_impcache,我们在源码的objc-msg-arm64.s文件中查找其汇编写的实现发现其就是调用_objc_msgForward,_objc_msgForward中三行代码,两行代码是在调用_objc_forward_handler

STATIC_ENTRY __objc_msgForward_impcache
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
复制代码

我们在objc-runtime.m中查找到

objc_defaultForwardHandler(**id** **self**, **SEL** sel){
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

void objc_setForwardHandler(void *fwd, void *fwd_stret){
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}
复制代码

_objc_forward_handler默认赋值的是我们熟悉的方法崩溃信息,并且下面有一个objc_setForwardHandler函数对其进行重新赋值.我们也看到了类方法、实例方法的崩溃信息是放在一起的,只是根据class_isMetaClass来进行判断,再次证明objc底层中并没有类方法和实例方法的区别. 上面都是这个找不到方法的imp的赋值,并没有进行调用,在lookUpImpOrForward方法中for循环赋值imp下面,我们也会找到一段代码,其只会执行一次

// No implementation found. Try method resolver once.
// 第一次进入时候传入的behavior为LOOKUP_INITIALIZE|LOOKUP_RESOLVER (0001 | 0010 = 0011)
// 0011 & 0010 = 0010 条件成立,会进入,然后重置behavior为LOOKUP_INITIALIZE 0001
// 再次调用 0001 & 0010 = 0000 则不会进入
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
复制代码

resolveMethod_locked

当所有查找方式没有找到目标imp时,系统会调用resolveMethod_locked给我们一次机会,让我们对这个错误进行补救,接下来我们查看一下其源代码

static NEVER_INLINE IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized()); // 线程安全, 防止调用两次
    runtimeLock.unlock();
    if (! cls->isMetaClass()) { // 判断是否是元类
        // 不是元类,调用resolveInstanceMethod方法
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 是元类,调用resolveClassMethod
        resolveClassMethod(inst, sel, cls);
        // 如果调用上面的方法还没有找到,尝试调用resolveInstanceMethod,
        // 原因是根据isa的继承链,根元类的父类是NSObject,所以在元类中如果没有找到,最后可能会在NSObjct中找到目标方法
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    // 重新调用lookUpImpOrForwardTryCache方法,返回方法查找流程,因为已经进行过一次动态方法决议,下次将不会再进入,所以不会造成死循环
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);

}
复制代码

很简单的一个方法,里面实际上就是判断我们传入的是否是元类,然后再调用不同的方法,我们先看看返回的lookUpImpOrForwardTryCache,其就是调用的_lookUpImpTryCache方法,只是传递的behavior = 4.我们要注意一点,元类在调用resolveClassMethod后如果没有找到方法,会再次尝试调用resolveInstanceMethod.

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior){
    runtimeLock.assertUnlocked();
    if (slowpath(!cls->isInitialized())) {
        // 判断类没有初始化,重新调用 lookUpImpOrForward 进行查找
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    IMP imp = cache_getImp(cls, sel); // 又一次从imp缓存中查找是否存在
    if (imp != NULL) goto done; // 找到了则跳去done
#if CONFIG_USE_PREOPT_CACHES // 没找到继续往下走,去共享缓存中查找
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) { // imp没找到, 再次调用lookUpImpOrForward, behavior = 1,不会再次调用动态方法决议
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil; // 说明动态方法决议已经执行过,且没有添加imp,imp == 消息转发imp, 则直接返回空
    }
    return imp; // 说明动态方法决议中添加了对应的imp
}
复制代码

其就是给我们的一次补救的机会,最后还是会再次调用方法查找流程,并且再次调用的过程中不会再次调用动态方法决议流程,避免死循环.消息转发的实现重点是在上面的现在我们来研究一下resolveInstanceMethodresolveClassMethod

resolveInstanceMethod方法

首先我们看看实例方法的动态决议resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls){
……
    SEL resolve_sel = @selector(resolveInstanceMethod:); // 创建一个方法名为resolveInstanceMethod的SEL
    // 判断resolveInstanceMethod是否在目标类中实现
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        return;
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); // 通过objc_msgSend方法调用resolveInstanceMethod, 消息接收者是cls,也就是说明 resolveInstanceMethod 是类方法
    // 缓存结果,不会调用第二次,重新调用方法查找流程
    // 虽然调用了resolveInstanceMethod,但是这个返回值是bool
    // 所以我们要获取对应的imp,还是需要通过方法查找流程
    // 如果通过resolveInstanceMethod添加了方法,就缓存在类中
    // 没添加,则缓存forward_imp
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    if (resolved  &&  PrintResolving) { //  组装日志并打印日志
        ……
    }
}
复制代码

这个方法也相对比较简单,最后调用lookUpImpOrNilTryCache,其内部仍然是调用上面的_lookUpImpTryCache返回方法查找的流程.总结如下:

  • 首先创建一个方法名为resolveInstanceMethodSEL对象resolve_sel;
  • 然后判断resolve_sel是否实现,这里通过cls->ISA可以知道,resolveInstanceMethod是个类方法
  • 通过objc_ msgSend直接调用resolveInstanceMethod方法,因为是直接对cls发送消息,所以也可以看出resolveInstanceMethods类方法;
  • 调用lookUpImpOrNilTryCache方法,重新返回到方法查找的流程当中去;

resolveClassMethod

我们再来看看类方法的流程

static void resolveClassMethod(id inst, SEL sel, Class cls){
    ……
    // 判断resolveClassMethod是否在目标类中实现
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        return;
    }
    Class nonmeta; // 获取目标类
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        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); // 通过objc_msgSend调用类中的resolveClassMethod方法
    // 缓存结果,不会调用第二次,重新调用方法查找流程
    // 类方法实际上就是元类对象中的对象方法,所以可以通过方法查找流程在元类中查找
    // 如果通过resolveClassMethod添加了,就缓存方法在元类中
    // 没添加,则缓存forward_imp 
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    if (resolved  &&  PrintResolving) {//  组装日志并打印日志
        ……
    }
}
复制代码

这个方法与resolveInstanceMethod比较类似,如果通过resolveClassMethod方法添加了目标imp,则将其缓存在目标元类中,否则缓存forward_imp

动态决议验证

我们跑一个demo来确认其是否调用 QQ20220509-160451@2x.png 我们可以看到,resolveInstanceMethod方法执行了两次,为什么会进行两次呢?我们查看崩溃日志 QQ20220509-160849@2x.png 发现其还调用了__forwarding__,这个我们在后面再看,当我们在resolveInstanceMethod中实现imp,则其就会调用实现的imp,类方法resolveClassMethod也是一样 QQ20220509-170404@2x.png QQ20220509-165533@2x.png 如果当我们在resolveClassMethod中的方法动态决议的实例方法也没有实现,其会调用resolveInstanceMethod的方法动态决议,如果我们在resolveClassMethod中的方法动态决议的使用的是元类的方法,则会死循环,一直查找元类该方法,因为resolveMethod_locked中根据! cls->isMetaClass()判断调用哪个方法 20220512101527969.png

在NSObject中实现resolveInstanceMethod

之前我们都探索过,无论是类还是元类,其根类都是NSObject,实例方法和类方法在慢速查找过程中如果找不到最后都会查找NSObject的方法,那我们对NSObject建个分类NSObject+ResolveInstanceMethod,然后在其中实现resloveInstanceMethod方法,则即可功的避免崩溃,虽然可行,只需了解,并不推荐,因为运行时系统会调用很多自己的方法,NSObject分类中重写resloveInstanceMethod方法会导致无限调用,出现崩溃。 20220512101527969.png resolveInstanceMethod只是我们防止崩溃的手段,并不能解决我们实际的问题。他的原理很简单:

  • 如果是实例方法,那么根据isa的继承链最后会在NSObject中找到resolveInstanceMethod方法并调用,因此我们在NSObject中动态添加方法是可行的。
  • 如果是类方法,由于根元类的父类是NSObject的原因,所以还会调用一次resolveInstanceMethod,最后在NSObject中找到并执行。

我们上篇文章了解到,查找到方法后会调用log_and_fill_cache写入cache中,该方法不仅可以写入cache,其还有个判断写入objcMsgLogEnabled来写入日志,我们在源码中全局搜索,发现仅有一个地方赋值为void instrumentObjcMessageSends(BOOL flag),我们来看一下写入的日志信息

extern void instrumentObjcMessageSends(BOOL flag);
WTPerson *p = [WTPerson new];
instrumentObjcMessageSends(true);
[p run];
instrumentObjcMessageSends(false);
复制代码

20220512112814252.png 我们看到,在动态决议方法之后,崩溃之前,还调用了forwardingTargetForSelectormethodSignatureForSelector两个方法, 这两个方法就是消息的转发,这三个方法就是我们常说的三根救命稻草

消息的转发 - forwardingTargetForSelector

还是之前的demo,我们不在实现resloveInstanceMethod,而是使用forwardingTargetForSelector 20220512115056261.png 我们通过forwardingTargetForSelector将方法转发给可以实现方法的类,这个就是消息的快速转发.当前方法的cache也是缓存到WTStudent中,因为已经转发给WTStudent

消息的转发 - methodSignatureForSelector

当快速转发也无法成功的时候,就剩下最后从拯救机会,methodSignatureForSelector也可以写在NSObject中和resloveInstanceMethod一样防止崩溃,他本身只是发送信号,需要和另一个接收信号的方法forwardInvocation一起使用。 20220512130620072.png

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s --> %@", __func__, NSStringFromSelector(aSelector));
    return [NSMethodSignature signatureWithObjCTypes:"v@::"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s --> %@", __func__, anInvocation);
    WTStudent *stu = [WTStudent new];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self];
    } else if([stu respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:stu ];
    } else {
        NSLog(@"别调这个方法了,尝试其他方法试试");
        SEL originSel = anInvocation.selector;
        anInvocation.target = stu;
        anInvocation.selector = @selector(runslow:);
        //**0位置是targe 1位置是sel,所以自定义的参数传递只能从2开始**
        [anInvocation setArgument:&originSel atIndex:2];
        [anInvocation invoke];
    }
    [stu runslow];
}
复制代码

上面提到resloveInstanceMethod调用了两次,现在我们来弄明白问什么。我们先打印两次的一下堆栈信息。 20220512133807457.png 第一次进入是objc_msgSend_uncached未找到cache时来到了我们的resloveInstanceMethod,第二次进入是CF_forwarding_prep_0__forwarding__进行消息转发methodSignatureForSelectormethodSignatureForSelector->class_getInstanceMethod->lookUpImpOrForward->resloveInstanceMethod

总结

防止崩溃的三根救命稻草resloveInstanceMethodforwardingTargetForSelectormethodSignatureForSelector

resloveInstanceMethod消息的动态决议流程:

  • 首先判断是否初始化,如果没有初始化则直接调用lookUpImpOrForward,里面有针对没初始化的进行相应的处理;
  • 然后去缓存中进行方法的快速查找,找到了就去done
  • 缓存中没找到,如果支持共享缓存,则去共享缓存中查找
  • 都没有查找到,则通过慢速方法查找去查找方法,behavior = 4,这次慢速查找不会再次调用动态方法决议
  • done流程中,如果已经执行过动态方法决议且并没有添加imp,则缓存中的sel对应imp消息转发imp,这时直接返回nil。否则返回添加的imp实现。

当我们动态决议中没有处理,则可以使用forwardingTargetForSelector进行快速消息转发,但是这种方式调用的方法名和参数必须完全一样,比较死板。如果转发的对象也无法实现对应方法,则使用最后的methodSignatureForSelector发送信号,进入慢速消息转发流程,来动态的添加方法的实现,如果三次都无法处理,则触发doesNotRecognizeSelector崩溃。

猜你喜欢

转载自juejin.im/post/7096729286656131085