前文提要

iOS 底层探索文章系列

1、动态方法决议

1.1 消息发送

我们先简单回顾一下消息发送流程

  1. 先去本类的缓存方法列表中查找
  2. 如果没有找到,就去本类的方法列表中查找
  3. 如果当前方法列表还是没有,就通过 superClass 指针在继承链中一直向上循环去查找,一直找到根 NSObject
  4. 如果还是没有找到,那么就进入 消息转发流程
  5. 到了消息转发流程还是没有处理的话,那么就会报 unrecognized selector 错误。

1.2 动态方法决议

当一个方法没有实现时,也就是在 cache list 和其继承关系的 method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,Runtime 会给一次机会动态添加方法实现。

我们可以通过重写 resolveInstanceMethodresolveClassMethod 方法,动态添加未实现的方法。
其中第一个是添加实例方法,第二个是添加类方法。这两个方法都有一个 BOOL 返回值,返回 NO 则进入消息转发流程。

1.2.1 底层方法实现

  1. static NEVER_INLINE IMP
  2. resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
  3. {
  4. runtimeLock.assertLocked();
  5. ASSERT(cls->isRealized());
  6. runtimeLock.unlock();
  7. if (! cls->isMetaClass()) {
  8. // try [cls resolveInstanceMethod:sel]
  9. resolveInstanceMethod(inst, sel, cls);
  10. }
  11. else {
  12. // try [nonMetaClass resolveClassMethod:sel]
  13. // and [cls resolveInstanceMethod:sel]
  14. resolveClassMethod(inst, sel, cls);
  15. if (!lookUpImpOrNil(inst, sel, cls)) {
  16. resolveInstanceMethod(inst, sel, cls);
  17. }
  18. }
  19. // chances are that calling the resolver have populated the cache
  20. // so attempt using it
  21. return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
  22. }

这里就是我们需要研究的动态方法解析代码了。流程如下

  • 如果我们调用的是实例方法,那么 cls 就不是元类,就会执行实例方法的动态决议。
  • 如果我们调用的是类方法,那么 cls 就是元类,则会先调用类方法的动态解析。如果没有找到,我们还会调用实例方法的动态解析。这是调用元类的实例方法,根据继承链,会从根元类(元类的 isa 会指向根元类)开始找,最终会找到 NSObject 根类的 resolveInstanceMethod 方法。

注意:类方法存储在元类里面是实例方法

  1. static void resolveInstanceMethod(id inst, SEL sel, Class cls)
  2. {
  3. runtimeLock.assertUnlocked();
  4. ASSERT(cls->isRealized());
  5. SEL resolve_sel = @selector(resolveInstanceMethod:);
  6. if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
  7. // Resolver not implemented.
  8. return;
  9. }
  10. BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
  11. bool resolved = msg(cls, resolve_sel, sel);
  12. // Cache the result (good or bad) so the resolver doesn't fire next time.
  13. // +resolveInstanceMethod adds to self a.k.a. cls
  14. IMP imp = lookUpImpOrNil(inst, sel, cls);
  15. if (resolved && PrintResolving) {
  16. if (imp) {
  17. _objc_inform("RESOLVE: method %c[%s %s] "
  18. "dynamically resolved to %p",
  19. cls->isMetaClass() ? '+' : '-',
  20. cls->nameForLogging(), sel_getName(sel), imp);
  21. }
  22. else {
  23. // Method resolver didn't add anything?
  24. _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
  25. ", but no new implementation of %c[%s %s] was found",
  26. cls->nameForLogging(), sel_getName(sel),
  27. cls->isMetaClass() ? '+' : '-',
  28. cls->nameForLogging(), sel_getName(sel));
  29. }
  30. }
  31. }

resolveInstanceMethodresolveClassMethod 都会看 cls 是否实现了对应的方法,然后给这个类发送消息,如果动态决议提供了方法,那么下次 lookUpImpOrNil 就会命中。resolveMethod_locked 最后就会返回对应的 IMP

1.2.2 示例代码

