iOS底层探索--动态方法决议&消息转发流程
前言
在iOS底层探索--方法慢速查找篇章中如果慢速查找找不到Method
的imp
,imp = _objc_msgForward_impcache
然后break
跳出死循环,在return_objc_msgForward_impcache
会进行一次方法决议: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_msgForward
的TailCallFunctionPointer
里面就是寄存器寻址调转,也就是调转$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
}
二、 动态方法决议
方法决议流程如下:
- 如果 非元类
!cls->isMetaClass()
,也就是如果是对象方法- 调用对象的解析方法
resolveInstanceMethod(inst, sel, cls);
- 查找这个方法
resolveInstanceMethod:
,如果没有找到就直接停止解析。(其实这个方法是一定有的,在NSObject中已经实现,返回了NO,相当于系统兜底了) - 调用
objc_msgSend(cls, resolveInstanceMethod:, sel)
, sel是一个参数 - 缓存结果将
+resolveInstanceMethod:
方法添加到类的cache中,下次resolver解析器就不执行了imp = lookUpImpOrNilTryCache(inst, sel, cls);
- 查找这个方法
- 调用对象的解析方法
- 如果是元类 ,即调用的类方法
- 调用类的解析方法
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);
,因为类方法在元类中是对象方法
- 调用类的解析方法
- 因为上面流程可能已经指向一个对象的某个SEL了,所以继续
lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
走方法查找流程
既然底层给了一次机会解析,那我得兜着,实现一下动态解析这两个方法:运行之后发现无论是调用没实现的对象方法,或者是类方法都打印了两次:对象方法动态决议:happy
和类方法动态决议:happy
,为什么呢?不是给了一次机会而已吗?
走两次,在这这疑问,在源码中,下断点,打印第一次进入
lookUpImpOrForward
和resolveMethod_locked
函数调用栈和第二次进入时的函数调用栈,期间会调用很多次这个方法,我们需要认准sel 和 cls
是同一个即可:第一次进lookUpImpOrForward 第一次进resolveMethod_locked
第二次进lookUpImpOrForward 第二次进resolveMethod_locked
由上可知,在第二次进入的时候,
instance
是nil
值,behavior = 2,behavior & LOOKUP_RESOLVER
= 1, behavior ^= LOOKUP_RESOLVER
,behavior = 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看看伪代码流程或者汇编流程。感兴趣的朋友可以慢慢看一下,文章就不带大家看了,所以得出整个消息转发的流程如下图: