前言
之前写到方法查找,当调用一个方法是,会先到类的cache
里进行快速查找,找到了就直接执行对应的imp。如果没有找到,会到类的bits
里去进行慢速查找,找到了就会到cache里进行插入。如果这一步也没有查找到,就会将imp赋值为forward_imp
。那么赋值完了以后,接下来又会怎么操作呢,一起去探索一下。
resolveInstanceMethod
_objc_msgForward_impcache
先来看看这个forward是什么。
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
复制代码
它就是_objc_msgForward_impcache
,全局搜索找一找这个在哪里实现的。
在汇编代码里找到它的实现,里面就执行了__objc_msgForward
,而__objc_msgForward
的大概意思就是执行x17
,x17
又是来自__objc_forward_handler
,接着找下去。但是会发现通过__objc_forward_handler
找不到,可能是这个方法不是汇编写的,所以把前面的下划线去掉,再次全局搜索。
可以看到_objc_forward_handler
就是objc_defaultForwardStretHandler
,而其中就只有一句,是不是很熟悉,没错这就是我们经常看到的找不到方法的错误信息打印。
但是通过lookUpImpOrForward()代码可以看到,其实还没有直接执行到这一步来,而是进行了一个判断。
先整理一下,如果没有找到imp,且cls的父类为nil
的时候,就会给imp赋值为forward_imp
,并且break出循环,然后就会走到这一句。画个图看看behavior
是多少。
这一步主要是对behavior
进行处理,后面还会再对其进行使用,也是一个比较关键的变量。下面来看resolveMethod_locked()
。
这里主要是做了一个判断,传进来的cls是否是元类,然后再根据情况调用resolveInstanceMethod()
或者resolveClassMethod()
,最后再调用lookUpImpOrForwardTryCache()
并返回结果。
这些操作也叫做消息动态决议,这也是苹果在遇到没有实现的方法时给的一次机会。
resolveInstanceMethod()
这是它的实现,下面折叠部分是一些日志打印,不重要。
可以看到这里就对进行了一个消息发送,发送的方法是resolveInstanceMethod:
,可以看到这个待发送的和当前方法名称相同,但是它们不是一个,因为待发送的方法有一个冒号。
然后尝试查找resolveInstanceMethod:
有没有实现,这个判断基本是不会为真的,因为系统(也就是NSObject)有实现resolveInstanceMethod:
,不过只返回了一个NO,还是需要我们自己去处理。
接下来对resolve_sel
进行消息发送,并返回是否有处理,然后再对sel
进行lookUpImpOrNilTryCache()
查找,并得到查找的imp
,下面就是根据imp
是否存在做出一些日志打印。
先来看看lookUpImpOrNilTryCache()
做了什么。
它就是调用了_lookUpImpTryCache()
,传入的参加基本都没有变化,只有behavior
做了处理,之前得到behavior = 1
,现在传入behavior | LOOKUP_NIL
,算出来传入的behavior = 5
。
在_lookUpImpTryCache()
中,先判断cls
是否初始化,没有的话直接走lookUpImpOrForward()
慢速查找(按照正常顺序走过来的话,cls
基本都是初始化了的)。
然后再进行快速查找,快速查找有了就直接goto done
,如果没找到再去共享缓存中查找,共享缓存没有再进行慢速查找,并且直接返回慢速查找的结果,这里behavior = 5
了(此时再进入慢速查找时判断behavior & LOOKUP_RESOLVER就是5 & 2 = 0
,就不会再进入resolveMethod_locked()
,不然就会无限递归下去)。
done
的地方判断behavior & LOOKUP_NIL(5 & 4 = 4)
,并且imp == (IMP)_objc_msgForward_impcache
(也就是之前看过的没有找到就会给imp赋值一个错误打印日志的sel),最后再返回nil。
Tips: 可以看到在
resolveInstanceMethod()
里已经调用了一次_lookUpImpTryCache()
,为什么还会在resolveMethod_locked()
的结尾再调用一次呢。
其实主要是因为可能存在多线程的问题,如果有其他线程先缓存好了,那么就要确保能最快的获取到,也是一个为了提升速度的操作。
接下来看看要怎么实现resolveInstanceMethod:
才不会报错。
实例方法动态决议例子
#import "DDAnimal.h"
#import <objc/message.h>
@interface DDAnimal : NSObject
- (void)run;
@end
@implementation DDAnimal
- (void)sayRun{
NSLog(@"%@ - %s",self , __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
NSLog(@"resolveInstanceMethod: %@ - %@",self,NSStringFromSelector(sel));
IMP sayRunImp = class_getMethodImplementation(self, @selector(sayRun));
Method method = class_getInstanceMethod(self, @selector(sayRun));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayRunImp, type);
}
return [super resolveInstanceMethod:sel];
}
@end
复制代码
上面这个例子可以看到run只有声明,没有实现,如果直接调用run肯定是会报错的。
现在加上+ (BOOL)resolveInstanceMethod:(SEL)sel
,在这个方法里先判断sel是否为run
,然后拿到事先写好的sayRun实现
,并且把这个实现添加到sel上,这样sel就有了实现,再次调用就不会报错了。
resolveClassMethod()
如果类是元类,那么就会调用resolveClassMethod()
,也就是当前sel是一个类方法。
这里为什么执行了一次resolveClassMethod()
,还要再执行一次resolveInstanceMethod()
呢?
主要是因为类方法不仅会在类中以类方法
的形式存在,还会在元类里以实例方法
的形式存在。所以如果在类方法里找不到,就会到元类里去找实例方法的处理。这些都可以在isa和继承的走势图里看到。
接下来看看它的代码实现。
可以看到它和resolveInstanceMethod()基本一样,只是需要对元类进行一个处理,看看它是否实现。
类方法动态决议例子
#import "DDAnimal.h"
#import <objc/message.h>
@interface DDAnimal : NSObject
+ (void)jump;
@end
@implementation DDAnimal
+ (void)sayJump {
NSLog(@"%@ - %s", self, __func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(jump)) {
id meta = objc_getMetaClass("DDAnimal");
IMP sayJumpImp = class_getMethodImplementation(meta, @selector(sayJump));
Method method = class_getInstanceMethod(meta, @selector(sayJump));
const char *type = method_getTypeEncoding(method);
return class_addMethod(meta, sel, sayJumpImp, type);
}
return [super resolveClassMethod:sel];
}
@end
复制代码
注意一点: 因为是类方法,所以要去元类获取。
总结
当我们调用一个方法之后,系统底层实际是进行了消息发送
,然后会快速查找
到cache
里去找,如果没找到就会慢速查找
到methodList
里去找,如果还没有找到,就会进行消息动态决议
,看看是否实现了resolveClassMethod:
或者resolveInstanceMethod:
,如果还是没有就会进行快速消息转发
和慢速消息转发
(不过目前还没有讲到消息转发),最后都没有找到就会报错unrecognized selector sent to class的错误,直接崩溃,整个流程结束。