1. 找不到方法的报错原理

当调用实例对象为实现的方法时,会报出以下错误:

  1. -[LGPerson sayNB]: unrecognized selector sent to instance 0x101f05530

lookUpImpOrForward函数中,遍历父类寻址方法,如果最终还是没有找到,imp会被赋值为_objc_msgForward_impcache,跳出循环并将其返回

objc源码中,搜索_objc_msgForward_impcache

  1. STATIC_ENTRY __objc_msgForward_impcache
  2. // No stret specialization.
  3. b __objc_msgForward
  4. END_ENTRY __objc_msgForward_impcache
  • 在汇编代码中找到,中间只有对__objc_msgForward的调用

搜索__objc_msgForward

  1. ENTRY __objc_msgForward
  2. adrp x17, __objc_forward_handler@PAGE
  3. ldr p17, [x17, __objc_forward_handler@PAGEOFF]
  4. TailCallFunctionPointer x17
  5. END_ENTRY __objc_msgForward
  • TailCallFunctionPointer负责跳转到x17,而__objc_forward_handler才是核心代码

但是通过函数调用栈发现,虽然报错最终来自objcobjc_exception_throw函数,但前面几个函数都是来自于CoreFoundation框架的函数
image.png

  • 经过CoreFoundation框架中的_CF_forwarding_prep_0___forwarding___+[NSObject(NSObject) doesNotRecognizeSelector:]

objc源码中,搜索_objc_forward_handler

  1. void objc_setForwardHandler(void *fwd, void *fwd_stret)
  2. {
  3. _objc_forward_handler = fwd;
  4. #if SUPPORT_STRET
  5. _objc_forward_stret_handler = fwd_stret;
  6. #endif
  7. }
  • 找到一个对_objc_forward_handler赋值的方法

查看objc_setForwardHandler的函数调用栈
image.png

  • 当应用启动时,在dyldImageLoaderMachO::doImageInit:流程中,调用了CoreFoundation__CFInitialize函数,里面对libobjcobjc_setForwardHandler函数进行调用

查看传入的fwd
image.png

在消息处理机制中,报错是最后一个环节,属于系统的无奈之举。当系统找不到方法时,提供给开发者三次挽救机会:

  1. 方法动态决议
  2. 消息转发流程-快速转发
  3. 消息转发流程-慢速转发

如果这些时机均未处理消息,则系统认为该消息无法处理,最终程序崩溃并打印错误信息

2. 方法动态决议

在消息处理机制中,当系统找不到方法,最先进入方法动态决议的流程

2.1 触发条件

lookUpImpOrForward函数中,当找不到方法跳出循环后,会被以下代码拦截

  1. if (slowpath(behavior & LOOKUP_RESOLVER)) {
  2. behavior ^= LOOKUP_RESOLVER;
  3. return resolveMethod_locked(inst, sel, cls, behavior);
  4. }

汇编代码中,传入的behavior值为3

  1. //LOOKUP_INITIALIZE | LOOKUP_RESOLVER
  2. //0001 | 0010 = 0011
  3. mov x3, #3
  4. bl _lookUpImpOrForward

找到LOOKUP_INITIALIZELOOKUP_RESOLVER的定义

  1. /* method lookup */
  2. enum {
  3. LOOKUP_INITIALIZE = 1,
  4. LOOKUP_RESOLVER = 2,
  5. LOOKUP_NIL = 4,
  6. LOOKUP_NOCACHE = 8,
  7. };

此判断相当于单例模式,首次触发if判断条件

  1. behavior & LOOKUP_RESOLVER = 0011 & 0010 = 0010
  • &运算的结果为2,符合条件

重新对behavior赋值,并执行resolveMethod_locked函数

  1. behavior ^= LOOKUP_RESOLVER = 0010 ^ 0010 = 0000

再次触发if判断条件,此时behavior值为00和任何数进行&运算都为0,所以此判断不会再次进入

2.2 源码分析

