iOS10 动态方法决议

动态方法决议起因

前面探究了方法的快速查找流程和慢速查找流程,对方法底查找流程有一定的了解。如果快速查找流程和慢速查找流程都没有找打方法的实现,后面的流程是怎么样的,苹果会给一次机会动态方法决议

案例分析

@interface NBPerson : NSObject
-(void)sayHello;

@end

@implementation NBPerson

@end

int main(int argc, const char * argv[]) {
  
    @autoreleasepool {
        
        NBPerson *nb = [NBPerson alloc];
        [nb sayHello];
      
    }
    return 0;
}
复制代码
  • 控制台经典报错

截屏2021-08-23 下午4.24.47.png

方法未实现为什么会报 unrecognized selector sent to instance

源码探究

  • 慢速查找流程的LookUpImpOrForward

截屏2021-08-23 下午4.36.26.png

  • 部分源码显示
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;

    for (unsigned attempts = unreasonableClassCount();;) {

        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            //当消息的快速查找、慢速查找流程都走完还找不到的情况下
            //imp会给一个默认值forward_imp,进入消息转发流程
            imp = forward_imp;
            break;
        }
    }
    return imp;
}
复制代码
  • 通过LookUpImpOrForward这个方法可以得知,imp在未找到的时候会被默认赋值一个`forward_imp
  • forward_imp只是一个符号,它代表的是(IMP)_objc_msgForward_impcache

_objc_msgForward_impcache

截屏2021-08-23 下午4.51.01.png

  • 在arm64架构下 此函数的汇编命令只有一个:__objc_msgForward

__objc_msgForward

截屏2021-08-23 下午4.55.24.png

  • 3条指令是指通过函数__objc_forward_handle,获取一个返回值。存放在x17寄存器然后通过TailCallFunctionPointer x17

_objc_forward_handle

截屏2021-08-23 下午5.02.12.png

  • _objc_forward_handle具体实现函数是objc_defaultForwardHandler
  • objc_defaultForwardHandler函数中我们可以看到打印在控制台的报错信息

探索流程

  1. 调用方法
  2. 汇编快速查找流程
  3. C++源码慢速查找流程
  4. lookUpImpOrForward
  5. 快速查找和慢速查找都找不到的情况下imp会被默认赋值为forward_imp,forward_imp真实代表的是_objc_msgForward_impcache
  6. _objc_msgForward_impcache具体实现是objc_msgForward
  7. objc_msgForward拿到_objc_forward_handle返回值存放在x17寄存器,b到[x17]
  8. _objc_forward_handle -> objc_defaultForwardHandle (这里实现日志)
  9. 控制台打印日志完成

动态方法决议

  • 源码解析
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;

    for (unsigned attempts = unreasonableClassCount();;) {

        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            //当消息的快速查找、慢速查找流程都走完还找不到的情况下
            //imp会给一个默认值forward_imp,进入消息转发流程
            imp = forward_imp;
            break;
        }
        //找到了sel对应的imp
        if (fastpath(imp)) {
            goto done;
        }
    }
    /**
    * 如果遍历查找的过程找到了,会跳过此步骤,取到done分支,进行后续操作
    * 如果找不到,会进行下面这个算法,最终进入resolveMethod_locked函数
    * 此算法真正达到的目的为单例,保证一个lookUpImpOrForward
    * 只执行一次resolveMethod_locked
    */ 
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    return imp;
}

复制代码

位运算实现单例解读

操作符号解读:

  • &:按位与,是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位都为1时,结果位才为1。
  • ^:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0
  • ^=:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0,将结果赋值为运算符左边。
//初始值
behavior = 3
LOOKUP_RESOLVER = 2

//第一次
behavior & LOOKUP_RESOLVER = 3 & 2 = 0b11 & 0b10 = 0b10 = 2
behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0b11 ^ 0b10 = 0b01 = 1

//非第一次
behavior & LOOKUP_RESOLVER = 1 & 2 = 0b01 & 0b10 = 0b00 = 0


//保证了每一个lookUpImpOrForward 函数最多只能执行一次 resolveMethod_locked(动态方法决议),直到behavior 被重新赋值

复制代码

resolveMethod_locked

  • 当你调用了一个方法的时候,第一进入消息的快速查询流程 -> 然后进入消息的慢速查找流程,依然找不到你实现的地方,此时imp=nil,理论上来讲程序应该崩溃
  • 但是在开发者的角度上来讲,此做法会令这个框架不稳定,或者说这个系统很不友善。所以此框架决定再给你一次机会为你提供了一个自定义的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()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        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);
}

复制代码
  • 此函数非常关键的三步
  1. resolveInstanceMethod:实例方法动态添加imp
  2. resolveClassMethod:类方法动态添加imp
  3. lookUpImpOrForwardTryCache,当完成添加之后,回到之前的慢速查找流程再来一遍

resolveInstanceMethod

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

//系统会在此处为你发送一个消息resolve_sel 
//当你的这个类检测了这个消息,并且做了处理
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

//那么此时系统会重新查找,此函数最终会触发LookUpImpOrForwar
    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));
        }
    }
}
复制代码

resolveClassMethod

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    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) {
            _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));
        }
    }
}

复制代码

lookUpImpOrNilTryCache

extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);

 IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // LOOKUP_NIL = 4  没有传参数behavior = 0   0 | 4 = 4
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
复制代码

lookUpImpTryCache

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    //cls 是否初始化
    if (slowpath(!cls->isInitialized())) {
        // 没有初始化就去查找 lookUpImpOrForward 查找时可以初始化
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    //在缓存中查找sel对应的imp
    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
    // 缓存中没有查询到imp 进入慢速查找流程
    // behavior = 4 ,4 & 2 = 0 不会进入动态方法决议,所以不会一直循环
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    //(behavior & LOOKUP_NIL) = 4 & 4 = 1
    //LOOKUP_NIL 只是配合_objc_msgForward_impcache 写入缓存
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
复制代码
判断cls是否初始化一般都会初始化的
缓存中查找
  • 在缓存中查找sel对应的imp
  • 如果imp存在跳转done流程
  • 判断是否有共享缓存给系统底层库用的
  • 如果缓存中没有查询到imp,进入慢速查找流程
慢速查找流程
  • 慢速查找流程中,behavior= 44 & 2 = 0进入动态方法决议,所以不会一直循环
  • 最重要的如果没有查询到此时imp= forward_imp,跳转lookUpImpOrForward中的done_unlockdone流程,插入缓存,返回forward_imp
done流程
  • done流程: (behavior & LOOKUP_NIL) 且 imp = _objc_msgForward_impcache,如果缓存中的是forward_imp,就直接返回nil,否者返回的impLOOKUP_NIL条件就是来查找是否动态添加了imp还有就是将imp插入缓存
  • lookUpImpOrNilTryCache的主要作用通过LOOKUP_NIL来控制插入缓存,不管sel对应的imp有没有实现,
  • 如果imp返回了有值那么一定是在动态方法决议中动态实现了imp

resolveInstanceMethod 实例探究

@interface NBPerson : NSObject


-(void)sayHello;

@end

@implementation NBPerson

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        NBPerson *nb = [NBPerson alloc];
        [nb sayHello];
    }
    return 0;
}

复制代码
//输出结果
2021-08-24 13:56:48.862237+0800 KCObjcBuild[24082:782193] --进入sayHello--
2021-08-24 13:56:48.863491+0800 KCObjcBuild[24082:782193] --进入sayHello--
复制代码
  • 从输出日志中我们发现两次查找sayHello方法
第一次我们知道是快速和慢速查找都没查到,然后进入动态方法决议,那么第二次呢?
  • 不妨我们来日志探究一下
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{

//其他代码省略
    printf("方法名=%s--behavior=%d\n",sel,behavior);
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        
        printf("调用方法决议---%s\n",sel);
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
   
   //其他代码省略dsa
}
复制代码
  • 打印日志

CF14124C-A976-424D-A4C8-9DC862366E82.png

  • 从日志中我们看到第一次能调用behavior=3,调用之后后面还有个behavior=4没有调用,然后behavior=2又调用了,这个behavior=2从哪里来的呢?

  • 全局搜索

C2D57F0C-38DB-486A-BF69-7DB826DEE381.png

输出结果

7E0D001D-0D4A-478A-BA0F-3B7DF54B1F0C.png

所以这个流程就可以确定是,
快速查找 -> 慢速查找 -> 都没找到就第一次动态方法决议 -> 我们实现reslove_instanceMethod不添加结果就是还没实现 -> class_getInstanceMethod默认behavior = 2 继续慢速查找 -> 第二次动态方法决议 -> 无法找到imp报错

接下来我们动态添加一下方法再看看
//添加实现
+(BOOL)resolveInstanceMethod:(SEL)sel{

    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    if (@selector(sayHello) == sel) {
        IMP imp = class_getMethodImplementation(self , @selector(sayHello2));
        Method meth = class_getInstanceMethod(self , @selector(sayHello2));
        const char * type = method_getTypeEncoding(meth);
        return  class_addMethod(self ,sel, imp, type);;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)sayHello2{
     NSLog(@"--%s---",__func__);
}

复制代码
  • 输出日志

截屏2021-08-24 下午2.45.20.png

  • 第一次动态决议都一样,第二次慢速查找的时候因为找到了 sayHello2直接去实现了,没有第二次调用动态方法决议
动态添加方法之后流程

快速查找 -> 慢速查找 -> 都没找到就第一次动态方法决议 -> 我们实现reslove_instanceMethod并动态添加方法 -> class_getInstanceMethod默认behavior = 2 继续慢速查找 -> 查找到sayHello2并实现

继续探究 resolveClassMethod

int main(int argc, const char * argv[]) {
     
    @autoreleasepool {
        
        NBPerson *nb = [NBPerson alloc];
        [NBPerson sayNB];
    }
    return 0;
}
复制代码
+(BOOL)resolveClassMethod:(SEL)sel{
    NSLog(@"--进入%@--",NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}
复制代码
  • 动态添加sayNB2
+(BOOL)resolveClassMethod:(SEL)sel{
  if (@selector(sayNB) == sel) {
    NSLog(@"resolveClassMethod--进入%@--",NSStringFromSelector(sel));
    IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(sayNB2));
    Method meth = class_getClassMethod(object_getClass([self class]) , @selector(sayNB2));
    const char * type = method_getTypeEncoding(meth);
    return  class_addMethod(object_getClass([self class]) ,sel, imp, type);;
  }
    return [super resolveClassMethod:sel];
}

+(void)sayNB2{
    NSLog(@"--%s---",__func__);
}
复制代码
  • 输出结果和 resolveInstanceMehod一样,就不展示了

根类探究

  • 新建NSObject 分类 NB
#import "NSObject+NB.h"

@implementation NSObject (NB)

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (@selector(sayHello) == sel) {
        NSLog(@"resolveInstanceMethod--进入根类%@--",NSStringFromSelector(sel));
    }
    return NO;
}

@end
复制代码
  • NBPerson 和NSObject+NB 都不动态添加方法输出结果
2021-08-24 15:36:25.434378+0800 KCObjcBuild[26342:864477] --进入sayHello--
resolveInstanceMethod--进入根类sayHello--
2021-08-24 15:36:25.435479+0800 KCObjcBuild[26342:864477] --进入sayHello--
resolveInstanceMethod--进入根类sayHello--

复制代码
  • NBPerson不动态添加 和NSObject+NB(动态添加)输出结果
2021-08-24 16:15:19.481990+0800 KCObjcBuild[27162:889603] --进入sayHello--
2021-08-24 16:15:19.482336+0800 KCObjcBuild[27162:889603] --进入根类sayHello--
2021-08-24 16:15:19.482417+0800 KCObjcBuild[27162:889603] ---[NSObject(NB) sayHello2]---
复制代码
  • NBPerson(动态添加) 和NSObject+NB(动态添加)输出结果
2021-08-24 16:18:09.871925+0800 KCObjcBuild[27226:891858] --进入sayHello--
2021-08-24 16:18:09.872140+0800 KCObjcBuild[27226:891858] ---[NBPerson sayHello2]---
复制代码
  • 总结reslove_instanceMethod动态添加流程,在类中动态添加,已添加动态添加完成,未添加从父类中添加,以此类推,如果找到根类(NSObject)还未添加 返回NO。
  • 再测试类方法结果一致,因为类的isa指向元类,
  • reslove_classMethod动态添加流程,在元类中动态添加实例方法,已添加动态添加完成,未添加从元类父类中添加,以此类推,如果找到根元类的父类根类(NSObject)还未添加 返回NO。

AOPOOP的区别

  • OOP:实际上是对对象的属性和行为的封装,功能相同的抽取出来单独封装,强依赖性,高耦合
  • AOP:是处理某个步骤和阶段的,从中进行切面的提取,有重复的操作行为,AOP就可以提取出来,运用动态代理,实现程序功能的统一维护,依赖性小,耦合度小,单独把AOP提取出来的功能移除也不会对主代码造成影响。AOP更像一个三维的纵轴,平面内的各个类有共同逻辑的通过AOP串联起来,本身平面内的各个类没有任何的关联
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享