本文为L_Ares个人写作,以任何形式转载请表明原文出处。
在第九节快速查找流程和第十节慢速查找流程之后,方法的查找流程就已经完成了。
但是在第十节慢速查找流程的done
之前的最后一步仍然没有找到方法的实现,还没有介绍。那一步就是本节的重点——动态决议
,也叫动态方法解析
。
动态决议的目的是 :
给没有找到
imp
的类一次通过resolveInstanceMethod
添加imp
的机会。从而不进行崩溃的信息打印,可以实现imp
的功能。
其实上一节我们已经见过它,就是没有走到它的里面去。因为上一节的方法都是实现了的,我们是可以在继承链上找到的,那么这一节,就不让方法实现,去看一下,慢速查找流程中,走到了动态协议是什么情况。
一、找到动态决议的入口
就是上一节的步骤,这里就多写一遍,免得再去找了吧。
还是要用到objc4-781源码。创建两个类 : JDPerson
和JDStudent
。其中,JDStudent
继承于JDPerson
。
@interface JDPerson : NSObject
- (void)studyWork;
@end
@implementation JDPerson
@end
@interface JDStudent : JDPerson
- (void)doHomeWork;
+ (void)tryYourBest;
+ (void)toImpTry;
@end
@implementation JDStudent
- (void)doHomeWork
{
NSLog(@"doHomeWork");
}
+ (void)toImpTry
{
NSLog(@"toImpTry");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDStudent *student = [[JDStudent alloc] init];
[student studyWork]; //挂上断点
NSLog(@"Hello, World!");
}
return 0;
}
可以看到-(void)studyWork
方法我并没有实现。那么我们进入断点并打开汇编。
然后给objc_msgSend
那行挂上断点并走到断点上,然后按住control
,点击step into
。
又来到了_objc_msgSend_uncached
。给这行挂上断点,并且走到断点上,继续按住control
,点击step into
。
依然是lookUpImpOrForward
。全局搜索lookUpImpOrForward
,找到它在runtime
中的实现。
于是我们又来到了上一节中的位置,这就证明,无论方法是否有实现,lookUpImpOrForward
都是在快速查找流程之后,进行慢速查找流程或者动态决议的必经之路。
拉到done
之前,找到如下图的代码片段 :
这就是我们探索动态决议的入口。
二、动态决议的实现
从图5我们可以看到,动态决议的这一步主要就是return
里面的那个函数,点进去。
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
//检查锁
runtimeLock.assertLocked();
//判断类是否合法
ASSERT(cls->isRealized());
//解锁
runtimeLock.unlock();
//判断当前的cls是不是元类
if (! cls->isMetaClass()) {
//不是元类,则决议解析实例方法
resolveInstanceMethod(inst, sel, cls);
}
else {
//如果是元类,则类方法决议解析
resolveClassMethod(inst, sel, cls);
//如果还是没有找到
if (!lookUpImpOrNil(inst, sel, cls)) {
//调用元类的实例方法的动态方法解析
resolveInstanceMethod(inst, sel, cls);
}
}
//调用的动态解析可能已经填充了缓存,所以再尝试一次lookUpImpOrForward慢速查找
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
思路 :
先判断锁的情况和类的合法性,类是否实现。
(1). 检查传入的类不是元类。使用实例方法的动态决议。
(2). 检查传入的类是元类。使用类方法的动态决议。如果在元类的类方法的动态决议还是没有找到。因为类方法在元类中属于实例方法,所以再找元类的实例方法的动态决议。
(3). 在动态决议的过程中可能imp
已经填充到了缓存,所以再执行一次慢速查找流程。
那么这里就要分情况看实例方法的动态决议
和类方法的动态决议
。
1. 实例方法的动态决议
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
//还是先检查锁和类是否实现
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//定义一个SEL变量,保存`resolveInstanceMethod`方法
SEL resolve_sel = @selector(resolveInstanceMethod:);
//到慢速查找流程中找当前类的父类是否实现了resolveInstanceMethod
//如果没有实现,直接就return了,所以这个一定会实现的,不然就不用动态决议了,直接就跳出函数了
//resolveInstanceMethod在全局搜索可以在NSObject中找到,也就是说NSObject已经帮我们实现了
//沿着继承链查找,最后到根类的时候一定会找到这个方法的实现的,而且返回的是一个BOOL值NO,可以自行查找一下看看
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
//定义了msg就是一个objc_msgSend发送消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//像当前的类cls发送一条消息,resolveInstanceMethod函数,并且参数是你的`sel`
//这句代码会直接查询快速流程,因为上面说过了,NSObject实现了resolveInstanceMethod
//在上面那句代码调用lookUpImpOrNil的时候,还记得done里面做了什么吧,直接把对应的sel-imp存入到了当前的cls里面吧
//所以这里的msg也就是objc_msgSend在快速查找的时候查找缓存就会找到当前的cls里面存在的resolveInstanceMethod是否实现
bool resolved = msg(cls, resolve_sel, sel);
//再次查找sel的imp有没有被你在cls的继承链上实现,如果有,肯定会返回imp,如果没有就只能返回nil了
IMP imp = lookUpImpOrNil(inst, sel, cls);
//如果resolveInstanceMethod在cls里面实现了
if (resolved && PrintResolving) {
//如果sel也找到了imp
if (imp) {
//会打印这个imp是实例方法还是类方法和sel名字等信息
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// 如果没有找到imp,证明cls在+resolveInstanceMethod方法里面什么都没做。
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}
注释一定要看,不然看不懂这里的思路。
思路 :
先知道
+ (BOOL)resolveInstanceMethod:(SEL)sel
这个方法是在NSObject
中实现的,默认返回的是NO
。因为函数的开始执行了一次
lookUpImpOrNil
,其实就是调用了lookUpImpOrForward
慢速查找流程,查找cls
继承链上的resolveInstanceMethod
方法实现。因为当前类cls
一定会因为根类是NSObject
,所以一定可以在NSObject
找到resolveInstanceMethod
的实现。从而会走到lookUpImpOrForward
的done
方法里面,将resolveInstanceMethod
放入cls
的方法缓存。然后利用
objc_msgSend
发送消息给cls
去寻找sel = resolveInstanceMethod
的imp
实现。并获取到一个BOOL
值判断cls
有没有实现resolveInstanceMethod
。
(1). 所以你可以在cls
里面实现resolveInstanceMethod
,添加sel
的imp
。下面会举例说明。
(2). 如果你不在cls
里面添加sel
的imp
,那么cls
的sel
还是找不到imp
。再次利用
lookUpImpOrNil
也就是lookUpImpOrForward
慢速查找cls
的sel
是否有对应的imp
。
(1). 如果有,那么就会执行imp
。
(2). 如果没有,证明resolveInstanceMethod
没有被你自定义实现,还是返回的默认的NO
。sel
也依然找不到对应的imp
。
举例 :
还是文章最开始的那段代码,我在@implementation JDStudent
里面添加+ (BOOL)resolveInstanceMethod:(SEL)sel
,并且给它一个实现。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"进入到了动态方法解析");
if (sel == @selector(studyWork)) {
NSLog(@"手动添加imp实现");
IMP swIMP = class_getMethodImplementation(self, @selector(doHomeWork));
Method swMethod = class_getInstanceMethod(self, @selector(doHomeWork));
const char* swType = method_getTypeEncoding(swMethod);
class_addMethod(self, sel, swIMP, swType);
}
return [super resolveInstanceMethod:sel];
}
你会看到控制台的结果是 :
这就证明,虽然JDPerson
没有实现studyWork
,它的子类JDStudent
在快速流程和慢速流程中都找不到sel == studyWork
的imp
实现,但是经过动态决议,手动的将studyWork
的imp
添加了JDStudent
的doHomeWork
的实现。
如果不添加if
里面的内容则会出现如下图所示的错误 :
这个错误就是在慢速查找流程中imp = forward_imp = (IMP)_objc_msgForward_impcache
拿到的错误信息。
问题 :
但是这个里面还有一个问题,如果不手动添加sel
的imp
的情况下,我明明就只执行了一次动态决议的方法resolveInstanceMethod
,为什么打印出来是执行了两次?
这只能证明系统在其他的地方还是做过了处理。先不看这个问题,先继续动态决议的主线流程,这个问题在下一节再说。
2. 类方法的动态决议
在最开始的代码的main.m
中,我们让类JDStudent
执行类方法+ (void)tryYourBest;
类方法tryYourBest
依然是没有实现的。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
JDStudent *student = [[JDStudent alloc] init];
[student studyWork];
[JDStudent tryYourBest];
NSLog(@"Hello, World!");
}
return 0;
}
查看结果。
不是给JDStudent
添加了动态决议的实现了吗?为什么没走进去呢?那就证明现在的resolveInstanceMethod
对类方法没生效。
于是就回到了这里。
于是我们来看resolveClassMethod
方法。
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
//检查锁、类的实现、cls是不是元类
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//第一步和实例方法的动态决议思路是一样的。请看实例方法的动态决议的注释
//不一样的是,这次检查的是resolveClassMethod。
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
//这个就是获取cls的元类,会判断cls是不是就是元类,如果是直接返回cls,如果不是,则找cls的元类并且返回给nonmeta
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);
}
}
//类方法是不是相当于元类的实例方法,类相当于元类的实例类对象,所以下面的流程是和实例方法一模一样的,请移步上面的实例方法的注释
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 = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
思路 :
其实大体的思路和实例方法的动态决议是一样的。
所以是不是我们可以在JDPerson
中添加resolveClassMethod
的实现,来解决imp
找不到的问题。
于是在JDPerson
的@implementation
中添加+ (BOOL)resolveClassMethod:(SEL)sel
,并实现
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"进入到了类的动态解析方法");
if (sel == @selector(tryYourBest)) {
NSLog(@"手动添加类方法的imp实现");
IMP trIMP = class_getMethodImplementation(objc_getMetaClass("JDStudent"), @selector(toImpTry));
Method trMethod = class_getInstanceMethod(objc_getMetaClass("JDStudent"), @selector(toImpTry));
const char* trType = method_getTypeEncoding(trMethod);
return class_addMethod(objc_getMetaClass("JDStudent"), sel, trIMP, trType);
}
return [super resolveClassMethod:sel];
}
结果 :
的确解决了问题。那么如果还是只给实现resolveClassMethod
但是不给tryYourBest
添加imp
呢?
一样会出现和实例方法的动态决议相同的情况。 这个还是放在后面一节和实例方法的问题一起说。
3. 关于一些操作的优缺点
这个思路网上很多人也说过了,因为类方法的动态决议resolveClassMethod
如果没有找到实现的话,还是会检查元类的resolveInstanceMethod
方法,所以其实在元类里面实现resolveInstanceMethod
的话,是不是就不用写两个了?
但是元类不是我们可以操控的呀,于是就找到了更下一层的NSObject
,然后就开始对NSObject
加上分类,在NSObject
的分类里面实现resolveInstanceMethod
。反正底层在找sel
-imp
的时候不是根据什么实例方法和类方法来找的,都是根据name
来找的吧,那只要判断name
就可以了嘛。
但是这样是有缺点的,如果没有实现的方法很多,难道全部都跑到NSObject
的分类里面去处理吗?耦合性太高了吧,而且还有一堆的if
判断,读写起来都困难。
所以也提供了一种在方法名前面统一前缀的方式,比如某一个类的方法名前面全都是jd_
,这个是不是也可以?利用NSStringFromSelector
拿到方法名的字符串,然后判断字符串是不是以jd_
开头的,如果是的话,做统一的处理。
但是呢,如果碰到其他人直接在上层的类里面就做了resolveInstanceMethod
的实现,那是不是又没用了,所以优缺点都是有的,怎么去使用,或者说大家怎么配合,也是能尽量避坑的关键。