oc底层—— 消息动态决议与消息转发

537 阅读10分钟

我们在前面讲解了objc_msgSend()快速查找慢速查找流程,那么如果它们两个都没找到方法呢,系统会走向哪里呢,我们分析下它后面的流程。

类的初始化补充

我们在前面分析objc_msgSend()的慢速查找流程时,讲到类的相关初始化,它里面有调用一个非常重要的方法+(void)initialize(),也就是说class第一次接收消息的时候会调用该方法,调用流程:realizeAndInitializeIfNeeded_locked——>initializeAndLeaveLocked——>initializeAndMaybeRelock——>initializeNonMetaClass——>callInitialize,我们在initializeNonMetaClass这个方法中还能看到一个非常关键的点,父类的initialize在子类之前,我们看下图:

截屏2022-04-27 上午10.12.02.png

我们可以看到,若父类存在且没有被初始化,会递归调用initializeNonMetaClass方法,所以可以非常清楚父类的初始化在子类之前。

慢速查找中获取父类缓存的流程补充

我们之前讲的类的缓存快速查找流程时,最终没有缓存命中CacheHit时,调用_objc_msgSend_uncached方法,但在进入慢速查找lookUpImpOrForward时,再次查找父类缓存,调用cache_getImp方法时,汇编走到了这里:

STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant

LGetImpMissDynamic:
    mov p0, #0
    ret

父类缓存没有命中时,直接调用LGetImpMissDynamic,那么imp=cache_getImp(curClass,sel)此时得到nil,进入下一轮循环,接着进入父类的methodlist查找,跟之前类的缓存查找流程稍微有点区别:

截屏2022-04-27 上午10.49.34.png

消息动态决议

我们在前面已经知道若在类和父类的缓存和方法列表中都没有找到imp,则会调用forward_imp,实际在项目中是直接崩溃了的,我们看下图:

截屏2022-04-27 上午11.03.52.png

我们在CTPerson声明了一个eat方法,但是我们没有实现它,接着调用下:

截屏2022-04-27 上午11.06.33.png

我们可以看到工程崩溃了,并提示unrecognized selector sent to instance 0x1330ee0,那系统为什么会提示这个错误呢?我们就可以分析下forward_imp,而在lookUpImpOrForward方法中对forward_imp有赋值,我们看下图:

截屏2022-04-27 上午11.09.26.png

那么很清楚了,我们就看下_objc_msgForward_impcache函数,这里就进入汇编:

STATIC_ENTRY __objc_msgForward_impcache
    // No stret specialization.
    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

.macro TailCallFunctionPointer
    braaz $0
.endmacro

b __objc_msgForward:跳转到__objc_msgForward__objc_msgForwardTailCallFunctionPointer只简单的返回$0,那么我们就重点找__objc_forward_handler,全局搜并没有找到这个函数,说明只会进入到源码中去,我们找objc_forward_handler

截屏2022-04-27 上午11.30.19.png

这里我们已经看到我们崩溃信息的提示,它是调用_objc_fatal()返回的。那我们在项目崩溃前没有办法处理吗,显然不会的,我们有三次机会处理它resolveInstanecMethod()[动态方法决议]——>forwardingTargetForSelector[快速转发]——>methodSignatureForSelector[慢速转发]——>消息无法处理这个流程,我们画张图清晰点:

截屏2022-04-27 下午12.18.43.png

我们从forward_imp函数后继续看,当imp==forward_imp时,跳出当前循环:

截屏2022-04-27 下午2.19.18.png

后面执行resolveMethod_locked()方法:

截屏2022-04-27 下午2.24.29.png

这里写了一个算法,保证resolveMethod_locked只会执行一次,behavior = 3 ,汇编那里带过的值,LOOKUP_RESOLVER = 2behavior & LOOKUP_RESOLVER = 2,下面behavior ^= LOOKUP_RESOLVERbehavior此时为1,那么当下来代码再执行到此次时,就是 1 & 2 = 0,条件就不会满足,resolveMethod_locked不会被调用。

我们接着看下resolveMethod_locked()方法的实现:

截屏2022-04-27 下午2.25.15.png

对象方法的动态决议

可以看到,不是元类的时候,调用的resolveInstanceMethod()方法,我们进去看下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());

    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //判断cls是有有实现resolve_sel,这里系统有默认实现,所以不会进入
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //系统向当前类cls发送resolve_sel消息,看当前类cls是否有实现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
    //从当前类的缓存和方法里再次查询,看是否存在sel对应的imp
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    //下面都是一些打印和输出,不用看
    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",

                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

