美文网首页
iOS底层探索--动态方法决议&消息转发流程

iOS底层探索--动态方法决议&消息转发流程

作者: spyn_n | 来源:发表于2021-10-17 19:19 被阅读0次

    前言

       在iOS底层探索--方法慢速查找篇章中如果慢速查找找不到Methodimpimp = _objc_msgForward_impcache然后break跳出死循环,在return_objc_msgForward_impcache\color{#ff0000}{之前}会进行一次方法决议:resolveMethod_locked(inst, sel, cls, behavior),给一次机会

    一、_objc_msgForward_impcache汇编流程

    //******** __objc_msgForward_impcache**********************************************************
    STATIC_ENTRY __objc_msgForward_impcache
        // No stret specialization.
        b   __objc_msgForward   // 调转__objc_msgForward
        END_ENTRY __objc_msgForward_impcache
    
    //******** __objc_msgForward**********************************************************
    ENTRY __objc_msgForward
        adrp    x17, __objc_forward_handler@PAGE
        ldr p17, [x17, __objc_forward_handler@PAGEOFF]
        TailCallFunctionPointer x17
        END_ENTRY __objc_msgForward
    
    //******** TailCallFunctionPointer**********************************************************
    .macro TailCallFunctionPointer
        // $0 = function pointer value
        br  $0  // 直接根据寄存器寻址调转寄存器0的地址
    .endmacro
    

    __objc_msgForward_impcache的汇编里面就一句代码,就是调转__objc_msgForward,而这个流程中__objc_msgForwardTailCallFunctionPointer里面就是寄存器寻址调转,也就是调转$0寄存器,这个寄存器就是下面两句之后的值:

    adrp x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    也就是拿到_objc_forward_handler,然后调用,在objc源码全局搜索一下发现在objc-runtime.mm文件中找到他的赋值,类似一个句柄,在底层实现,在非OBJC2中是默认是空的,在OBJC2中默认等于 objc_defaultForwardHandler就是打印一些东西,这就可以解释了这个经典的错误:
    Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[PSYPerson happy]: unrecognized selector sent to instance 0x101021350'
    底层也提供了自定义定制的API:void objc_setForwardHandler

    #if !__OBJC2__    // 非OBJC2**************************
    
    // Default forward handler (nil) goes to forward:: dispatch.
    void *_objc_forward_handler = nil;
    void *_objc_forward_stret_handler = nil;
    
    #else
    
    // // OBJC2的默认值*************************
    __attribute__((noreturn, cold)) void
    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;
    
    #if SUPPORT_STRET
    struct stret { int i[100]; };
    __attribute__((noreturn, cold)) struct stret
    objc_defaultForwardStretHandler(id self, SEL sel)
    {
        objc_defaultForwardHandler(self, sel);
    }
    void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
    #endif
    
    #endif
    // 支持定制
    void objc_setForwardHandler(void *fwd, void *fwd_stret)
    {
        _objc_forward_handler = fwd;
    #if SUPPORT_STRET
        _objc_forward_stret_handler = fwd_stret;
    #endif
    }
    
    

    二、 动态方法决议

    方法决议流程如下:
    1. 如果 非元类 !cls->isMetaClass() ,也就是如果是对象方法
      • 调用对象的解析方法 resolveInstanceMethod(inst, sel, cls);
        • 查找这个方法resolveInstanceMethod:,如果没有找到就直接停止解析。(其实这个方法是一定有的,在NSObject中已经实现,返回了NO,相当于系统兜底了)
        • 调用objc_msgSend(cls, resolveInstanceMethod:, sel), sel是一个参数
        • 缓存结果将+resolveInstanceMethod:方法添加到类的cache中,下次resolver解析器就不执行了imp = lookUpImpOrNilTryCache(inst, sel, cls);
    2. 如果是元类 ,即调用的类方法
      • 调用类的解析方法 resolveClassMethod(inst, sel, cls);
        • 查找这个方法resolveClassMethod:,如果没有找到就直接停止解析。(其实这个方法是一定有的,在NSObject中已经实现,返回了NO,相当于系统兜底了)
        • 调用objc_msgSend(cls, resolveClassMethod:, sel), sel是一个参数
        • 缓存结果将+resolveClassMethod:方法添加到元类cache中,下次resolver就不执行了imp = lookUpImpOrNilTryCache(inst, sel, cls);
      • 如果尝试查找元类的缓存
      • 如果元类中缓存为空,则解析原类中的对象方法resolveInstanceMethod(inst, sel, cls);,因为类方法在元类中是对象方法
    3. 因为上面流程可能已经指向一个对象的某个SEL了,所以继续lookUpImpOrForwardTryCache(inst, sel, cls, behavior);走方法查找流程

    既然底层给了一次机会解析,那我得兜着,实现一下动态解析这两个方法:运行之后发现无论是调用没实现的对象方法,或者是类方法都打印了两次:对象方法动态决议:happy类方法动态决议:happy,为什么呢?不是给了一次机会而已吗?

    实现动态解析这两个方法 对象方法 类方法
    走两次,在这这疑问,在源码中,下断点,打印第一次进入lookUpImpOrForwardresolveMethod_locked函数调用栈和第二次进入时的函数调用栈,期间会调用很多次这个方法,我们需要认准sel 和 cls是同一个即可:
    第一次进lookUpImpOrForward 第一次进resolveMethod_locked
    第二次进lookUpImpOrForward 第二次进resolveMethod_locked

      由上可知,在第二次进入的时候,instancenil值,behavior = 2,behavior & LOOKUP_RESOLVER = 1, behavior ^= LOOKUP_RESOLVERbehavior = 0;并且从函数调用栈可知,其是CodeFundation框架的_CF_forwarding_prep_0触发的,但是我们去苹果官网文档或者开源库中查找并没有看到开源的源码。带着这疑问,我们在消息转发流程里面探索。我们回到resolveInstanceMethod:方法上,既然苹果给了一次解析机会,我们可以在这个方法中调用Runtime的API动态的给class添加sel,甚至指定一个IMP,来达到解决崩溃的目的,但是问题又来了,崩溃的信息是这样-[PSYPerson happy]: unrecognized selector sent to instance 0x101021350,从resolveInstanceMethod:到崩溃信息是否还有其他流程,如果有,在resolveInstanceMethod :这个方法中暴力的截获,万一苹果在后面的流程中做了啥操作呢,显然不能这么干。

    三、消息的转发流程

      我们在方法的流程中,慢速查找方法的时候,有个函数log_and_fill_cache,填充缓存并有一个log打印,根据objcMsgLogEnabled为YES时在临时文件/tmp/msgSends-xxxx打印,而objcMsgLogEnabled默认是false,它的设置是在方法void instrumentObjcMessageSends(BOOL flag),我们可以在声明外部函数只打印调用未实现的方法的流程,并在/tep/路径下看到msgSends-xxx文件的打印如图:


    image.png

    可以看到resolveInstanceMethod:之后还有forwardingTargetForSelector:methodSignatureForSelector:最后才到doesNotRecognizeSelector:并且方法动态决议的时候走了两次,第二次进来是由CodeFundation框架的_CF_forwarding_prep_0触发的。这其中的流程可以使用CodeFundation库借助IDA或者hopper看看伪代码流程或者汇编流程。感兴趣的朋友可以慢慢看一下,文章就不带大家看了,所以得出整个消息转发的流程如下图:

    消息转发流程

    相关文章

      网友评论

          本文标题:iOS底层探索--动态方法决议&消息转发流程

          本文链接:https://www.haomeiwen.com/subject/brmbpltx.html