iOS底层-动态方法决议 & aop

前言

上篇 objc_msgSend慢速查找 中,其中当查找不到 imp 时(不仅是当前类找不到,其父类直到 NSObject 都没找到),会进行 behavior 判断,进而直接返回 resolveMethod_locked 方法的返回值。

其目的是苹果给你一次容错的机会。如果在resolveMethod_locked 方法不处理,那么就会抛出崩溃异常。下面将重点探究 resolveMethod_locked 的原理,即 动态方法决议

动态方法决议

// 如果找不到imp,执行一次方法解析
// 执行一次的原因:
// 第一次:behavior = 3,LOOKUP_RESOLVER = 2, 3 & 2 = 2,进入if, behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1
// 第二次:behavior = 1,LOOKUP_RESOLVER = 2, 1 & 2 = 0,不再进入if
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    behavior ^= LOOKUP_RESOLVER;
    // 动态方法决议
    return resolveMethod_locked(inst, sel, cls, behavior);
}
复制代码
  • 此方法只执行 一次,类似于单利。
  • 经过运算,最终 behavior = 1

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()) {
        // 对象方法的动态决议
        resolveInstanceMethod(inst, sel, cls);
    } 
    // 元类
    else {
        // 类方法的动态决议
        resolveClassMethod(inst, sel, cls);
        // 再次尝试查找,此时 behavior = 1
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            // 如果没找到imp
            // 对象方法的动态决议
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    // 因为上面在执行动态方法决议时,有可能已经存在缓存,此时再次查找缓存,并使用。此时 behavior = 1
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
  • 如果非元类,执行对象方法动态协议 resolveInstanceMethod
  • 如果元类,首先执行类方法动态协议 resolveClassMethod,然后 lookUpImpOrNilTryCache 尝试查找 imp ,如果没找到 imp,则执行元类对象方法动态协议 resolveInstanceMethod
  • 最后再次 lookUpImpOrForwardTryCache 查找缓存。

lookUpImpOrNilTryCache 和 lookUpImpOrForwardTryCache

当元类时,会再次尝试查找 lookUpImpOrNilTryCache,那么最后会执行 lookUpImpOrForwardTryCache,那么两者有什么区别?

IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // 此时参数 behavior = 5
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // 此时参数 behavior = 1
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}
复制代码
  • 共同调用一个方法 _lookUpImpTryCache
  • clsbehavior 参数不同,这也导致了两个本质上的不同,是否进行了消息转发。lookUpImpOrForwardTryCache 进行消息转发,而 lookUpImpOrNilTryCache 不进行消息转发。

_lookUpImpTryCache

既然 lookUpImpOrNilTryCachelookUpImpOrForwardTryCache 都会执行 _lookUpImpTryCache,下面具体分析源码:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    if (slowpath(!cls->isInitialized())) {
        // 如果类没有初始化,执行慢速查找流程,原因是在慢速查找流程中,有对类进行初始化的操作
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    // 如果类已经初始化,再次查找缓存
    IMP imp = cache_getImp(cls, sel);
    // 如果存在
    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)) {
        // 如果不存在,再次执行慢速查找流程
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    
done:
    // 对于慢速查找动态方法决议来说,由于 behavior = 5, 该if不会执行
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    // 返回imp
    return imp;
}
复制代码
  • isInitialized() 基本不会执行,因为在慢速查找流程中,已经对类进行初始化了。
  • cache_getImp 查找缓存。如果能找到,返回 imp;如果找不到,查找共享缓存,如果共享缓存也没找到,那么再次执行 lookUpImpOrForward 慢速查找流程。
  • 再次执行慢速查找流程时,不会执行 resolveMethod_locked 动态方法决议了。

resolveInstanceMethodresolveClassMethod 是在返回 lookUpImpOrForwardTryCache 之前调用的。那么他们到底是什么作用?又该如何使用?

resolveInstanceMethod 对象方法动态决议

当对象进行调用 objc_msgSend 方法时,并且通过快速和慢速查找都没找到时,会执行 resolveInstanceMethod,其源码分析如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    // 获取类方法 +resolveInstanceMethod
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 先进行元类查找是否实现了`resolveInstanceMethod` 方法,也就是类的类方法。如果没有实现直接返回。
    // 如果当前类的元类是NSObject,则不会返回,因为NSObject默认实现了。
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        return;
    }
    // 系统会发送一个 resolveInstanceMethod 方法,因为消息的接收者是cls,所以 resolveInstanceMethod 是类方法。
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);
    // 查找 imp
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    // 日志打印
    if (resolved  &&  PrintResolving) { ... }
}
复制代码
  • 获取 resolveInstanceMethod 类方法 ,之所以是 类方法 是因为在下面的流程中系统自动调用了 objc_msgSend,其消息接收者是 cls,所以是 类方法
  • 进行元类 resolveInstanceMethod 的查找,此处不会为空。
  • 系统给类发送 +resolveInstanceMethod 消息。
  • 查找imp。此时并没有直接返回 imp,而是只进行了判断。
  • resolved  &&  PrintResolving 是日志打印