所以我们上面源码可以看到resolveInstanceMethod()函数主要用途:1.判断当前类cls是否实现了resolveInstanceMethod方法;2.调用lookUpImpOrNilTryCache对当前类cls的缓存和methodlist再次查询一次。这里还有一个问题就是,resolveMethod_locked()函数本身还调用了一次lookUpImpOrForwardTryCache(),这两次调用查询有什么区别呢?我们看下_lookUpImpTryCache方法实现:

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{

    runtimeLock.assertUnlocked();
    //判断类、元类是否做过初始化,若没有从新调用lookUpImpOrForward
    if (slowpath(!cls->isInitialized())) {

        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //缓存是否存在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=null,系统会再给一次机会,重新在走一次慢速查找
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
done:
    //
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

首先是cls->isInitialized(),判断类、元类是否做过相关的初始化,若没有直接调用lookUpImpOrForward()慢速查找,若已经初始化,从当前类cls的缓存查找,若缓存没有,再次调用lookUpImpOrForward()查询,若找到imp,直接执行done,我们看到这里有条件判断:

// resolveMethod_locked 调用
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}
//resolveInstanceMethod 调用
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
//_lookUpImpTryCache函数done:
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;

上面resolveInstanceMethod()函数调用的lookUpImpOrNilTryCache(inst, sel, cls, behavior | LOOKUP_NIL(4)),那么当前的behavior值为4,说明系统已经给过一次的补救的机会,此时imp还是等于_objc_msgForward_impcache,说明当前类没有实现resolveInstanceMethodresolveClassMethod方法,直接返回nil;而lookUpImpOrForwardTryCache(inst, sel, cls, behavior)behavior传值为1,是从resolveMethod_locked带过来的值,behavior & LOOKUP_NIL = 0,所以会return imp

我们研究了源码之后,在工程中实际验证下,我们在CTPerson中声明了一个对象方法eat,但没有做实现,然后再CTPerson.m文件中实现resolveInstanceMethod方法: 截屏2022-04-28 下午3.18.51.png 我们在resolveInstanceMethod方法中判读sel==@selector(eat),动态给eat添加一个run方法的实现,我们打印输出下:

截屏2022-04-28 下午3.19.55.png 可以看到,调用eat方法,输出了run,而且程序也没有崩溃。

类方法的动态决议

resolveMethod_locked()方法里,若是元类,调用的resolveClassMethod()

// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    resolveInstanceMethod(inst, sel, cls);
}

这里要讲下还会调用一次resolveInstanceMethod的原因,因为类方法在元类里相当于实例方法存在的,其实在底层是不存在类方法和对象方法的区分的,我们找对象方法的路径:class——>suprerclass——>NSObject——>nil,类方法:metaClass——>rootMetalClass——>NSObject——>nil

我们看下resolveClassMethod方法实现:

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

项目验证消息动态决议

我们可以看到流程跟对象方法的动态决议,大致是一样的,只不过这里是向nonmeta发送resolveClassMethod消息。所以我们同样在工程中验证下:

截屏2022-04-28 下午4.37.32.png

截屏2022-04-28 下午4.37.47.png

我们看到没有实现processClassMethod这个类方法,通过resolveClassMethod方法里动态添加varifyClassMethod类方法的实现,成功调用。

其实到这里我们已经很清楚,所有的类的方法里都会调用resolveInstanceMethod,所有元类的方法都会调用resolveClassMethod,而他们最终又都会找到NSObject这里,我们其实可以把他们放在NSObject的分类里处理:

截屏2022-04-28 下午5.15.33.png

这里我们还要考虑一个问题,如果resolveInstanceMethodresolveClassMethod我们都没实现呢,后面如何处理呢?这里我们可以通过instrumentObjcMessageSends辅助分析:

截屏2022-04-28 下午5.36.41.png

这里run方法没有实现,所以工程崩溃,但这时候在沙盒/tmp/msgSends下会有一个日志文件msgSends-xxxx,我们打开后可以看到:

截屏2022-04-28 下午5.35.55.png

resolveInstanceMethod后,还有forwardingTargetForSelectormethodSignatureForSelector方法,这些方法有什么用,做了什么呢,我们后面分析这里。

消息转发

其实我们在前面直接调用instrumentObjcMessageSends是有点奇怪的,这个方法从哪里来的呢,我们看下lookUpImpOrForward里有找到imp调用log_and_fill_cache插入缓存数据的地方:

截屏2022-04-28 下午6.07.04.png

