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_msgForward
END_ENTRY __objc_msgForward_impcache
- 在汇编代码中找到,中间只有对
__objc_msgForward
的调用
搜索__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
负责跳转到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 = 0011
mov x3, #3
bl _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 IMP
resolveMethod_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
方法之后,再次触发方法动态决议,系统再给我们一次挽救的机会