找到resolveMethod_locked函数的定义

  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. resolveInstanceMethod(inst, sel, cls);
  9. }
  10. else {
  11. resolveClassMethod(inst, sel, cls);
  12. if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
  13. resolveInstanceMethod(inst, sel, cls);
  14. }
  15. }
  16. return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
  17. }
  • 系统提供给开发者的挽救机会
  • 判断当前cls是否为元类
  • 如果不是元类,调用类对象的resolveInstanceMethod方法
  • 否则,是元类,调用类对象的resolveClassMethod方法
  • 如果未能解决,调用类对象所属元类的resolveInstanceMethod方法

2.3 实例方法

  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 (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
  7. return;
  8. }
  9. BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
  10. bool resolved = msg(cls, resolve_sel, sel);
  11. IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
  12. }
  • 入参:

inst:实例对象
sel:找不到的实例方法
cls:类对象

  • 调用lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveInstanceMethod方法进行消息慢速查找

◦ 在NSObject中,该方法已默认实现
◦ 继承自NSObject的类对象,不会被return拦截

  • 系统使用objc_msgSend,发送resolveInstanceMethod消息

◦ 消息接收者为类对象
◦ 消息主体中的SELresolveInstanceMethod
◦ 参数为找不到的实例方法

  • 调用lookUpImpOrNilTryCache函数,对之前找不到的实例方法进行消息慢速查找

◦ 如果在resolveInstanceMethod成功处理,返回处理后的imp
◦ 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程

案例

LGPerson.h中,声明sayNB实例方法

  1. #import <Foundation/Foundation.h>
  2. @interface LGPerson : NSObject
  3. -(void)sayNB;
  4. @end

LGPerson.m中,实现say666实例方法和resolveInstanceMethod类方法,未实现sayNB实例方法

  1. #import "LGPerson.h"
  2. #import <objc/runtime.h>
  3. @implementation LGPerson
  4. -(void)say666{
  5. NSLog(@"实例方法-say666");
  6. }
  7. + (BOOL)resolveInstanceMethod:(SEL)sel{
  8. if(sel==@selector(sayNB)){
  9. NSLog(@"resolveInstanceMethod:%@,%@", self, NSStringFromSelector(sel));
  10. IMP imp = class_getMethodImplementation(self, @selector(say666));
  11. Method methodSay666 = class_getInstanceMethod(self, @selector(say666));
  12. const char *type = method_getTypeEncoding(methodSay666);
  13. return class_addMethod(self, @selector(sayNB), imp, type);
  14. }
  15. return [super resolveInstanceMethod:sel];
  16. }
  17. @end
  • 如果调用的实例方法为sayNB,动态添加sayNB方法,并将imp填充为say666的函数地址

main函数中,调用实例对象persayNB方法

  1. int main(int argc, const char * argv[]) {
  2. @autoreleasepool {
  3. LGPerson *per= [LGPerson alloc];
  4. [per sayNB];
  5. }
  6. return 0;
  7. }
  8. -------------------------
  9. //输出结果:
  10. 实例方法-say666
  • 自动进入resolveInstanceMethod方法

2.4 类方法

  1. static void resolveClassMethod(id inst, SEL sel, Class cls)
  2. {
  3. runtimeLock.assertUnlocked();
  4. ASSERT(cls->isRealized());
  5. ASSERT(cls->isMetaClass());
  6. if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
  7. return;
  8. }
  9. Class nonmeta;
  10. {
  11. mutex_locker_t lock(runtimeLock);
  12. nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
  13. if (!nonmeta->isRealized()) {
  14. _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
  15. nonmeta->nameForLogging(), nonmeta);
  16. }
  17. }
  18. BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
  19. bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
  20. IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
  21. }
  • 入参:

inst:类对象
sel:找不到的类方法
cls:元类

  • 调用lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveClassMethod方法进行消息慢速查找

◦ 在NSObject中,该方法已默认实现
◦ 继承自NSObject的类对象,不会被return拦截

  • 调用getMaybeUnrealizedNonMetaClass函数,验证当前类对象和元类的关系,返回一个普通类
  • 系统使用objc_msgSend,发送resolveClassMethod消息