resolveInstanceMethod 流程分析

  • 在执行 resolveInstanceMethod 方法时,并执行 lookUpImpOrNilTryCache 进行查找,查找元类是否实现了 resolveInstanceMethod 方法。其中 cls->ISA() 说明是元类,最终会找到 NSObject 中。如果没有实现,则将 resolveInstanceMethod 缓存到元类中。( 此流程 NSObject 默认实现了)
  • 接着系统发送 resolveInstanceMethod 消息。
  • 再次执行 lookUpImpOrNilTryCacheZLSubObject 中查找 imp,其目的是将 imp 加入 ZLSubObject 的缓存中,如果找不到会存入_objc_msgForward_impcache
  • 返回继续执行 lookUpImpOrForwardTryCache 查找,此时消息慢速查找不会执行。因为前面已经写入了对应的缓存。这次会从缓存中获取到 imp_objc_msgForward_impcache。直接进行了消息转发。

结论:

  • lookUpImpOrNilTryCache 只是将方法插入缓存
  • lookUpImpOrForwardTryCache 从缓存中获取 imp
  • 这就是为什么执行了 lookUpImpOrNilTryCache 查找了 imp,却没有返回,反而又执行了 lookUpImpOrForwardTryCache 进行查找 imp,调用两次的原因。

resolveInstanceMethod 案例分析

继续使用上篇 objc_msgSend慢速查找-案例 的案例,其中 ZLObject 改进如下:

@interface ZLObject : NSObject

+ (void)classMethod1;
+ (void)classMethod2;

- (void)instanceMethod1;
- (void)instanceMethod2;

@end

@implementation ZLObject

+ (void)classMethod1 {
    NSLog(@"%s",__func__);
}

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

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

@end
复制代码

执行代码如下:

ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj subInstanceMethod];
[subObj instanceMethod1];
[subObj instanceMethod2];
复制代码

打印结果如下:

-[ZLSubObject subInstanceMethod]
-[ZLObject instanceMethod1]
resolveInstanceMethod: ZLSubObject-instanceMethod2
resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLSubObject instanceMethod2]: unrecognized selector sent to instance 0x10060e470
复制代码
  • 虽然还是崩溃,但是会发现多了两条打印:resolveInstanceMethod: ZLSubObject-instanceMethod2
  • 这说明在类中实现了 + (BOOL)resolveInstanceMethod 是可以获取到的。

但是为什么会打印两条:resolveInstanceMethod: ZLSubObject-instanceMethod2

+ (BOOL)resolveInstanceMethod 打上断点如下,并执行:

第一次进入堆栈如下:

第二次进入堆栈如下:

结合 resolveInstanceMethod 的流程,分析如下:

  • 第一次执行 resolveInstanceMethod 方法时,最终会执行 lookUpImpOrForwardTryCache 进行消息转发。当消息转发时,会执行 class_getInstanceMethod 方法。
  • class_getInstanceMethod的源码如下:
Method class_getInstanceMethod(Class cls, SEL sel)
{
   if (!cls  ||  !sel) return nil;
    // 慢速查找方法,且 behavior = LOOKUP_RESOLVER
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
    // 查找 cls 的 method
    return _class_getMethod(cls, sel);
}
复制代码
  • 又执行了一次 lookUpImpOrForward,并且 behavior = LOOKUP_RESOLVER,所以这次不进行消息转发了,不会造成死循环。这就是第二次打印的原因。

resolveInstanceMethod 案例改进

根据上面分析,如果 imp 实现了,那么就不会进行消息转发,也会不会打印第二次。相关代码修改如下:

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@-%@", self, NSStringFromSelector(sel));
    if (sel == @selector(instanceMethod2)) {
        IMP imp = class_getMethodImplementation(self, @selector(instanceMethod1));
        Method m = class_getInstanceMethod(self, @selector(instanceMethod1));
        const char * type = method_getTypeEncoding(m);
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}
复制代码

打印结果如下:

resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLObject instanceMethod1]
复制代码

当获取到 instanceMethod2 时,通过动态添加方式,让其执行其他已经实现的方法,比如 instanceMethod1 方法。

