1. 找不到方法的报错原理
当调用实例对象为实现的方法时,会报出以下错误:
-[LGPerson sayNB]: unrecognized selector sent to instance 0x101f05530
在lookUpImpOrForward函数中,遍历父类寻址方法,如果最终还是没有找到,imp会被赋值为_objc_msgForward_impcache,跳出循环并将其返回
在objc源码中,搜索_objc_msgForward_impcache
STATIC_ENTRY __objc_msgForward_impcache// No stret specialization.b __objc_msgForwardEND_ENTRY __objc_msgForward_impcache
- 在汇编代码中找到,中间只有对
__objc_msgForward的调用
搜索__objc_msgForward
ENTRY __objc_msgForwardadrp x17, __objc_forward_handler@PAGEldr p17, [x17, __objc_forward_handler@PAGEOFF]TailCallFunctionPointer x17END_ENTRY __objc_msgForward
TailCallFunctionPointer负责跳转到x17,而__objc_forward_handler才是核心代码
但是通过函数调用栈发现,虽然报错最终来自objc的objc_exception_throw函数,但前面几个函数都是来自于CoreFoundation框架的函数
- 经过
CoreFoundation框架中的_CF_forwarding_prep_0→___forwarding___→+[NSObject(NSObject) doesNotRecognizeSelector:]
在objc源码中,搜索_objc_forward_handler
void objc_setForwardHandler(void *fwd, void *fwd_stret){_objc_forward_handler = fwd;#if SUPPORT_STRET_objc_forward_stret_handler = fwd_stret;#endif}
- 找到一个对
_objc_forward_handler赋值的方法
查看objc_setForwardHandler的函数调用栈
- 当应用启动时,在
dyld的ImageLoaderMachO::doImageInit:流程中,调用了CoreFoundation的__CFInitialize函数,里面对libobjc的objc_setForwardHandler函数进行调用
查看传入的fwd
在消息处理机制中,报错是最后一个环节,属于系统的无奈之举。当系统找不到方法时,提供给开发者三次挽救机会:
- 方法动态决议
- 消息转发流程-快速转发
- 消息转发流程-慢速转发
如果这些时机均未处理消息,则系统认为该消息无法处理,最终程序崩溃并打印错误信息
2. 方法动态决议
在消息处理机制中,当系统找不到方法,最先进入方法动态决议的流程
2.1 触发条件
在lookUpImpOrForward函数中,当找不到方法跳出循环后,会被以下代码拦截
if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}
汇编代码中,传入的behavior值为3
//LOOKUP_INITIALIZE | LOOKUP_RESOLVER//0001 | 0010 = 0011mov x3, #3bl _lookUpImpOrForward
找到LOOKUP_INITIALIZE和LOOKUP_RESOLVER的定义
/* method lookup */enum {LOOKUP_INITIALIZE = 1,LOOKUP_RESOLVER = 2,LOOKUP_NIL = 4,LOOKUP_NOCACHE = 8,};
此判断相当于单例模式,首次触发if判断条件
behavior & LOOKUP_RESOLVER = 0011 & 0010 = 0010
&运算的结果为2,符合条件
重新对behavior赋值,并执行resolveMethod_locked函数
behavior ^= LOOKUP_RESOLVER = 0010 ^ 0010 = 0000
再次触发if判断条件,此时behavior值为0。0和任何数进行&运算都为0,所以此判断不会再次进入
2.2 源码分析
找到resolveMethod_locked函数的定义
static NEVER_INLINE IMPresolveMethod_locked(id inst, SEL sel, Class cls, int behavior){runtimeLock.assertLocked();ASSERT(cls->isRealized());runtimeLock.unlock();if (! cls->isMetaClass()) {resolveInstanceMethod(inst, sel, cls);}else {resolveClassMethod(inst, sel, cls);if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);}
- 系统提供给开发者的挽救机会
- 判断当前
cls是否为元类 - 如果不是元类,调用类对象的
resolveInstanceMethod方法 - 否则,是元类,调用类对象的
resolveClassMethod方法 - 如果未能解决,调用类对象所属元类的
resolveInstanceMethod方法
2.3 实例方法
static void resolveInstanceMethod(id inst, SEL sel, Class cls){runtimeLock.assertUnlocked();ASSERT(cls->isRealized());SEL resolve_sel = @selector(resolveInstanceMethod:);if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {return;}BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(cls, resolve_sel, sel);IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);}
- 入参:
◦ inst:实例对象
◦ sel:找不到的实例方法
◦ cls:类对象
- 调用
lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveInstanceMethod方法进行消息慢速查找
◦ 在NSObject中,该方法已默认实现
◦ 继承自NSObject的类对象,不会被return拦截
- 系统使用
objc_msgSend,发送resolveInstanceMethod消息
◦ 消息接收者为类对象
◦ 消息主体中的SEL为resolveInstanceMethod
◦ 参数为找不到的实例方法
- 调用
lookUpImpOrNilTryCache函数,对之前找不到的实例方法进行消息慢速查找
◦ 如果在resolveInstanceMethod成功处理,返回处理后的imp
◦ 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程
案例
在LGPerson.h中,声明sayNB实例方法
#import <Foundation/Foundation.h>@interface LGPerson : NSObject-(void)sayNB;@end
在LGPerson.m中,实现say666实例方法和resolveInstanceMethod类方法,未实现sayNB实例方法
#import "LGPerson.h"#import <objc/runtime.h>@implementation LGPerson-(void)say666{NSLog(@"实例方法-say666");}+ (BOOL)resolveInstanceMethod:(SEL)sel{if(sel==@selector(sayNB)){NSLog(@"resolveInstanceMethod:%@,%@", self, NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(self, @selector(say666));Method methodSay666 = class_getInstanceMethod(self, @selector(say666));const char *type = method_getTypeEncoding(methodSay666);return class_addMethod(self, @selector(sayNB), imp, type);}return [super resolveInstanceMethod:sel];}@end
- 如果调用的实例方法为
sayNB,动态添加sayNB方法,并将imp填充为say666的函数地址
在main函数中,调用实例对象per的sayNB方法
int main(int argc, const char * argv[]) {@autoreleasepool {LGPerson *per= [LGPerson alloc];[per sayNB];}return 0;}-------------------------//输出结果:实例方法-say666
- 自动进入
resolveInstanceMethod方法
2.4 类方法
static void resolveClassMethod(id inst, SEL sel, Class cls){runtimeLock.assertUnlocked();ASSERT(cls->isRealized());ASSERT(cls->isMetaClass());if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {return;}Class nonmeta;{mutex_locker_t lock(runtimeLock);nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);if (!nonmeta->isRealized()) {_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",nonmeta->nameForLogging(), nonmeta);}}BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);}
- 入参:
◦ inst:类对象
◦ sel:找不到的类方法
◦ cls:元类
- 调用
lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveClassMethod方法进行消息慢速查找
◦ 在NSObject中,该方法已默认实现
◦ 继承自NSObject的类对象,不会被return拦截
- 调用
getMaybeUnrealizedNonMetaClass函数,验证当前类对象和元类的关系,返回一个普通类 - 系统使用
objc_msgSend,发送resolveClassMethod消息
◦ 消息接收者为类对象
◦ 消息主体中的SEL为resolveClassMethod
◦ 参数为找不到的类方法
- 调用
lookUpImpOrNilTryCache函数,对之前找不到的类方法进行消息慢速查找
◦ 如果在resolveInstanceMethod成功处理,返回处理后的imp
◦ 如果依然找不到方法,返回_objc_msgForward_impcache函数地址,进入消息转发流程
找到getMaybeUnrealizedNonMetaClass函数的定义
static Class getMaybeUnrealizedNonMetaClass(Class metacls, id inst){static int total, named, secondary, sharedcache, dyld3;runtimeLock.assertLocked();ASSERT(metacls->isRealized());total++;if (!metacls->isMetaClass()) return metacls;if (metacls->ISA() == metacls) {Class cls = metacls->getSuperclass();ASSERT(cls->isRealized());ASSERT(!cls->isMetaClass());ASSERT(cls->ISA() == metacls);if (cls->ISA() == metacls) return cls;}if (inst) {Class cls = remapClass((Class)inst);while (cls) {if (cls->ISA() == metacls) {ASSERT(!cls->isMetaClassMaybeUnrealized());return cls;}cls = cls->getSuperclass();}#if DEBUG_objc_fatal("cls is not an instance of metacls");#else// release build: be forgiving and fall through to slow lookups#endif}...}
- 判断cls,如果非元类,直接返回
- 如果
cls为元类,且isa指向自己,证明当前cls为根元类,获取其父类NSObject并返回 - 遍历当前类及其父类,找到
isa指向元类的所属类 - 如果均未找到,
DEBUG模式下,错误提示:当前类对象不是该元类的实例 Release模式下,按以下流程查找该元类的类对象
◦ 查看元类是否存在指向其非元类的指针,存在直接返回
◦ 按照元类的mangledName查找类对象,如果存在且isa指向元类,将其返回
◦ 在全局Map中查找类对象,存在将其返回
◦ 在dyld的closure table中查找类对象,存在将其返回
◦ 在共享缓存中查找类对象,存在将其返回
◦ 以上流程均未找到,错误提示:没有指向该元类的类
案例
在LGPerson.h中,声明sayNB类方法
#import <Foundation/Foundation.h>@interface LGPerson : NSObject+(void)sayNB;@end
在LGPerson.m中,实现say666实例方法和resolveInstanceMethod类方法,未实现sayNB实例方法
#import "LGPerson.h"#import <objc/runtime.h>@implementation LGPerson-(void)say666{NSLog(@"实例方法-say666");}+ (BOOL)resolveClassMethod:(SEL)sel{if(sel==@selector(sayNB)){NSLog(@"resolveClassMethod:%@,%@", self, NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(self, @selector(say666));Method methodSay666 = class_getInstanceMethod(self, @selector(say666));const char *type = method_getTypeEncoding(methodSay666);const char * c = NSStringFromClass(self).UTF8String;return class_addMethod(objc_getMetaClass(c), @selector(sayNB), imp, type);}return [super resolveClassMethod:sel];}@end
- 如果调用的类方法为
sayNB,动态添加sayNB方法,并将imp填充为say666的函数地址 - 由于需要添加的
sayNB是类方法,所以需要在元类中添加
在main函数中,调用LGPerson的sayNB类方法
int main(int argc, const char * argv[]) {@autoreleasepool {[LGPerson sayNB];}return 0;}-------------------------//输出结果:实例方法-say666
- 自动进入
resolveClassMethod方法
3. “优化”方案
如果想挽救实例方法和类方法,需要在类中实现resolveInstanceMethod和resolveClassMethod方法。如果想对每一个类的方法都进行挽救处理,则需要在每一个类中都实现这两个方法。如此繁琐的操作,有没有更好的实现方式呢?
对于实例方法的查找流程,通过类对象、父类、最后找到根类。而类方法的查找流程,通过元类、根元类、最后同样找到根类
在根类中,无论是实例方法还是类方法,找不到时都会调用resolveInstanceMethod。所以我们只需要在NSObject中,实现resolveInstanceMethod方法,就可以对所有类的实例方法及类方法都进行挽救处理
创建NSObject+LG分类,写入以下代码:
#import "NSObject+LG.h"#import <objc/runtime.h>@implementation NSObject (LG)-(void)say666{NSLog(@"666");}+ (BOOL)resolveInstanceMethod:(SEL)sel{if(sel==@selector(sayNB)){NSLog(@"resolveInstanceMethod:%@,%@", self, NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(self, @selector(say666));Method methodSay666 = class_getInstanceMethod(self, @selector(say666));const char *type = method_getTypeEncoding(methodSay666);return class_addMethod(self, @selector(sayNB), imp, type);}return NO;}@end
在main函数中,调用LGPerson的sayNB类方法,同时调用实例对象per的sayNB方法
int main(int argc, const char * argv[]) {@autoreleasepool {[LGPerson sayNB];LGPerson *per= [LGPerson alloc];[per sayNB];}return 0;}-------------------------//输出结果:实例方法-say666实例方法-say666
此方案的优点:
- 任意一个类,只要继承自
NSObject,它的所有方法都可以被监听到 - 我们可以将自定义方法按指定策略进行命名,然后按照相同策略进行监听,只要遇到符合策略的方法无法找到时,可以将其上报服务端,让开发者在第一时间得到问题的反馈
- 在
NSObject分类中对所有方法统一监听,这种方式符合AOP面向切面的设计模式
◦ 传统OOP面向对象设计模式,虽然每一个对象的分工都非常明确,但它们之间一些相同行为,会导致大量的冗余代码。如果我们将其提取,创建公共类进行继承,势必造成强依赖与高耦合
◦ 而AOP的优势,对于原始的类与对象无侵入,只要维护好NSObject分类中的监听方法即可
缺点:
- 在监听方法中写入大量的判断条件,不利于查找与维护
- 所有的方法都被监听,其中包含了大量的系统方法,造成性能消耗
- 在
NSObject分类中监听,导致系统提供的消息转发流程无法触发
对于容错处理,我们应该给开发者更大的容错空间。所以我们使用AOP设计模式,提供的“优化”方案,在这个场景下并不是一个真正的好方案
4. resolveInstanceMethod两次调用
第一次
_objc_msgSend_uncached→resolveMethod_locked→resolveInstanceMethod
第二次
CoreFoundation框架:___forwarding___→-[NSObject(NSObject) methodSignatureForSelector:]→__methodDescriptionForSelector→objc:class_getInstanceMethod→resolveMethod_locked→resolveInstanceMethod
慢速转发流程methodSignatureForSelector方法之后,再次触发方法动态决议,系统再给我们一次挽救的机会
总结
找不到方法的报错原理:
_objc_msgForward_impcache由汇编代码实现内部调用
__objc_msgForward,其中__objc_forward_handler为核心代码当应用启动时,在
dyld的ImageLoaderMachO::doImageInit:流程中,调用了CoreFoundation的__CFInitialize函数,里面对libobjc的objc_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消息消息接收者为类对象
消息主体中的
SEL为resolveInstanceMethod参数为找不到的实例方法
调用
lookUpImpOrNilTryCache函数,对之前找不到的实例方法进行消息慢速查找如果在
resolveInstanceMethod成功处理,返回处理后的imp如果依然找不到方法,返回
_objc_msgForward_impcache函数地址,进入消息转发流程
类方法:
入参:
inst:类对象sel:找不到的类方法cls:元类
调用
lookUpImpOrNilTryCache函数,内部调用_lookUpImpTryCache函数,对当前类对象的resolveClassMethod方法进行消息慢速查找在
NSObject中,该方法已默认实现继承自
NSObject的类对象,不会被return拦截
调用
getMaybeUnrealizedNonMetaClass函数,验证当前类对象和元类的关系,返回一个普通类
系统使用objc_msgSend,发送resolveClassMethod消息
消息接收者为类对象
消息主体中的
SEL为resolveClassMethod参数为找不到的类方法
调用
lookUpImpOrNilTryCache函数,对之前找不到的类方法进行消息慢速查找如果在
resolveInstanceMethod成功处理,返回处理后的imp如果依然找不到方法,返回
_objc_msgForward_impcache函数地址,进入消息转发流程
“优化”方案:
- 在
NSObject分类中,实现resolveInstanceMethod方法,可以对所有类的实例方法及类方法都进行挽救处理
优点:
任意一个类,只要继承自
NSObject,它的所有方法都可以被监听到我们可以将自定义方法按指定策略进行命名,然后按照相同策略进行监听,只要遇到符合策略的方法无法找到时,可以将其上报服务端,让开发者在第一时间得到问题的反馈
在
NSObject分类中对所有方法统一监听,这种方式符合AOP面向切面的设计模式传统
OOP面向对象设计模式,虽然每一个对象的分工都非常明确,但它们之间一些相同行为,会导致大量的冗余代码。如果我们将其提取,创建公共类进行继承,势必造成强依赖与高耦合而
AOP的优势,对于原始的类与对象无侵入,只要维护好NSObject分类中的监听方法即可
缺点:
在监听方法中写入大量的判断条件,不利于查找与维护
所有的方法都被监听,其中包含了大量的系统方法,造成性能消耗
在
NSObject分类中监听,导致系统提供的消息转发流程无法触发
结论:
- 对于容错处理,我们应该给开发者更大的容错空间。所以我们使用
AOP设计模式,提供的“优化”方案,在这个场景下并不是一个真正的好方案
resolveInstanceMethod两次调用:
第一次:方法动态决议的正常流程
第二次:慢速转发流程
methodSignatureForSelector方法之后,再次触发方法动态决议,系统再给我们一次挽救的机会