下面是 resolveInstanceMethod 实例代码,我们调用了没有实现的 sayBad 对象方法。

  1. - (void)zl_sayBad {
  2. // implementation
  3. }
  4. + (BOOL)resolveInstanceMethod:(SEL)sel {
  5. if (sel == @selector(sayBad)) {
  6. NSLog(@"来了老弟 %@", NSStringFromSelector(sel));
  7. IMP imp = class_getMethodImplementation(self, @selector(zl_sayBad));
  8. Method method = class_getInstanceMethod(self, @selector(zl_sayBad));
  9. const char *type = method_getTypeEncoding(method);
  10. return class_addMethod(self, sel, imp, type);
  11. }
  12. return [super resolveInstanceMethod:sel];
  13. }

我们可以在 resolveInstanceMethod 处下一个断点

image.png

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

image.png

可以看到,resolveInstanceMethod 只调用了一次。

下面我们看一下 resolveClassMethod 实例代码,我们调用了没有实现的 sayHappy 类方法。

  1. + (void)zl_sayHappy {
  2. // implementation
  3. }
  4. + (BOOL)resolveClassMethod:(SEL)sel {
  5. if (sel == @selector(sayHappy)) {
  6. NSLog(@"来了老弟 %@", NSStringFromSelector(sel));
  7. IMP imp = class_getMethodImplementation(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));
  8. Method method = class_getInstanceMethod(objc_getMetaClass("ZLStudent"), @selector(zl_sayHappy));
  9. const char *type = method_getTypeEncoding(method);
  10. return class_addMethod(objc_getMetaClass("ZLStudent"), sel, imp, type);
  11. }
  12. return [super resolveClassMethod:sel];
  13. }

其实根据继承链路图,我们可以在根类 NSObject 里面实现 resolveInstanceMethod 进行重写方法,来进行统一拦截。

2、消息转发

2.1 探究底层源码

如果 IMP 没有找到,并且动态方法决议阶段也没有处理,则进入消息转发阶段

  1. const IMP forward_imp = (IMP)_objc_msgForward_impcache;

这部分又回到了汇编代码 __objc_msgForward_impcache 部分

  1. STATIC_ENTRY __objc_msgForward_impcache
  2. // No stret specialization.
  3. b __objc_msgForward
  4. END_ENTRY __objc_msgForward_impcache
  5. ENTRY __objc_msgForward
  6. adrp x17, __objc_forward_handler@PAGE
  7. ldr p17, [x17, __objc_forward_handler@PAGEOFF]
  8. TailCallFunctionPointer x17
  9. END_ENTRY __objc_msgForward

下面我们又回调了C函数部分 _objc_forward_handler

  1. __attribute__((noreturn, cold)) void
  2. objc_defaultForwardHandler(id self, SEL sel)
  3. {
  4. _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
  5. "(no message forward handler is installed)",
  6. class_isMetaClass(object_getClass(self)) ? '+' : '-',
  7. object_getClassName(self), sel_getName(sel), self);
  8. }
  9. void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

这就是我们开发中最常看到的 unrecognized selector sent to instance 了。但是我们怎么没有看到消息转发部分的源码呢?

其实是因为苹果没有把它开源,所以没法看,但是我们怎么知道中间有哪些过程呢?其实我们可以开一下上帝视角。

  1. // 内部一个打印消息的函数
  2. extern void instrumentObjcMessageSends(BOOL flag);
  3. int main(int argc, const char * argv[]) {
  4. @autoreleasepool {
  5. ZLStudent *student = [ZLStudent alloc];
  6. instrumentObjcMessageSends(YES);
  7. [student sayBad];
  8. instrumentObjcMessageSends(NO);
  9. }
  10. return 0;
  11. }

这个函数开启之后,会在 /private/tmp 目录下创建一个 msgSends-xxxxxxxx 是内部生成的一个编号。

image.png

image.png

然后就可以看到调用里面对对象进行了一些的函数调用过程

  1. resolveInstanceMethod
  2. forwardingTargetForSelector
  3. methodSignatureForSelector
  4. resolveInstanceMethod
  5. doesNotRecognizeSelector