resolveClassMethod 类方法动态决议

类方法动态决议对象方法动态协议 有很多相似的地方。类方法动态决议源码如下:

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    // 先查找当前类是否实现了`resolveClassMethod` 方法,如果没有实现直接返回。
    // 这里不会返回,因为NSObject默认实现了。
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        return;
    }
    // 非元类
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        // 获取非元类
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized", nonmeta->nameForLogging(), nonmeta);
        }
    }
    // 系统发送一个非元类的 resolveClassMethod 的类方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
    // 查找 imp
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    // 日志打印
    if (resolved  &&  PrintResolving) { ... }
}
复制代码

流程分析:

  • 在执行 resolveClassMethod 方法时,并执行 lookUpImpOrNilTryCache 进行查找,查找类是否实现了 resolveClassMethod 方法。如果没有实现,则将 resolveClassMethod 缓存到类中。( 此流程 NSObject 默认实现了)
  • 接着获取非元类 nonmeta,目的是防止非元类没有实现。其中获取非元类方法 getMaybeUnrealizedNonMetaClass,主要逻辑是:如果是非元类直接返回 非元类;如果是 类的ISA 指向 自身,那么返回 NSObject;如果是其他类,循环获取父类,直到找到 NSObject 为止;以及其他类的判断逻辑,此处不再赘述。
  • 系统发送 resolveClassMethod 消息。
  • 再次执行 lookUpImpOrNilTryCacheZLSubObject 中查找 imp,其目的是将 imp 加入 ZLSubObject 的缓存中,如果找不到会存入_objc_msgForward_impcache
  • 返回继续执行 lookUpImpOrForwardTryCache 查找,此时消息慢速查找不会执行。因为前面已经写入了对应的缓存。这次会从缓存中获取到 imp_objc_msgForward_impcache。直接进行了消息转发。
  • resolved  &&  PrintResolving 是日志打印。

结论:

  • lookUpImpOrNilTryCache 只是将方法插入缓存
  • lookUpImpOrForwardTryCache 从缓存中获取 imp

resolveClassMethod 案例分析

ZLObject 中添加代码如下:

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@-%@", self, NSStringFromSelector(sel));
    return  [super resolveClassMethod:sel];
}
复制代码

执行代码如下:

[ZLSubObject classMethod2];
复制代码

打印结果如下:

resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
+[ZLSubObject classMethod2]: unrecognized selector sent to class 0x100008310
复制代码
  • 虽然还是崩溃,但是会发现打印多条:ZLSubObject-。其中 classMethod2 也是两次打印。 ( encodeWithOSLogCoder 暂时不去探究 )
  • 当调用 lookUpImpOrNilTryCache 没有找到 imp 时,就调用 resolveInstanceMethod 去查找(此时 cls 为元类,因为类方法其本质上是元类的对象方法),没有找到就执lookUpImpOrForwardTryCache,即消息转发。
  • 最后在消息转发的时候会再执行一次方法动态决议。
  • 此时打印两条的原因和 resolveInstanceMethod 类似,第二次是因为消息转发,执行了元类的 class_getInstanceMethod

resolveClassMethod 案例改进

相关代码修改如下:

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@-%@", self, NSStringFromSelector(sel));
    if (sel == @selector(classMethod2)) {
        Class metaClass = objc_getMetaClass("ZLObject");
        IMP imp = class_getMethodImplementation(metaClass, @selector(classMethod1));
        Method m = class_getClassMethod(self, @selector(classMethod1));
        const char * type = method_getTypeEncoding(m);
        return class_addMethod(metaClass, sel, imp, type);
    }
    return  [super resolveClassMethod:sel];
}
复制代码

打印结果如下:

resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-encodeWithOSLogCoder:options:maxLength:
resolveClassMethod: ZLSubObject-classMethod2
+[ZLObject classMethod1]
复制代码

当获取到 classMethod2 时,通过动态添加方式,让其执行其他已经实现的方法,比如 classMethod1 方法。

动态方法决议-分类

由于调用类方法时,执行类方法动态决议,当 lookUpImpOrNilTryCache 没找到时,执行元类的 resolveInstanceMethod

// 类方法的动态决议
resolveClassMethod(inst, sel, cls);
// 再次尝试查找,此时 behavior = 1
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
    // 如果没找到imp
    // 对象方法的动态决议
    resolveInstanceMethod(inst, sel, cls);
}
复制代码

所以,不管是 对象方法 动态决议还是 类方法 动态决议,都可以在元类 resolveInstanceMethod 中对方法进行处理。

