前文提要
iOS 底层探索文章系列
1、动态方法决议
1.1 消息发送
我们先简单回顾一下消息发送流程
- 先去本类的缓存方法列表中查找
- 如果没有找到,就去本类的方法列表中查找
- 如果当前方法列表还是没有,就通过
superClass指针在继承链中一直向上循环去查找,一直找到根NSObject。 - 如果还是没有找到,那么就进入 消息转发流程。
- 到了消息转发流程还是没有处理的话,那么就会报
unrecognized selector错误。
1.2 动态方法决议
当一个方法没有实现时,也就是在 cache list 和其继承关系的 method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime 会给一次机会动态添加方法实现。
我们可以通过重写 resolveInstanceMethod 和 resolveClassMethod 方法,动态添加未实现的方法。
其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个 BOOL 返回值,返回 NO 则进入消息转发流程。
1.2.1 底层方法实现
static NEVER_INLINE IMPresolveMethod_locked(id inst, SEL sel, Class cls, int behavior){runtimeLock.assertLocked();ASSERT(cls->isRealized());runtimeLock.unlock();if (! cls->isMetaClass()) {// try [cls resolveInstanceMethod:sel]resolveInstanceMethod(inst, sel, cls);}else {// try [nonMetaClass resolveClassMethod:sel]// and [cls resolveInstanceMethod:sel]resolveClassMethod(inst, sel, cls);if (!lookUpImpOrNil(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using itreturn lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);}
这里就是我们需要研究的动态方法解析代码了。流程如下
- 如果我们调用的是实例方法,那么
cls就不是元类,就会执行实例方法的动态决议。 - 如果我们调用的是类方法,那么
cls就是元类,则会先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这是调用元类的实例方法,根据继承链,会从根元类(元类的isa会指向根元类)开始找,最终会找到NSObject根类的resolveInstanceMethod方法。
注意:类方法存储在元类里面是实例方法。
static void resolveInstanceMethod(id inst, SEL sel, Class cls){runtimeLock.assertUnlocked();ASSERT(cls->isRealized());SEL resolve_sel = @selector(resolveInstanceMethod:);if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {// Resolver not implemented.return;}BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;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. clsIMP imp = lookUpImpOrNil(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 和 resolveClassMethod 都会看 cls 是否实现了对应的方法,然后给这个类发送消息,如果动态决议提供了方法,那么下次 lookUpImpOrNil 就会命中。resolveMethod_locked 最后就会返回对应的 IMP。
1.2.2 示例代码
下面是 resolveInstanceMethod 实例代码,我们调用了没有实现的 sayBad 对象方法。
- (void)zl_sayBad {// implementation}+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(sayBad)) {NSLog(@"来了老弟 %@", NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(self, @selector(zl_sayBad));Method method = class_getInstanceMethod(self, @selector(zl_sayBad));const char *type = method_getTypeEncoding(method);return class_addMethod(self, sel, imp, type);}return [super resolveInstanceMethod:sel];}
我们可以在 resolveInstanceMethod 处下一个断点

我们可以看到首先方法没有被缓存,调用了 _objc_msgSend_uncached 函数,之后查找 IMP 或者转发 lookUpImpOrForward,然后来到 resolveInstanceMethod。这是发生在消息转发之前的,在执行完 class_addMethod 并返回 YES 之后,就把 SEL 和 IMP 添加到类里面了,同时会进行缓存方法。我们可以连续调用两次 sayBad 方法。

可以看到,resolveInstanceMethod 只调用了一次。
下面我们看一下 resolveClassMethod 实例代码,我们调用了没有实现的 sayHappy 类方法。
+ (void)zl_sayHappy {// implementation}+ (BOOL)resolveClassMethod:(SEL)sel {if (sel == @selector(sayHappy)) {NSLog(@"来了老弟 %@", NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));Method method = class_getInstanceMethod(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));const char *type = method_getTypeEncoding(method);return class_addMethod(objc_getMetaClass("ZLStudent"), sel, imp, type);}return [super resolveClassMethod:sel];}
其实根据继承链路图,我们可以在根类 NSObject 里面实现 resolveInstanceMethod 进行重写方法,来进行统一拦截。
2、消息转发
2.1 探究底层源码
如果 IMP 没有找到,并且动态方法决议阶段也没有处理,则进入消息转发阶段
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
这部分又回到了汇编代码 __objc_msgForward_impcache 部分
STATIC_ENTRY __objc_msgForward_impcache// No stret specialization.b __objc_msgForwardEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForwardadrp x17, __objc_forward_handler@PAGEldr p17, [x17, __objc_forward_handler@PAGEOFF]TailCallFunctionPointer x17END_ENTRY __objc_msgForward
下面我们又回调了C函数部分 _objc_forward_handler
__attribute__((noreturn, cold)) voidobjc_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;
这就是我们开发中最常看到的 unrecognized selector sent to instance 了。但是我们怎么没有看到消息转发部分的源码呢?
其实是因为苹果没有把它开源,所以没法看,但是我们怎么知道中间有哪些过程呢?其实我们可以开一下上帝视角。
// 内部一个打印消息的函数extern void instrumentObjcMessageSends(BOOL flag);int main(int argc, const char * argv[]) {@autoreleasepool {ZLStudent *student = [ZLStudent alloc];instrumentObjcMessageSends(YES);[student sayBad];instrumentObjcMessageSends(NO);}return 0;}
这个函数开启之后,会在 /private/tmp 目录下创建一个 msgSends-xxxx,xxxx 是内部生成的一个编号。


然后就可以看到调用里面对对象进行了一些的函数调用过程
resolveInstanceMethodforwardingTargetForSelectormethodSignatureForSelectorresolveInstanceMethoddoesNotRecognizeSelector
这时候,了解消息转发流程的同学可能要问了,forwardInvocation 怎么没看到?其实这是因为我们没有调用 methodSignatureForSelector,所以这时候是不会调用 forwardInvocation 的。我们现在加上这段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSLog(@"来了老弟 %s", __FUNCTION__);if (aSelector == @selector(sayBad)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"];}return [super methodSignatureForSelector:aSelector];}
然后我们重新查看新生成的日志,就能看到调用了 forwardInvocation 了。

消息转发流程也可以分为两个阶段
- 快速消息转发
- 慢速消息转发
我们接下来先看一下快速消息转发的实现流程
2.2 快速消息转发
我们可以在 forwardingTargetForSelector 方法中将未实现的消息,转发给其他对象。可以在下面的示例代码中看到,返回响应未实现方法的其他对象。

可以看到,消息接收者成功被转到 ZLPicker 对象身上去了。但是如果 forwardingTargetForSelector 方法未做出任何响应的话,就会来到 消息慢速转发流程 上了。
2.3 慢速消息转发
慢速消息转发时,首先会调用 methodSignatureForSelector 方法,在方法内部生成 NSMethodSignature 类型的方法签名对象。在生成签名对象时,可以指定 target 和 SEL,可以将这两个参数换成其他参数,将消息转发给其他对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSLog(@"来了老弟 %s", __FUNCTION__);if (aSelector == @selector(sayBad)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"];}return [super methodSignatureForSelector:aSelector];}
生成 NSMethodSignature 签名对象后,就会调用 forwardInvocation 方法,这是消息转发中最后一步了。在这一步,只要我们重写了 forwardInvocation 方法,就算不做任何操作,也不会发送消息找不到的崩溃了,只是这样会造成 事务 的浪费。

如果我们实在找不到其他对象进行处理,那么我们可以就像漂流瓶一样把 anInvocation 放出去。

这种方法是因为我们开了上帝视角才找到系统调用了这些方法的,那么有没有一些让我们实实在在看到系统的调用流程呢,接下来我们使用 Hopper Disassembler 反汇编来分析一下消息转发调用流程。
3、Hopper Disassembler 分析
我们先来看看崩溃时的调用堆栈信息
2020-09-25 07:13:53.630518+0800 类方法的归属[2941:85139] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ZLStudent sayBad]: unrecognized selector sent to instance 0x1007b3da0'*** First throw call stack:(0 CoreFoundation 0x00007fff380b8b57 __exceptionPreprocess + 2501 libobjc.A.dylib 0x00007fff70f2b5bf objc_exception_throw + 482 CoreFoundation 0x00007fff38137be7 -[NSObject(NSObject) __retain_OA] + 03 CoreFoundation 0x00007fff3801d3bb ___forwarding___ + 14274 CoreFoundation 0x00007fff3801cd98 _CF_forwarding_prep_0 + 1206 libdyld.dylib 0x00007fff720d3cc9 start + 1)libc++abi.dylib: terminating with uncaught exception of type NSException
可以看到,现在我们的首要目标就是找到 _CF_forwarding_prep_0,我们可以看到,这个函数存在在 CoreFoundation 中,所以我们首先要把这个 Mach-O 拷出来,然后拖入到 Hopper Disassembler,我们在控制台使用 lldb 指令 image list 将系统库的路径都给输出出来,然后找到 CoreFoundation 库的路径

/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
拖入之后,我们开始查找 ___forwarding_prep_0___

下一步进入 ___forwarding_prep_0___ 函数之后,找到 ____forwarding___

在 ____forwarding___ 函数里面发现了forwardingTargetForSelector

如果没有实现 forwardingTargetForSelector 方法的话,则继续往下执行。我们阅读以下这里面的内容,其实做了一些判断,如果没有实现 methodSignatureForSelector 方法或者返回为nil的话,则最后会跳转到 loc_64e3c


我们继续往下看,这里也看到如果没有实现 methodSignatureForSelector方法或者为空则跳转到 loc_64ec2, 如果实现了 forwardInvocation 则会调用 forwardInvocation

这样,我们通过反汇编 CoreFoundation 的形势,也可以看到整个流程。
4、消息转发流程图

5、总结
如果一个实例方法不能在类和它的继承链的方法列表中不能被找到,则进入到方法解析和消息转发流程。
- 1 首先判断当前实例的类对象是否实现了
resolveInstanceMethod方法,如果实现了,会调用resolveInstanceMethod方法。这个时候我们可以在resolveInstanceMethod方法里动态的添加该SEL对应的方法。之后会 重新执行查找方法实现 的流程,如果依旧没找到方法实现,或者没有实现resolveInstanceMethod方法,则进入消息转发流程。 - 2 调用
forwardingTargetForSelector方法,尝试找到一个能响应该消息的对象。如果找到了,则直接把消息转发给它,如果返回nil,则继续下一步流程。 - 3 调用
methodSignatureForSelector方法,尝试获得一个方法签名,如果获取不到,则直接调用doesNotRecognizeSelector抛出异常信息。 - 4 调用
forwardInvocation方法,进行事务处理。如果不处理的话,则把事务抛出,爱谁谁接。