◦ 消息接收者为类对象
◦ 消息主体中的SELresolveClassMethod
◦ 参数为找不到的类方法

  • 调用lookUpImpOrNilTryCache函数,对之前找不到的类方法进行消息慢速查找

◦ 如果在resolveInstanceMethod成功处理,返回处理后的imp
◦ 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程

找到getMaybeUnrealizedNonMetaClass函数的定义

  1. static Class getMaybeUnrealizedNonMetaClass(Class metacls, id inst)
  2. {
  3. static int total, named, secondary, sharedcache, dyld3;
  4. runtimeLock.assertLocked();
  5. ASSERT(metacls->isRealized());
  6. total++;
  7. if (!metacls->isMetaClass()) return metacls;
  8. if (metacls->ISA() == metacls) {
  9. Class cls = metacls->getSuperclass();
  10. ASSERT(cls->isRealized());
  11. ASSERT(!cls->isMetaClass());
  12. ASSERT(cls->ISA() == metacls);
  13. if (cls->ISA() == metacls) return cls;
  14. }
  15. if (inst) {
  16. Class cls = remapClass((Class)inst);
  17. while (cls) {
  18. if (cls->ISA() == metacls) {
  19. ASSERT(!cls->isMetaClassMaybeUnrealized());
  20. return cls;
  21. }
  22. cls = cls->getSuperclass();
  23. }
  24. #if DEBUG
  25. _objc_fatal("cls is not an instance of metacls");
  26. #else
  27. // release build: be forgiving and fall through to slow lookups
  28. #endif
  29. }
  30. ...
  31. }
  • 判断cls,如果非元类,直接返回
  • 如果cls为元类,且isa指向自己,证明当前cls为根元类,获取其父类NSObject并返回
  • 遍历当前类及其父类,找到isa指向元类的所属类
  • 如果均未找到,DEBUG模式下,错误提示:当前类对象不是该元类的实例
  • Release模式下,按以下流程查找该元类的类对象

◦ 查看元类是否存在指向其非元类的指针,存在直接返回
◦ 按照元类的mangledName查找类对象,如果存在且isa指向元类,将其返回
◦ 在全局Map中查找类对象,存在将其返回
◦ 在dyldclosure table中查找类对象,存在将其返回
◦ 在共享缓存中查找类对象,存在将其返回
◦ 以上流程均未找到,错误提示:没有指向该元类的类

案例

LGPerson.h中,声明sayNB类方法

  1. #import <Foundation/Foundation.h>
  2. @interface LGPerson : NSObject
  3. +(void)sayNB;
  4. @end

LGPerson.m中,实现say666实例方法和resolveInstanceMethod类方法,未实现sayNB实例方法

  1. #import "LGPerson.h"
  2. #import <objc/runtime.h>
  3. @implementation LGPerson
  4. -(void)say666{
  5. NSLog(@"实例方法-say666");
  6. }
  7. + (BOOL)resolveClassMethod:(SEL)sel{
  8. if(sel==@selector(sayNB)){
  9. NSLog(@"resolveClassMethod:%@,%@", self, NSStringFromSelector(sel));
  10. IMP imp = class_getMethodImplementation(self, @selector(say666));
  11. Method methodSay666 = class_getInstanceMethod(self, @selector(say666));
  12. const char *type = method_getTypeEncoding(methodSay666);
  13. const char * c = NSStringFromClass(self).UTF8String;
  14. return class_addMethod(objc_getMetaClass(c), @selector(sayNB), imp, type);
  15. }
  16. return [super resolveClassMethod:sel];
  17. }
  18. @end
  • 如果调用的类方法为sayNB,动态添加sayNB方法,并将imp填充为say666的函数地址
  • 由于需要添加的sayNB是类方法,所以需要在元类中添加

main函数中,调用LGPersonsayNB类方法

  1. int main(int argc, const char * argv[]) {
  2. @autoreleasepool {
  3. [LGPerson sayNB];
  4. }
  5. return 0;
  6. }
  7. -------------------------
  8. //输出结果:
  9. 实例方法-say666
  • 自动进入resolveClassMethod方法

3. “优化”方案

