前言
上一篇文章慢速方法查找一文详细分析了消息慢速查找的流程,当在找不到的时候imp = forward_imp(消息转发),那么这篇文章主要就是探索消息转发的过程,以及我们可以在这过程中可以做出哪些灵性的处理。动态方法决议又是怎么实现的?带着问题开始我们的探索吧!!哈哈
动态方法决议
通过汇编的断点可以得知,当imp没有找到的时候会进入libobjc.A.dylib_objc_msgForward_impcache方法,那么上篇文章_lookUpImpOrForward慢速方法查找已经知道其中的逻辑如下:
//动态方法决议
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//behavior = 3 LOOKUP_RESOLVER = 2
//3^2 = 1
behavior ^= LOOKUP_RESOLVER;
//动态方法决议
return resolveMethod_locked(inst, sel, cls, behavior);
}
-
behavior
上篇文章已经分析,值中有LOOKUP_INITIALIZE|LOOKUP_RESOLVER
进入后异或LOOKUP_INITIALIZE|LOOKUP_RESOLVER^ LOOKUP_RESOLVER = LOOKUP_INITIALIZE
,相当于清空了LOOKUP_RESOLVER
。 -
resolveMethod_locked
的最后一个参数就是LOOKUP_INITIALIZE
。
resolveMethod_locked源码分析
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 {//这里是cls是元类
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
的时候,就会进入动态方法决议查找的流程,即resolveMethod_locked
方法。 - 查找的是实例方法则进行对象方法动态决议
resolveInstanceMethod
。 - 查找的流程会根据
isa
的走位来进行查找,类->元类->根元类
。 - 如果都没找到,最后会调用
lookUpImpOrForwardTryCache
查找(重新查找一遍)。
补充:查找的是类方法
则先进行类方法动态决议resolveClassMethod
,再执行resolveInstanceMethod
(这里resolveInstanceMethod
调用与实例方法的resolveInstanceMethod
参数不同。)。
lookUpImpOrNilTryCache源码分析
IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
return _lookUpImpTryCache(inst, sel, cls, behavior);
}
ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertUnlocked();
//判断类是否已经初始化,正常情况下类是已经初始化了,所以这个判断基本不会进入
if (slowpath(!cls->isInitialized())) {
// see comment in lookUpImpOrForward
//进入慢速消息查找流程,因为已经查找动态决议方法,之后behavior = LOOK_INITIALIZE
//没有了动态决议的参数(LOOK_RESOLVER)
return lookUpImpOrForward(inst, sel, cls, behavior);
}
//进行缓存快速查找
IMP imp = cache_getImp(cls, sel);
//缓存中查找到imp,直接进行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
if (slowpath(imp == NULL)) {
//imp不存在的话,继续进行消息的慢速查找流程
return lookUpImpOrForward(inst, sel, cls, behavior);
}
done:
//判断消息是否已经转发,_objc_msgForward_impcache方法会讲方法写进缓存
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
return nil;
}
//返回imp,在之后的流程处理
return imp;
}
-
isInitialized
用来判断类(cls)
是否已经初始化,一般情况下是不会进入
的。 - 先去查找缓存
(cache)
中的imp
,有的话就返回imp
。 - 没有在缓存
(cache)
中找到的话,就会尝试在共享缓存
中查找,找到就返回imp
。 - 仍然没有会进行
lookUpImpOrForward
也就是再进行一次慢速消息查找。 -
lookUpImpOrNilTryCache
的主要作用通过LOOKUP_NIL
来控制插入缓存,不管sel
对应的imp
有没有实现,还有就是如果imp
返回了有值那么一定是在动态方法决议中动态实现了imp
。
注意:既然这个函数也是进行快速
和慢速
消息查找的,那么就说明resolveInstanceMethod
与resolveClassMethod
可以在某个时机将方法加入类中(加入到cache
)。这样后面方法的调用才有意义。
对象方法动态决议 resolveInstanceMethod
通过以上的分析,就是在快速
和慢速
消息查找过程中找不到imp
的话,苹果仍然会给机会给我们在resolveInstanceMethod
方法中进行处理,那么我们可以猜想在resolveInstanceMethod
会在类中添加imp
,请往下看resolveInstanceMethod
方法源码:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
//先进行元类查找,是否实现了resolveInstanceMethod实例方法,也就是类方法。
//没有实现的话就返回,但是这里不返回,原因是NSObject默认实现了resolveInstanceMethod
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//发送消息(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
//imp的快速和慢速的查找流程,这里不会进行动态转发
IMP imp = lookUpImpOrNilTryCache(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 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));
}
}
}
- 首先判断
cls
是否为元类。 - 先进行元类
resolveInstanceMethod
方法的查找,找到之后会进行缓存。 - 系统会自动给类(
cls
)发送resolveInstanceMethod
消息,既然是给类发送消息,那么resolveInstanceMethod
是类方法。(+resolveInstanceMethod
)。 - 接着进行
imp
的快速和慢速的查找流程,但是resolveInstanceMethod
方法没有返回imp
,原因在于这里不需要返回只需要对缓存进行更新的处理。 -
lookUpImpOrNilTryCache
与lookUpImpOrForwardTryCache
唯一区别就是是否进行动态转发
,这里是不进行
。 - 可以看到返回的
resolved
只是进行了日志打印。也就是resolved
返回YES/NO
对功能没有影响。
注意:如果没有实现,缓存中没有,进入lookUpImpOrForward
查找,sel
没有查找到对应的imp
,此时imp = forward_imp
,动态方法决议只调用一次
,此时会走done_unlock
和done
流程,既sel
和forward_imp
插入缓存,进行消息转发。
类方法resolveInstanceMethod动态决议
static void resolveClassMethod(id inst, SEL sel, Class cls)
{ //inst->对象 cls->类 sel->方法编号
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
//查询元类是否实现,NSObject默认实现了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);
}
}
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);
if (resolved && PrintResolving) {
//以下是进行一些日志的输出
if (imp) {
...
}
else {
// Method resolver didn't add anything?
...
}
}
}
-
resolveClassMethod
在NSobject
中已经实现,只要元类初始化
就可以了,目的是缓存在元类中
。 - 调用
resolveClassMethod
类方法,目的是实现可能resolveClassMethod
方法中动态实现sel
对应的imp
。 -
imp = lookUpImpOrNilTryCache(inst, sel, cls)
缓存sel
对应的imp
,不管imp
有没有动态添加,如果没有缓存的就是forward_imp
。
resolveInstanceMethod实例探究
创建XXPerson类,声明sayLost方法,但是不进行实现,代码如下:
@interface XXPerson : NSObject
-(void)sayLost;
@end
在XXperson.m中添加resolveInstanceMethod方法,并打印相关的信息,代码如下:
#import "XXPerson.h"
@implementation XXPerson
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"--xjl--%@",NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
进行sayLost调用之后发现如下:
疑问:在崩溃之前确实调用了
resolveInstanceMethod
方法,而且调用了2
次,这是为什么呢?众所周知,第一次系统会走动态方法决议,NSObject
调用resolveInstanceMethod
,那么第二次呢?
打开函数调用栈查看情况如下:(注意栈是先进后出的
)
- 第一次进入
resolveInstanceMethod
时查看堆栈信息,发现走的是慢速查找
流程的动态决议方法。
- 由上图可知,第二次调用
resolveInstanceMethod
是由系统库coreFoundation
调起的。在消息转发完成之后再次开启了慢速查找流程,进入动态方法决议又调用了一次resolveInstanceMethod
,所以总共调用了两次,第二次调用的详细流程会在后面详细分析。
动态添加sayLost方法
- 当第一次进来
resolveInstanceMethod
方法的时候,我们动态添加了sayLost
方法,lookUpImpOrForwardTryCache
直接获取imp
,直接调用imp
,查找流程结束。 - 动态方法协议成功之后程序
崩溃
情况也得到了解决,这是系统给开发者容错的机会。
具体流程:resolveMethod_locked
--> resolveInstanceMethod
--> 调用resolveInstanceMethod
--> lookUpImpOrNilTryCache(inst, sel, cls)
--> lookUpImpOrForwardTryCache
--> 调用imp
。
resolveClassMethod实例探究
新创建XJLPerson类,并在类中定义类方法+(void)test,在XJLPerson.m文件中实现+resolveClassMethod方法,代码如下:
@interface XJLPerson : NSObject
+(void)test;
@end
#import "XJLPerson.h"
@implementation XJLPerson
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"--xjl--%@",NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
@end
调用结果:
2021-07-13 16:18:02.437689+0800 KCObjcBuild[16765:324532] --xjl--test
2021-07-13 16:18:02.438672+0800 KCObjcBuild[16765:324532] --xjl--test
2021-07-13 16:18:02.439058+0800 KCObjcBuild[16765:324532] +[XJLPerson test]: unrecognized selector sent to class 0x1000086b0
-
resolveClassMethod
方法也像resolveInstanceMethod
方法一样,调用了两次,而且逻辑都是一样的。 - 调用
resolveClassMethod
以后,会去查找lookUpImpOrNilTryCache
有没有具体动态实现sel
对应的imp
,元类的缓存中此时有sel
对应的imp
,这个imp
是forward_imp
。lookUpImpOrNilTryCache
里面有判断直接返回nil
,此时直接到resolveInstanceMethod
查找,因为类方法实际上就是元类中的实例方
。 - 如果最后还是没有实现
lookUpImpOrForwardTryCache
获取到forward_imp
进入消息转发
流程。
动态添加+test方法
-
resolveClassMethod
只调用一次,因为动态添加了test
方法 -
resolveClassMethod
和resolveInstanceMethod
的调用流程基本一样,如果resolveClassMethod
没有查询到调用一次resolveInstanceMethod
调用。
resolveClassMethod方法拓展
@interface XJLPerson : NSObject
+(void)test;
@end
#import "XJLPerson.h"
@implementation XJLPerson
+(BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveClassMethod--xjl--%@",NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod--xjl--%@",NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
@end
打印结果如下:
2021-07-13 17:04:29.162523+0800 KCObjcBuild[17860:344756] resolveClassMethod--xjl--test
2021-07-13 17:04:29.163433+0800 KCObjcBuild[17860:344756] resolveClassMethod--xjl--test
这就奇怪了,进入了两次resolveClassMethod
方法,完全没有进入resolveInstanceMethod
方法啊。不是说了类方法就是元类的实例方法嘛?别急,我们先进去源码断点看看是啥回事咯!!!别慌!
通过上面
lldb
调试发现inst
是XJLPerson
类,其sa
指向元类与cls
的地址一致,那么cls
就是XJLPerson
的元类。
断点进入
resolveClassMethod
方法中调用的lookUpImpOrNilTryCache
方法:
此时
inst
是XJLPerson
的元类,cls
是根元类。快速和慢速查找实到根元类
查找,意味着元类调用了实例方法
。
msg(cls,resolve_sel,sel)
也可以验证objc_msgSend
是发送消息是不区分-
和+
方法的。objc_msgSend
的接收者cls
是元类,这意味着向元类中发消息,消息的查找会到元类的元类(根元类
)中查找,所以resolveInstanceMethod
在元类中,在类中是不被调用的。虽然类和元类的名字一样,但是地址是不一样的。这就解释了为什么XJLPerson
类中的resolveInstanceMethod
没被调用。
按照以上分析的逻辑,根元类的父类是NSObject(根类),如果根元类中找不到方法的时候,会在根类中查找,那么我们创建NSObject+XJL的分类,里面实现+resolveInstanceMethod,代码如下:
#import "NSObject+XJL.h"
@implementation NSObject (XJL)
+(BOOL)resolveInstanceMethod:(SEL)sel{
if(@selector(test) == sel){
NSLog(@"resolveInstanceMethod--进入%@--",NSStringFromSelector(sel));
}
return NO;
}
@end
打印结果:
根代码逻辑是一样的,先会调用
resolveClassMethod
方法,再调用resolveInstanceMethod
方法,而且都会调用2
次。
动态方法决议的具体运用
resolveClassMethod
方法中如果没有动态添加类方法
,会调用元类
中的resolveInstanceMethod
。猜想能不能把resolveInstanceMethod
写到一个公用类
中,使类方法
和实例方法
都能调用。
- 实例方法查找流程:
对象
-->类
-->直到根类(NSObject)
-->nil
。 - 类方法查找流程:
类
-->元类
-->直到根类(NSObject)
-->nil
。
最后还是无论是类方法还是实例方法都会走到根类(NSObject)中查找方法,那么我们创建一个NSObject+XJL的分类,来提供动态方法,代码如下:
-
实例方法
跟类方法
都调用了resolveInstanceMethod
方法,区别在于开始训着方法的地方实例方法是类中,类方法在元类中。
动态方法决议优点
- 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页又或许做其他的操作。
- 如果项目中是不同的模块你可以根据命名不同,进行业务的区别。
- 这种方式叫
切面编程
---AOP
。
拓展一下AOP和OOP
-
OOP
:实际上是对对象的属性和行为的封装,功能相同的抽取出来单独封装,强依赖性
,高耦合
。 -
AOP
:是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP
就可以提取出来,运用动态代理,实现程序功能的统一维护
,依赖性小
,耦合度小
,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP
更像一个三维的纵轴
,平面内的各个类有共同逻辑的通过AOP
串联起来,本身平面内的各个类没有任何的关联
。
注意:如果需要详细了解AOP的小伙伴可以自行查找一些资料,我这里就引出一下知识点而已哦。
总结
学习完动态方法决议后,其实发觉它的主要意义是给多一次机会我们去处理一些崩溃的方法,这样子能够大大提高APP的流畅性和容错性。但是这篇文章说讲的动态方法决议只是引出其使用的场景,这样子操作其实也不是非常的合理的,下一篇文章会在消息的转发里程中得到解决哦,敬请期待!!加油!,