Dynamic resolution of messages and message forwarding

In the previous article objc_msgSend exploration , we explored the normal 方法的快速查找and 方法的慢速查找process, and we also know that when the imp is not found, the initially defined message forwarding imp will be assigned to the current imp to carry out the message forwarding process. Today we will explore this 消息转发流程.

_objc_msgForward

In the last article, we know that if we can't find the method, _objc_msgForward_impcachewe will assign the value to imp. We look for the implementation written in assembly in the source code objc-msg-arm64.sfile and find that it is the call _objc_msgForward. _objc_msgForwardThere are three lines of code and two lines of code are calling_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
复制代码

We found objc-runtime.min

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_handlerThe default assignment is the familiar method crash information, and there is a objc_setForwardHandlerfunction below to reassign it. We also saw that the crash information of the class method and instance method are put together, just based class_isMetaClasson the judgment, once again proving the bottom layer of objc There is no difference between class method and instance method. The above is the assignment of imp that cannot find the method, and it is not called. lookUpImpOrForwardBelow the assignment of imp in the for loop in the method, we will also find a piece of code, which will only be executed once

// 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

When all search methods do not find the target imp, the system will call resolveMethod_lockedto give us a chance, let us remedy this error, then let's check its source code

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);

}
复制代码

A very simple method, which is actually to judge whether what we pass in is 元类, and then call different methods. Let's first look at the returned method lookUpImpOrForwardTryCache, which is the method called _lookUpImpTryCache, just passed behavior = 4. We should pay attention that after 元类the call resolveClassMethodIf no method is found, the call will be attempted again 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

总结

crash-proof 三根救命稻草: resloveInstanceMethod, forwardingTargetForSelectorandmethodSignatureForSelector

resloveInstanceMethodDynamic resolution process for messages:

  • First determine whether to initialize, if not, it will be called directly lookUpImpOrForward, and there will be corresponding processing for those that are not initialized;
  • Then go to the cache and go to it when 方法的快速查找you find itdone
  • Not found in the cache, if the shared cache is supported, go to the shared cache to find
  • If it is not found, then use the slow method search to find the method, behavior = 4, this time the slow search will not be called again动态方法决议
  • In the doneprocess, if it has been executed 动态方法决议and has not been added , the corresponding impin the cache isselimp消息转发imp , then return directly nil. Otherwise return the added impimplementation.

When there is no processing in our dynamic resolution, we can use it forwardingTargetForSelectorfor fast message forwarding, but the method names and parameters called in this way must be exactly the same, which is relatively rigid. If the forwarded object also cannot implement the corresponding method, use the last methodSignatureForSelectorsent signal to enter the slow message forwarding process to dynamically add the implementation of the method. If it cannot be processed three times, a doesNotRecognizeSelectorcrash will be triggered.

Guess you like

Origin juejin.im/post/7096729286656131085