前言
在之前的文章中,我们探索了方法的快速查找
和方法的慢速查找流程
,那么如果在调用objc_msgSend
的时候,经过快速查找和慢速查找依旧没有找到的话,系统接下来会怎么处理呢?我们在之前也稍微提到过一下,系统会给我们一次补救的机会,也就是动态方法决议
流程,今天我们就来探索一下这个动态方法决议
动态方法决议
resolveMethod_locked方法
我们来回顾下上篇文章中的部分代码
//**这个时候传入的behavior为LOOKUP_INITIALIZE|LOOKUP_RESOLVER**
//**条件成立,会进入,然后重置behavior为LOOKUP_INITIALIZE**
//**再次调用则不会进入**
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
//**动态方法决议流程**
return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
当没有方法的慢速查找
没有找到目标imp
时,系统会调用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方法**
resolveInstanceMethod(inst, sel, cls);
}
else {
//**是元类,调用resolveClassMethod**
resolveClassMethod(inst, sel, cls);
//**如果调用上面的方法还没有找到,尝试调用resolveInstanceMethod**
//**原因是根据isa的继承链,根元类的父类是NSObject,所以在元类中如果没有找到**
//**最后可能会在NSObjct中找到目标方法**
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
//**重新调用lookUpImpOrForwardTryCache方法,返回方法查找流程**
//**因为已经进行过一次动态方法决议,下次将不会再进入,所以不会造成死循环**
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
很简单的一个方法,里面实际上就是判断我们传入的类
是否是元类
,然后再调用不同的方法,我们先看看查找实例方法
的流程,其中最重要的就是resolveInstanceMethod
方法
resolveInstanceMethod方法
我们来看看resolveInstanceMethod
的源码
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
//**如果目标类没有初始化,直接报错**
ASSERT(cls->isRealized());
//**创建一个方法名为resolveInstanceMethod的SEL**
SEL resolve_sel = @selector(resolveInstanceMethod:);
//**判断resolveInstanceMethod是否在目标类中实现**
//**如果我们的类是继承自NSObject的话,那么这个判断就永远为false**
//**因为在NSObject中已经默认实现了resolveInstanceMethod方法**
//**因为是去cls->ISA也就是元类中查找,所以我们可以断定,resolveInstanceMethod是个类方法**
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
//**强制类型转换objc_msgSend**
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//**通过objc_msgSend方法调用resolveInstanceMethod**
bool resolved = msg(cls, resolve_sel, sel);
//**拼接上LOOKUP_NIL参数后,重新调用方法查找流程**
//**虽然调用了resolveInstanceMethod,但是这个返回值是bool**
//**所以我们要获取对应的imp,还是需要通过方法查找流程**
//**如果通过resolveInstanceMethod添加了方法,就缓存在类中**
//**没添加,则缓存forward_imp**
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
//**组装相应的信息**
if (resolved && PrintResolving) {
if (imp) {
………
}
else {
………
}
}
}
复制代码
这个方法也相对比较简单
- 首先创建一个方法名为
resolveInstanceMethod
的SEL
对象resolve_sel
; - 然后判断
resolve_sel
是否实现,如果继承NSObject
则必定已经实现,这里通过cls->ISA
可以知道,resolveInstanceMethod
是个类方法
; - 通过
objc_ msgSend
直接调用resolveInstanceMethod
方法,因为是直接对cls
发送消息,所以也可以看出resolveInstanceMethods
是类方法
; - 调用
lookUpImpOrNilTryCache
方法,重新返回到方法查找的流程当中去;
resolveClassMethod方法
我们再来看看类方法的流程
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//**判断resolveClassMethod是否实现,NSObject中默认实现**
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);
}
}
//**强制类型转换objc_msgSend**
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//**通过objc_msgSend调用类中的resolveClassMethod方法**
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
//**拼接上LOOKUP_NIL参数后,重新调用方法查找流程**
//**类方法实际上就是元类对象中的对象方法,所以可以通过方法查找流程在元类中查找**
//**如果通过resolveClassMethod添加了,就缓存方法在元类中**
//**没添加,则缓存forward_imp**
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
……
}
else {
……
}
}
}
复制代码
这个方法与resolveInstanceMethod
比较类似,如果通过resolveClassMethod
方法添加了目标imp
,则将其缓存在目标元类
中,否则缓存forward_imp
。
lookUpImpOrNilTryCache方法
我们接下来看看,在resolveInstanceMethod
和resolveClassMethod
中都会调用的lookUpImpOrNilTryCache
方法
extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
//**这里behavior没传,所以是默认值0**
//**behavior | LOOKUP_NIL = 0 | 4 = 4**
return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
复制代码
非常简单的方法,就是给参数behavior
拼接上LOOKUP_NIL
然后调用_lookUpImpTryCache
方法。
_lookUpImpTryCache方法
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
//**判断类是否初始化**
if (slowpath(!cls->isInitialized())) {
//**没有初始化直接调用lookUpImpOrForward**
//**里面针对没初始化的类,有相关处理**
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//**去缓存中,查找sel是否有对应的imp**
IMP imp = cache_getImp(cls, sel);
//**找到了则跳去done**
if (imp != NULL) goto done;
//**没找到继续往下走,去共享缓存中查找**
#if CONFIG_USE_PREOPT_CACHES
if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
}
#endif
//**没找到对应的imp,调用lookUpImpOrForward方法查找,behavior = 4**
//** 4 & 2 = 0 ,所以这次方法查找不会再次进行动态方法决议**
//**将_objc_msgForward_impcache缓存起来,方便下次直接返回**
if (slowpath(imp == NULL)) {
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//**命中缓存中,并且sel对应的imp为_objc_msgForward_impcache**
//**说明动态方法决议已经执行过,且没有添加imp,则直接返回空**
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
//**说明动态方法决议中添加了对应的imp**
return imp;
}
复制代码
这个方法的流程
- 首先判断
类
是否初始化,如果没有初始化则直接调用lookUpImpOrForward
,里面有针对没初始化的类
进行相应的处理; - 然后去缓存中进行
方法的快速查找
,找到了就去done
- 缓存中没找到,如果支持共享缓存,则去共享缓存中查找
- 都没有查找到,则通过慢速方法查找去查找方法,
behavior = 4
,这次慢速查找不会再次调用动态方法决议
- 在
done
流程中,如果已经执行过动态方法决议
且并没有添加imp
,则缓存中的sel
对应imp
为_objc_msgForward_impcache
,这时直接返回nil
。否则返回添加的imp
实现。
到此,不管是类
还是元类
的动态方法决议
流程就已经走完了,实际上就是系统在快速方法查找
和慢速方法查找
都没有找到的情况下,给我们的一次补救的机会,最后还是会再次调用方法查找流程,并且再次调用的过程中不会再次调用动态方法决议
流程,避免死循环
动态方法决议举例
首先请出我们的老朋友DMPerson
@interface DMPerson : NSObject
- (void)sayHello;
+ (void)sayBye;
@end
@implementation DMPerson
@end
复制代码
实例方法举例
实现resolveInstanceMethod方法
在DMPerson
中实现resolveInstanceMethod
方法
@implementation DMPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"--进入%@--",NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
DMPerson *p = [DMPerson alloc];
[p sayHello];
}
return 0;
}
复制代码
接着我们运行代码,查看结果
理所当然的,我们的程序崩溃了,但是我们在崩溃前,我们看到了我们在resolveInstanceMethod
打印的东西,说明确实进入了我们的resolveInstanceMethod
,那么我们自然就能够在其中进行一些奇奇怪怪的操作,来避免崩溃了。但是我们还发现resolveInstanceMethod
调用了两次,这是为什么呢?我们来看看堆栈信息。
首先,当第一次进入resolveInstanceMethod
时,我们看到确实是我们之前所说的流程,当快速方法查找
和慢速方法查找
都没找到的时候,系统会给我们一次机会,进行动态方法决议
。
而第二次进入resolveInstanceMethod
,我们发现并不是从main
函数直接进入的,中间有个__forwarding__
函数的调用,这个函数是系统在进行消息转发
时底层调用的,所以我们暂且认为是系统在消息转发
的过程中,再次调用了resolveInstanceMethod
方法。具体的我们在消息转发
的探索中,再进行说明。
动态添加sayHello
既然我们已经确定了当慢速方法查找
没找到后,系统会给我们机会来弥补我们的错误,那么我们就要开始进行我们的补救操作了。
@implementation DMPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"--进入%@--",NSStringFromSelector(sel));
if (sel == @selector(sayHello)) {
IMP imp = class_getMethodImplementation(self, @selector(sayGood));
Method method = class_getInstanceMethod(self, @selector(sayGood));
const char *type = method_getTypeEncoding(method);
//**动态给类添加`sayHello`的IMP为`sayGood`**
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- (void)sayGood
{
NSLog(@"%s",__func__);
}
@end
复制代码
接下来运行代码咯
当我调用[DMPerson sayHello]
的时候,并没有崩溃,而是打印输出了sayGood
。
类方法举例
实现resolveClassMethod方法
在DMPerson
中实现resolveClassMethod
方法
@implementation DMPerson
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"--进入%@--",NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
DMPerson *p = [DMPerson alloc];
[DMPerson sayBye];
}
return 0;
}
复制代码
同样运行代码
同样熟悉的味道,证明确实是方法未找到的情况下,进入了resolveClassMethod
。
动态添加sayNO
@implementation DMPerson
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"--进入%@--",NSStringFromSelector(sel));
if (sel == @selector(sayBye)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("DMPerson"), @selector(sayNO));
Method method = class_getInstanceMethod(objc_getMetaClass("DMPerson"), @selector(sayNO));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("DMPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
+ (void)sayNO
{
NSLog(@"%s",__func__);
}
@end
复制代码
由于类方法
实际上是存储在元类
中的,所以我们需要将方法添加到DMPerson
的元类
中去,然后看看运行结果
成功的打印了sayNO
,动态添加成功。
在NSObject中实现resolveInstanceMethod
我们成功的在实例方法
和类方法
的动态方法决议
流程中,成功的动态添加了相应的IMP
,让系统不会在没找到方法实现
的情况下直接崩溃。但是现在我们有个疑问
我既然都能够在类里面动态决议了,这么费死劲的动态添加它,为什么我不直接实现这个方法呢?
接下来我们介绍动态方法决议
的一种应用场景,我们对NSObject建个分类DMPerson+DM
,然后在其中实现resloveInstanceMethod
方法
@implementation NSObject (DM)
- (void)sayGood {
NSLog(@"%s",__func__);
}
+ (void)sayNO {
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(sayHello)) {
IMP imp = class_getMethodImplementation(self, @selector(sayGood));
Method method = class_getInstanceMethod(self, @selector(sayGood));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, imp, type);
}
if (sel == @selector(sayBye)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("DMPerson"), @selector(sayNO));
Method method = class_getInstanceMethod(objc_getMetaClass("DMPerson"), @selector(sayNO));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("DMPerson"), sel, imp, type);
}
return NO;
}
@end
复制代码
然后我们把之前DMPerson
中的动态方法决议
的代码都注释掉,然后在main
中调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
DMPerson *p = [DMPerson alloc];
[p sayHello];
[DMPerson sayBye];
}
return 0;
}
复制代码
看看打印结果
不管是类方法
还是实例方法
,我都成功的避免了他的崩溃。他的原因很简单
- 如果是实例方法,那么根据
isa的继承链
最后会在NSObject
中找到resolveInstanceMethod
方法并调用,因此我们在NSObject
中动态添加方法是可行的。 - 如果是类方法,由于
根元类
的父类是NSObject
的原因,所以还会调用一次resolveInstanceMethod
,最后在NSObject
中找到并执行。
那么通过这种方式,我们就能够将我们应用内所有的未找到方法导致的崩溃,全都避免了崩溃,并且能够收集起来,最后上传服务器,方便我们进行修复。
总结
整个方法的查找流程我们已经探索了大半,这是一个漫长而又复杂的过程。而系统把这个流程做的这么复杂的原因,就是为了保持系统的稳定性,所以给了我们一次机会去进行弥补。那么如果我们在动态方法决议
流程中依旧没有弥补,接下来系统还会给机会吗?答案是,会的。那就是接下来我们要继续探索的消息转发流程
,我们将在下篇文章中,继续进行探索。