Objective-C 动态方法决议

前言

上一篇文章慢速方法查找一文详细分析了消息慢速查找的流程,当在找不到的时候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

注意:既然这个函数也是进行快速慢速消息查找的,那么就说明resolveInstanceMethodresolveClassMethod可以在某个时机将方法加入类中(加入到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,原因在于这里不需要返回只需要对缓存进行更新的处理。
  • lookUpImpOrNilTryCachelookUpImpOrForwardTryCache唯一区别就是是否进行动态转发,这里是不进行
  • 可以看到返回的resolved只是进行了日志打印。也就是resolved返回YES/NO对功能没有影响。

注意:如果没有实现,缓存中没有,进入lookUpImpOrForward查找,sel没有查找到对应的imp,此时imp = forward_imp动态方法决议只调用一次,此时会走done_unlockdone流程,既selforward_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?
            ...
        }
    }
}
  • resolveClassMethodNSobject中已经实现,只要元类初始化就可以了,目的是缓存在元类中
  • 调用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,这个impforward_implookUpImpOrNilTryCache里面有判断直接返回nil,此时直接到resolveInstanceMethod查找,因为类方法实际上就是元类中的实例方
  • 如果最后还是没有实现lookUpImpOrForwardTryCache获取到forward_imp进入消息转发流程。

动态添加+test方法

动态添加类方法
  • resolveClassMethod只调用一次,因为动态添加了test方法
  • resolveClassMethodresolveInstanceMethod的调用流程基本一样,如果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调试发现instXJLPerson类,其sa指向元类与cls的地址一致,那么cls就是XJLPerson的元类。
断点进入resolveClassMethod方法中调用的lookUpImpOrNilTryCache方法:
断点进入lookUpImpOrNilTryCache方法

此时instXJLPerson的元类,cls是根元类。快速和慢速查找实到根元类查找,意味着元类调用了实例方法
msg方法

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的流畅性和容错性。但是这篇文章说讲的动态方法决议只是引出其使用的场景,这样子操作其实也不是非常的合理的,下一篇文章会在消息的转发里程中得到解决哦,敬请期待!!加油!,

你可能感兴趣的:(Objective-C 动态方法决议)