方法找不到的报错底层原理
我们在前面 objc_msgSend 消息转发流程探究二 中最后讲到,当在缓存跟方法列表中都找不到对应的 imp
的时候,会把 imp
赋值为 forward_imp
并返回出去。
这里我们全局搜索 __objc_msgForward_impcache
会看到汇编代码的执行,b __objc_msgForward
。
接着我们再搜索 __objc_msgForward
,进到 __objc_msgForward
方法后最后会执行 TailCallFunctionPointer
方法。
这里我们可以看到 TailCallFunctionPointer
的宏定义就是跳转到 $0
,这里 $0
就是 TailCallFunctionPointer x17
传进来的参数 x17
,x17
就是 __objc_forward_handler
。
这里我们再搜索 __objc_forward_handler
会找不到,因为这里执行的不再是汇编代码,我们去掉 __
继续搜索。这里就会来到 objc_defaultForwardHandler
方法。这里会打印报错信息,打印结果就是 + , - 前缀[类名 方法名]: unrecognized selector sent to instance 对象地址 " "(no message forward handler is installed)"
。
对象方法动态决议
上面我们讲到,当 imp
查找不到的时候会来到 objc_defaultForwardHandler
方法,会打印崩溃日志,也就是调用一个没有实现的方法的时候系统会崩溃,但是在调用 objc_defaultForwardHandler
方法之前系统还给了我们一次动态决议的机会,我们可以在这里做一些处理。下面是源码的执行流程。
if (slowpath(behavior & LOOKUP_RESOLVER))
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// No implementation found. Try method resolver once.
// 这个方法是单例方法,只会执行一次
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
}
复制代码
就是执行 objc_defaultForwardHandler
方法之前会执行 lookUpImpOrForward
方法中的这段代码。这里 behavior
是 lookUpImpOrForward
方法的参数,全局这里我们全局搜索可以看到 behavior
为 3, LOOKUP_RESOLVER = 2
,所以 behavior & LOOKUP_RESOLVER = 3 & 2 = 2
,behavior ^= LOOKUP_RESOLVER
等于 behavior = 2 ^ 2 = 0
, 最后再执行 slowpath(behavior & LOOKUP_RESOLVER) 因为behavior为 0,0 与上任何数都为 0,所以不会再进到这个判断了。进到判断里面会执行 resolveMethod_locked
方法。
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};
复制代码
resolveMethod_locked
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//这段代码的意义是当我们找出 imp 的时候,会先在缓存跟方法列表中查找,都找不到的会一般会崩溃,但是如果直接崩溃的话会导致系统不稳定,所以这里苹果的开发者会再次给我一次机会,只要我们在 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior) 之前处理了,系统会再次执行 lookUpImpOrForwardTryCache 方法,再次查找缓存跟方法列表
// 这里会有判断如果不是元类会执行 resolveInstanceMethod 方法,如果是元类就会执行 resolveClassMethod 方法。
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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
这段代码的意义是当我们找出 imp
的时候,会先在缓存跟方法列表中查找,都找不到的会一般会崩溃,但是如果直接崩溃的话会导致系统不稳定,所以这里苹果的开发者会再次给我一次机会,只要我们在 return lookUpImpOrForwardTryCache(inst, sel, cls, behavior)
之前处理了,系统会再次执行 lookUpImpOrForwardTryCache
方法,再次查找缓存跟方法列表
// 这里会有判断如果不是元类会执行 resolveInstanceMethod
方法,如果是元类就会执行 resolveClassMethod 方法。这里我们先来看 resolveInstanceMethod
方法。
resolveInstanceMethod
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))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 在这里系统会自动给当前的类发送 resolve_sel 消息,也就是 resolveInstanceMethod 方法。
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. cls
// 如果在当前类有执行 resolve_sel 并且有做处理,接着就会在当前表里面继续查找一遍
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}
复制代码
在这个方法类名系统会主动给当前的类发送 resolve_sel
消息,如果在当前类有执行 resolve_sel
并且有做处理,接着就会在当前表里面继续查找一遍。执行 lookUpImpOrNilTryCache
方法。
这里我们通过案例来看一下。
@interface LGPerson : NSObject
- (void)say1;
@end
#import "LGPerson.h"
@implementation LGPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s",__func__);
return YES;
}
@end
复制代码
我们定义一个 LGPerson
类,在 .h
文件中声明 say1
方法,而 .m
文件只是重写了 resolveInstanceMethod
方法并且打印函数信息。
这里我们可以看到在日志输入的第三行打印了报错信息,但是在这之前调用了 resolveInstanceMethod
方法。那么我们是不是在 resolveInstanceMethod
方法中做些处理,是不是就可以方法崩溃呢?
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(say1)) {
// 指向 sayHello 方法的 imp 指针
IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
// method
Method method = class_getInstanceMethod(self, @selector(sayHello));
// 类型
const char *type = method_getTypeEncoding(method);
// 这里对 say1 方法添加新的 imp
return class_addMethod(self, sel, sayHelloImp, type);
}
return YES;
}
@end
复制代码
这里我们在 resolveInstanceMethod
方法中把 say1
方法对应的 sel
添加了对应 sayHello
方法的 imp
,当我们再次调用 say1
方法的时候会看到没有报错,而是执行了 sayHello
方法。
类方法的动态决议
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 方法。
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
我们在前面 resolveMethod_locked
方法中讲到,如果是元类就会执行 resolveClassMethod
方法,这里我们就来看一下类方法的动态决议流程。
resolveClassMethod
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// 判断 resolveClassMethod 方法是否存在
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
// 因为类方法存在于元类里面,所以这里对原理进行局部操作,防止方法没有实现
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
// 在这里会对元类发送 resolveClassMethod 消息,那么这里我们想做拦截改怎么拦截呢?这里是对元类发送消息,因为元类的方法是以对象方法存在,我们都知道类的类方法以对象方法的形式存在于元类里面,又因为 resolveClassMethod 为类方法,所以我们在类里面重写 resolveClassMethod 方法就可以拦截
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
}
复制代码
在这里会对元类发送 resolveClassMethod
消息,那么这里我们想做拦截改怎么拦截呢?这里是对元类发送消息,因为元类的方法是以对象方法存在,我们都知道类的类方法以对象方法的形式存在于元类里面,又因为resolveClassMethod
为类方法,所以我们在类里面重写 resolveClassMethod
方法就可以拦截。这里我们通过案例来试一下。
@interface LGPerson : NSObject
- (void)say1;
+ (void)say2;
@end
#import "LGPerson.h"
#import <objc/message.h>
@implementation LGPerson
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(say1)) {
// 指向 sayHello 方法的 imp 指针
IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
// method
Method method = class_getInstanceMethod(self, @selector(sayHello));
// 类型
const char *type = method_getTypeEncoding(method);
// 这里对 say1 方法添加新的 imp
return class_addMethod(self, sel, sayHelloImp, type);
}
return YES;
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(say2)) {
// 指向 sayHappy 方法的 imp 指针
IMP sayHappyImp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayHappy));
// method
Method method = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayHappy));
// 类型
const char *type = method_getTypeEncoding(method);
// 这里对 say2 方法添加新的 imp
return class_addMethod(objc_getMetaClass("LGPerson"), sel, sayHappyImp, type);
}
return YES;
}
@end
复制代码
跟对象方法动态决议类似,我们重写 resolveClassMethod
方法,并判断 sel
为 say2
方法的时候,设置 imp
为指向 sayHappy
方法的 sayHappyImp
,最后我们调用 say2
方法的时候调用了 sayHappy
方法。
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
复制代码
这里我们可以发现与对象方法动态决议有区别的地方,这里还会执行一次 resolveInstanceMethod
方法,这是因为类方法在元类中是以对象方法存在,所以也就会走对象方法的决议流程。所以 resolveInstanceMethod
方法会被调用两次。
#import "NSObject+LG.h"
#import <objc/message.h>
@implementation NSObject (LG)
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (void)sayHappy{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(say1)) {
// 指向 sayHello 方法的 imp 指针
IMP sayHelloImp = class_getMethodImplementation(self, @selector(sayHello));
// method
Method method = class_getInstanceMethod(self, @selector(sayHello));
// 类型
const char *type = method_getTypeEncoding(method);
// 这里对 say1 方法添加新的 imp
return class_addMethod(self, sel, sayHelloImp, type);
} else if (sel == @selector(say2)) {
// 指向 sayHappy 方法的 imp 指针
IMP sayHappyImp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayHappy));
// method
Method method = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayHappy));
// 类型
const char *type = method_getTypeEncoding(method);
// 这里对 say1 方法添加新的 imp
return class_addMethod(objc_getMetaClass("LGPerson"), sel, sayHappyImp, type);
}
return NO;
}
@end
复制代码
所以我们在 NSObject+LG
分类里面,只重写 resolveInstanceMethod
方法,并统一在这里处理也是可以的。
经过上面对前面动态方法的探究,我们会思考一个问题,苹果给我们提供动态决议方法的意义是什么呢?我们总结如下。
- 通过给
NSObject
添加分类的形式,能全局对所有找不到的方法进行监听。- 可以针对自己的模块针对自己定义的方法进行监控,防止崩溃并做些其他处理,同时通过后台上传错误日志,及时对 bug 进行修改。