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

2021-10-17  本文已影响0人  spyn_n

前言

   在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看看伪代码流程或者汇编流程。感兴趣的朋友可以慢慢看一下,文章就不带大家看了,所以得出整个消息转发的流程如下图:

消息转发流程
上一篇下一篇

猜你喜欢

热点阅读