如果想挽救实例方法和类方法,需要在类中实现resolveInstanceMethodresolveClassMethod方法。如果想对每一个类的方法都进行挽救处理,则需要在每一个类中都实现这两个方法。如此繁琐的操作,有没有更好的实现方式呢?

对于实例方法的查找流程,通过类对象、父类、最后找到根类。而类方法的查找流程,通过元类、根元类、最后同样找到根类

在根类中,无论是实例方法还是类方法,找不到时都会调用resolveInstanceMethod。所以我们只需要在NSObject中,实现resolveInstanceMethod方法,就可以对所有类的实例方法及类方法都进行挽救处理

创建NSObject+LG分类,写入以下代码:

  1. #import "NSObject+LG.h"
  2. #import <objc/runtime.h>
  3. @implementation NSObject (LG)
  4. -(void)say666{
  5. NSLog(@"666");
  6. }
  7. + (BOOL)resolveInstanceMethod:(SEL)sel{
  8. if(sel==@selector(sayNB)){
  9. NSLog(@"resolveInstanceMethod:%@,%@", self, NSStringFromSelector(sel));
  10. IMP imp = class_getMethodImplementation(self, @selector(say666));
  11. Method methodSay666 = class_getInstanceMethod(self, @selector(say666));
  12. const char *type = method_getTypeEncoding(methodSay666);
  13. return class_addMethod(self, @selector(sayNB), imp, type);
  14. }
  15. return NO;
  16. }
  17. @end

main函数中,调用LGPersonsayNB类方法,同时调用实例对象persayNB方法

  1. int main(int argc, const char * argv[]) {
  2. @autoreleasepool {
  3. [LGPerson sayNB];
  4. LGPerson *per= [LGPerson alloc];
  5. [per sayNB];
  6. }
  7. return 0;
  8. }
  9. -------------------------
  10. //输出结果:
  11. 实例方法-say666
  12. 实例方法-say666

此方案的优点:

  • 任意一个类,只要继承自NSObject,它的所有方法都可以被监听到
  • 我们可以将自定义方法按指定策略进行命名,然后按照相同策略进行监听,只要遇到符合策略的方法无法找到时,可以将其上报服务端,让开发者在第一时间得到问题的反馈
  • NSObject分类中对所有方法统一监听,这种方式符合AOP面向切面的设计模式

◦ 传统OOP面向对象设计模式,虽然每一个对象的分工都非常明确,但它们之间一些相同行为,会导致大量的冗余代码。如果我们将其提取,创建公共类进行继承,势必造成强依赖与高耦合
◦ 而AOP的优势,对于原始的类与对象无侵入,只要维护好NSObject分类中的监听方法即可

缺点:

  • 在监听方法中写入大量的判断条件,不利于查找与维护
  • 所有的方法都被监听,其中包含了大量的系统方法,造成性能消耗
  • NSObject分类中监听,导致系统提供的消息转发流程无法触发

对于容错处理,我们应该给开发者更大的容错空间。所以我们使用AOP设计模式,提供的“优化”方案,在这个场景下并不是一个真正的好方案

4. resolveInstanceMethod两次调用

第一次
image.png

  • _objc_msgSend_uncachedresolveMethod_lockedresolveInstanceMethod

第二次
image.png

  • CoreFoundation框架:___forwarding___-[NSObject(NSObject) methodSignatureForSelector:]__methodDescriptionForSelector
  • objcclass_getInstanceMethodresolveMethod_lockedresolveInstanceMethod

慢速转发流程methodSignatureForSelector方法之后,再次触发方法动态决议,系统再给我们一次挽救的机会

总结

找不到方法的报错原理:

  • _objc_msgForward_impcache由汇编代码实现

  • 内部调用__objc_msgForward,其中__objc_forward_handler为核心代码

  • 当应用启动时,在dyldImageLoaderMachO::doImageInit:流程中,调用了CoreFoundation__CFInitialize函数,里面对libobjcobjc_setForwardHandler函数进行调用,传入_CF_forwarding_prep_0函数地址,对libobjc中的_objc_forward_handler赋值

  • 当出现找不到方法实现的情况,由汇编代码调用C++_objc_forward_handler函数,等同于调用CoreFoundation框架中的_CF_forwarding_prep_0___forwarding___,最终来到libobjc+[NSObject(NSObject) doesNotRecognizeSelector:]

  • 通过class_isMetaClass不难看出,底层没有类方法和实例方法的区分,在开发中看到的+-方法,都是伪装