objcMsgLogEnabled && implementer 为真时,会调用logMessageSend发送日志,implementercurClass,来到这里它肯定是存在的,而objcMsgLogEnabled默认为false,所以我们源码全局搜objcMsgLogEnabled找到它赋值的地方:

截屏2022-04-28 下午6.11.09.png

所以我们才把instrumentObjcMessageSends(BOOL flag)扩展出去。

消息快速转发——forwardingTargetForSelector

forwardingTargetForSelector这个方法在Developer Documentation有详细介绍,这里我们直接项目操作下:

截屏2022-04-29 上午10.23.25.png

我们在项目中没有实现run这个方法,添加了forwardingTargetForSelector方法实现,系统会自动调用它。在这里我们可以进行重定向,把它交给另一个对象student处理:

截屏2022-04-29 上午10.34.39.png 截屏2022-04-29 上午10.35.23.png

我们看到消息成功的发送,打印student run

截屏2022-04-29 上午10.36.08.png

那如果student也没有实现run方法呢,系统接下来还会进行一次慢速转发流程。

消息慢速转发——methodSignatureForSelector

我们在Apple Documentation里可以看到它返回的是一个方法签名NSMethodSignature,它和forwardInvocation方法配合使用:

截屏2022-04-29 上午10.42.15.png

接下来我们在CTPerson.m文件实现methodSignatureForSelector方法:

截屏2022-04-29 上午10.52.35.png

我们看到系统已经调用了methodSignatureForSelector方法:

截屏2022-04-29 上午10.53.16.png

我们在官方文档上可以看到methodSignatureForSelectorforwardInvocation要配合使用,而且附有demo:

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL aSelector = [invocation selector]; 
    if ([friend respondsToSelector:aSelector]) 
        [invocation invokeWithTarget:friend]; 
    else 
        [super forwardInvocation:invocation];
}

我们在工程中验证下,没有问题CTStudentrun方法正常打印:

截屏2022-04-29 下午7.29.35.png

HOPPER反汇编

我们上面讲到的消息快速转发和慢速转发流程是从相关日志中推测出来的,其实有点不太正规的,我们可以bt打印堆栈看下相关信息:

截屏2022-04-29 下午8.07.58.png

上图中我们可以看到CoreFoundation几个关键的函数,__forwarding_____forwarding_prep_0___,这两个函数在苹果开源的CoreFoundation相关源码中无法找到,所以使用HOPPER打开CoreFoundation可执行文件反汇编看下相关流程,全局搜forwarding

coreFoundation可执行文件,可以创建一个ios工程(模拟器启动即可),然后控制台输入image list指令读取相关文件路径,找到coreFoundation的可执行文件路径。

截屏2022-04-29 下午8.12.35.png

__forwarding_prep_0___下可以看到__forwarding__的调用,点进去看下:

截屏2022-04-29 下午8.16.06.png

上图我们可以看到一个关键函数forwardingTargetForSelector,这里做了判断class_respondsToSelector(r12,@selecor(forwardingTargetForSelector:)),是否当前类能够相应forwardingTargetForSelector,若不能,go to loc_646a7;若当前类有实现forwardingTargetForSelector,调用forwardingTargetForSelector,调用forwardingTargetForSelector返回一个rax,若rax不存在或者rax等于rbx(self),则 go to loc_646a7,我们看下loc_646a7

截屏2022-04-29 下午8.26.44.png

上图我们看到class_respondsToSelector(r12,@selector(methodSignatureForSelector)),判断类是否能够相应methodSignatureForSelector方法,若可以响应,则直接调用。这里的_forwardStackInvocation是系统内部方法,没有对外暴露。接着下面执行,就看到forwardInvocation方法:

截屏2022-04-29 下午8.32.30.png

forwardInvocation没有实现,则调用doesNotRecognizeSelector。我们看张图熟悉下消息转发的流程:

截屏2022-04-29 下午8.41.37.png

我们开始在沙盒/tmp/msgSends下看到的日志文件中resolveInstanceMethod被调用了两次,这里工程打印看下看下:

截屏2022-04-29 下午10.16.17.png

我们看到在慢速转发后,有一个_forwardStackInvocation,之后又调用了一次resolveInstanceMethod,说明系统在这里又触发了一次查询,我们在源码resovleInstanceMethod里打个断点:

截屏2022-04-29 下午10.29.23.png

第二次调用resolveInstanceMethod走到这里时,bt打出堆栈信息,可以看到执行顺序:class_respondsToSelector_inst——>lookUpImpOrNilTryCache——>lookUpImpOrForward——>resolveMethod_locked——>resolveInstanceMethod