根据 isa的走位图NSObject 既是根类也是元类,在 NSObject 调用 +方法 存到 NSObject 的元类中,也就是 NSObject 自己,通过 NSObjectresolveInstanceMethod 方法就可以实现了。

添加一个 NSObject 的分类,实现方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@-%@", self, NSStringFromSelector(sel));
    if (sel == @selector(instanceMethod2)) {
        IMP imp = class_getMethodImplementation(self, @selector(instanceMethod1));
        Method m = class_getInstanceMethod(self, @selector(instanceMethod1));
        const char * type = method_getTypeEncoding(m);
        return class_addMethod(self, sel, imp, type);
    } else if (sel == @selector(classMethod2)) {
        Class metaClass = objc_getMetaClass("ZLObject");
        IMP imp = class_getMethodImplementation(metaClass, @selector(classMethod1));
        Method m = class_getClassMethod(self, @selector(classMethod1));
        const char * type = method_getTypeEncoding(m);
        return class_addMethod(metaClass, sel, imp, type);
    }
    return NO;
}
复制代码

分别调用对象方法和类方法:

ZLSubObject *subObj = [[ZLSubObject alloc] init];
[subObj instanceMethod2];

[ZLSubObject classMethod2];
复制代码

执行结果:

resolveInstanceMethod: ZLSubObject-instanceMethod2
-[ZLObject instanceMethod1]
resolveInstanceMethod: ZLSubObject-classMethod2
+[ZLObject classMethod1]
复制代码

这样就在 NSObject 的分类中实现 resolveInstanceMethod,既处理了类方法,也处理了实例方法。两次调用参数不同,一次是类调用,一次是元类调用。

总结

  • lookUpImpOrNilTryCache 只对方法进行缓存,lookUpImpOrForwardTryCache,从缓存查找 imp,进行消息转发。
  • resolveInstanceMethodresolveClassMethod 打印两条的原因,是因为第二次执行了消息转发,执行了元类的 class_getInstanceMethod
  • 当执行类方法动态决议时,如果没有命中,会调用元类的 resolveInstanceMethod 对象方法。这也符合,调用类方法其实是调用元类的对象方法这一原则。
  • NSObject 的分类中实现 resolveInstanceMethod,既处理了类方法,也处理了实例方法。
  • 动态方法决议的返回值不会影响功能,只是对日志打印有影响。

动态方法决议流程图

aop & oop

动态方法决议的意义在于,当苹果查找 imp 找不到的时候给的一次解决错误的机会。例如上面的案例中,可以在 NSObject 的分类中实现 + resolveInstanceMethod, 这样找不到的 imp 都会在 resolveInstanceMethod 中监听到。

那么在实际的开发过程中,是不是可以用这种方式处理呢?比如在自己的工程中类名是可以根据前缀、模块、事务、类型等进行区分(例如:ZLLoginCodeController),当发现 方法 有问题的时候,可以进行容错处理。比如当获取登录验证码时 getLoginCodeEvent 出现问题的时候,进行上报处理。其实是有一定的 弊端

这种方式就是切面编程。

oop

oop 是面向对象编程(Object Oriented Programming),是常用的编程方式,其特点是封装性继承性多态性oop 达到了软件工程的三个主要目标:重用性灵活性扩展性OOP = 对象+类+继承+多态+消息,其中核心概念是 类和对象

一般情况下会提取 公共的类,但是遵循后会有 弊端,就是对类有很强的依赖,耦合性很强。但是其实对于开发者来说更关心是的业务,所以处理公共类时,尽量 少侵入,最好 无侵入。通过动态方式注入代码,对原始方法没有影响。其中要切入的方法和类就是 切点

aop

aop 是面向切面编程(Aspect Oriented Programming),aopoop的延伸。是 函数式编程 的一种衍生范型。利用 aop 可以对业务逻辑的各个部分进行 隔离,从而使得业务逻辑各部分之间的 耦合度降低,提高程序的可重用性,同时提高了开发的效率。

但是其缺点也暴露出来了:

  • 代码冗余:比如上面案例中的if-else 判断,如果方法再多,还得写if-else判断。
  • 浪费性能:方法会调用很多次,浪费了性能。如果命中还好,没有命中会走多次,会有 性能消耗
  • 重复处理:如果其它模块也做了相应处理,重复了这块,不一定会执行到。

动态方法决议是消息转发机制 的前一个阶段。意味着如果在这里做了容错处理,后面的流程就被切掉了。那么转发流程就没有意义了。所以在后面的流程做 aop 更合理。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享