这时候,了解消息转发流程的同学可能要问了,forwardInvocation 怎么没看到?其实这是因为我们没有调用 methodSignatureForSelector,所以这时候是不会调用 forwardInvocation 的。我们现在加上这段

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  2. NSLog(@"来了老弟 %s", __FUNCTION__);
  3. if (aSelector == @selector(sayBad)) {
  4. return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  5. }
  6. return [super methodSignatureForSelector:aSelector];
  7. }

然后我们重新查看新生成的日志,就能看到调用了 forwardInvocation 了。

image.png

消息转发流程也可以分为两个阶段

  • 快速消息转发
  • 慢速消息转发

我们接下来先看一下快速消息转发的实现流程

2.2 快速消息转发

我们可以在 forwardingTargetForSelector 方法中将未实现的消息,转发给其他对象。可以在下面的示例代码中看到,返回响应未实现方法的其他对象。

image.png

可以看到,消息接收者成功被转到 ZLPicker 对象身上去了。但是如果 forwardingTargetForSelector 方法未做出任何响应的话,就会来到 消息慢速转发流程 上了。

2.3 慢速消息转发

慢速消息转发时,首先会调用 methodSignatureForSelector 方法,在方法内部生成 NSMethodSignature 类型的方法签名对象。在生成签名对象时,可以指定 targetSEL,可以将这两个参数换成其他参数,将消息转发给其他对象。

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  2. NSLog(@"来了老弟 %s", __FUNCTION__);
  3. if (aSelector == @selector(sayBad)) {
  4. return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  5. }
  6. return [super methodSignatureForSelector:aSelector];
  7. }

生成 NSMethodSignature 签名对象后,就会调用 forwardInvocation 方法,这是消息转发中最后一步了。在这一步,只要我们重写了 forwardInvocation 方法,就算不做任何操作,也不会发送消息找不到的崩溃了,只是这样会造成 事务 的浪费。

image.png

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

image.png

这种方法是因为我们开了上帝视角才找到系统调用了这些方法的,那么有没有一些让我们实实在在看到系统的调用流程呢,接下来我们使用 Hopper Disassembler 反汇编来分析一下消息转发调用流程。

3、Hopper Disassembler 分析

我们先来看看崩溃时的调用堆栈信息

  1. 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'
  2. *** First throw call stack:
  3. (
  4. 0 CoreFoundation 0x00007fff380b8b57 __exceptionPreprocess + 250
  5. 1 libobjc.A.dylib 0x00007fff70f2b5bf objc_exception_throw + 48
  6. 2 CoreFoundation 0x00007fff38137be7 -[NSObject(NSObject) __retain_OA] + 0
  7. 3 CoreFoundation 0x00007fff3801d3bb ___forwarding___ + 1427
  8. 4 CoreFoundation 0x00007fff3801cd98 _CF_forwarding_prep_0 + 120
  9. 6 libdyld.dylib 0x00007fff720d3cc9 start + 1
  10. )
  11. libc++abi.dylib: terminating with uncaught exception of type NSException

可以看到,现在我们的首要目标就是找到 _CF_forwarding_prep_0,我们可以看到,这个函数存在在 CoreFoundation 中,所以我们首先要把这个 Mach-O 拷出来,然后拖入到 Hopper Disassembler,我们在控制台使用 lldb 指令 image list 将系统库的路径都给输出出来,然后找到 CoreFoundation 库的路径

image.png

  1. /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation

拖入之后,我们开始查找 ___forwarding_prep_0___

image.png

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

image.png

____forwarding___ 函数里面发现了forwardingTargetForSelector

image.png

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

image.png

image.png

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

image.png

这样,我们通过反汇编 CoreFoundation 的形势,也可以看到整个流程。

4、消息转发流程图

image.png

5、总结

如果一个实例方法不能在类和它的继承链的方法列表中不能被找到,则进入到方法解析和消息转发流程。

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