消息处理机制:

  • 报错是最后一个环节,属于系统的无奈之举

  • 当系统找不到方法时,提供给开发者三次挽救机会:

    • 方法动态决议

    • 消息转发流程-快速转发

    • 消息转发流程-慢速转发

  • 挽救失败,由doesNotRecognizeSelector:报出异常


方法动态决议:

  • 触发条件,通过&运算、按位异或,实现单例模式,保证只进入一次

  • 源码分析:

    • 判断当前cls是否为元类

      • 如果不是元类,调用类对象的resolveInstanceMethod方法

      • 否则,是元类,调用类对象的resolveClassMethod方法

    • 如果未能解决,调用类对象所属元类的resolveInstanceMethod方法

实例方法:

  • 入参:

    • inst:实例对象

    • sel:找不到的实例方法

    • cls:类对象

  • 调用lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveInstanceMethod方法进行消息慢速查找

    • NSObject中,该方法已默认实现

    • 继承自NSObject的类对象,不会被return拦截

  • 系统使用objc_msgSend,发送resolveInstanceMethod消息

    • 消息接收者为类对象

    • 消息主体中的SELresolveInstanceMethod

    • 参数为找不到的实例方法

  • 调用lookUpImpOrNilTryCache函数,对之前找不到的实例方法进行消息慢速查找

    • 如果在resolveInstanceMethod成功处理,返回处理后的imp

    • 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程

类方法:

  • 入参:

    • inst:类对象

    • sel:找不到的类方法

    • cls:元类

  • 调用lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveClassMethod方法进行消息慢速查找

    • NSObject中,该方法已默认实现

    • 继承自NSObject的类对象,不会被return拦截

  • 调用getMaybeUnrealizedNonMetaClass函数,验证当前类对象和元类的关系,返回一个普通类

系统使用objc_msgSend,发送resolveClassMethod消息

  • 消息接收者为类对象

  • 消息主体中的SELresolveClassMethod

  • 参数为找不到的类方法

  • 调用lookUpImpOrNilTryCache函数,对之前找不到的类方法进行消息慢速查找

    • 如果在resolveInstanceMethod成功处理,返回处理后的imp

    • 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程

“优化”方案:

  • NSObject分类中,实现resolveInstanceMethod方法,可以对所有类的实例方法及类方法都进行挽救处理

优点:

  • 任意一个类,只要继承自NSObject,它的所有方法都可以被监听到

  • 我们可以将自定义方法按指定策略进行命名,然后按照相同策略进行监听,只要遇到符合策略的方法无法找到时,可以将其上报服务端,让开发者在第一时间得到问题的反馈

  • NSObject分类中对所有方法统一监听,这种方式符合AOP面向切面的设计模式

    • 传统OOP面向对象设计模式,虽然每一个对象的分工都非常明确,但它们之间一些相同行为,会导致大量的冗余代码。如果我们将其提取,创建公共类进行继承,势必造成强依赖与高耦合

    • AOP的优势,对于原始的类与对象无侵入,只要维护好NSObject分类中的监听方法即可

缺点:

  • 在监听方法中写入大量的判断条件,不利于查找与维护

  • 所有的方法都被监听,其中包含了大量的系统方法,造成性能消耗

  • NSObject分类中监听,导致系统提供的消息转发流程无法触发

结论:

  • 对于容错处理,我们应该给开发者更大的容错空间。所以我们使用AOP设计模式,提供的“优化”方案,在这个场景下并不是一个真正的好方案

resolveInstanceMethod两次调用:

  • 第一次:方法动态决议的正常流程

  • 第二次:慢速转发流程methodSignatureForSelector方法之后,再次触发方法动态决议,系统再给我们一次挽救的机会