消息的动态决议和消息转发

184 阅读7分钟

上篇文章我们探索了消息发送机制,包括快速查找和慢速查找,如果上述两种方式都没有找到应该怎么办?下面我们来探索一下

消息的动态决议

找到源码 lookUpImpOrForward 的实现,在父类查找方法完毕后如果还没找到 imp, 并且 method resolver 不起作用,就会把 forward_imp 赋值给 imp:

image.png 先追踪一下 forward_imp,发现底层会抛出异常:

image.png

image.png

image.png forward_imp 就已经找到底了,通过 objc_defaultForwardHandler 就会抛出未找到方法实现的异常,可以发现在底层是没有元类的概念的。再继续往 lookUpImpOrForward 下面看:

image.png resolveMethod_locked 里面就是消息的动态决议和消息转发,并且这个方法只会执行一次,可以进行简单验证,声明一个方法不写实现:

image.png 进入 resolveMethod_locked 发现 resolveInstanceMethodresolveClassMethod,这两个分别是实例方法和类方法的动态决议 image.png 下面通过代码验证一下,在类里面添加这两个方法,看看如果其他方法没有实现会不会走这边:

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

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

分别调用未实现的实例方法和类方法,会发现崩溃:

image.png image.png 可以发现调用未实现的方法时,系统会调用 resolveInstanceMethod 两次,我们可以用 runtimeAPI 动态地给这个 sel 添加 imp, 这样就不会崩溃:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
    if (sel == @selector(getName)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(getName1));
        class_addMethod(self.class, sel, imp, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

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

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"%s--%@", __func__, NSStringFromSelector(sel));
    if (sel == @selector(classMethod)) {
        IMP imp = class_getMethodImplementation(self.class, @selector(classMethod1));
        class_addMethod(objc_getMetaClass("User"), sel, imp, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

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

image.png "v@:"类的底层探索(下)里面讲过,是方法的返回值

再来探索一下 resolveInstanceMethod 的源码,发现动态地添加 imp 后,会调用 lookUpImpOrNilTryCachecache 中查找 imp, 那么我们来验证一下 cache 中是否存在 imp:

image.png image.png 发现确实在缓存里,就是说使用 runtimeAPI 动态添加方法时,是存在 cache 里的

注意:要打两个断点,因为 lldb 命令会产生 classresponseToSelector 方法,从而造成 cache 扩容,导致 getName 方法丢掉,所以应该是先扩容,再执行 getName 方法

resolveClassMethod 实质

现在对代码做如下调整:

image.png 打印结果如下:

image.pngclass_getMethodImplementation 里面会调用 lookUpImpOrForwardlookUpImpOrForward 里面调用的 resolveMethod_locked 会判断,如果传入的 cls 不是元类,就会调用 resolveInstanceMethod

image.png

image.png 再进行如下改动:

image.png 会发现进入死循环,综合上述分析,在进入 class_getMethodImplementation 后再调用 resolveMethod_locked 时,传入的 cls 是元类,会一直调用 resolveClassMethod

消息转发

lookUpImpOrForward 中找到 imp 后会跳到 done 调用 log_and_fill_cache 进行方法缓存

image.png 其中 implementer 肯定是 true, 也就是说在调用 cache.insert() 方法缓存方法之前,如果 objcMsgLogEnabledtrue,就会产生日志文件,文件位置如下:

image.png 下面探索一下如何给 objcMsgLogEnabled 赋值:

image.png 可以发现 objcMsgLogEnabled 初始值为 false, 而且只有在 instrumentObjcMessageSends 方法中会对其赋值,可以使用 extern 关键字来调用这个函数来给 objcMsgLogEnabled 赋值,并且产生日志:

image.png

image.png 我们发现了在消息的动态决议 resolveInstanceMethod 和报错 doesNotRecognizeSelector 之间有 forwardingTargetForSelectormethodSignatureForSelector, 它们分别对应消息的快速转发和慢速转发

消息的快速转发

我们在 User.m 里重写 forwardingTargetForSelector 这个方法:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    return nil;
}

发现在系统未找到方法实现之前确实调用了 forwardingTargetForSelectorimage.png 同样我们可以在这个方法里面做一些处理,当前类不能相应某些方法时,可以把方法转发给能响应此方法的类,比如再创建一个 CrashDefender 类,里面去实现 User 里未实现的方法:

@implementation User

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    if (aSelector == @selector(getName)) {
        return [CrashDefender new];
    }
    return nil;
}

@end


@implementation CrashDefender

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

@end

此时再调用 User 里面的 getName 方法就不会崩溃了,因为 getName 已经转发给了 CrashDefender 类来处理。同时 getName 方法缓存在 CrashDefendercache 里,因为 forwardingTargetForSelector 返回了 CrashDefender 的实例,是这个实例在调用方法。 image.png 我们可以通过这个方法来把未处理的消息转发给一个单独的类,这个单独的类就可以去统一处理那些其他类里面没有处理的消息,也可以通过这个forwardingTargetForSelector方法去进行那些方法找不到的 crash 的收集等。这是对实例对象的方法的快速转发,对于类方法来说也有对应的消息转发的类方法+ (id)forwardingTargetForSelector:(SEL)aSelector,原理和- (id)forwardingTargetForSelector:(SEL)aSelector方法一样

消息的慢速转发

如果在 forwardingTargetForSelector 没有进行处理,那么系统会调用消息的慢速转发:methodSignatureForSelector, 同样在 User.m 里重写该方法, 并调用 getName

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

image.png 注意的是 methodSignatureForSelector 需要和 forwardInvocation 一起使用,methodSignatureForSelector 只是为方法提供了一个有效签名,提供了有效签名之后,系统就会调用 forwardInvocation 来处理这个签名。在 User.m 中使用 forwardInvocation 来避免崩溃:

image.png 同样可以在 forwardInvocation 中用其他的类来处理本类相应不了的方法,可以先看一下 NSInvocation 的属性,里面提供了消息接收者 target 和方法名 selector

image.png 通过上面两个参数就可以用 forwardInvocation 这样处理:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    CrashDefender *defender = [CrashDefender new];

    if ([self respondsToSelector:anInvocation.selector]) {
        // 本身可以响应 自己响应
        [anInvocation invokeWithTarget:self];
    } else if ([defender respondsToSelector:anInvocation.selector]) {
        // 本身不可以响应 但 CrashDefender 可以
        [anInvocation invokeWithTarget:defender];
    } else {
        NSLog(@"该功能正在开发中,敬请期待");
    }
}

总结

消息的动态决议 resolveInstanceMethod, 消息的快速查找 forwardingTargetForSelector, 消息的慢速查找 methodSignatureForSelector 是系统提供给我们防止程序因找不到方法而崩溃的“三根救命稻草”,相比于其他两种,消息的慢速查找 methodSignatureForMethod 更为灵活,可以在 forwardInvocation 中什么事也不做。结合上篇的消息派发机制 objc_msgSend, 可以总结消息的发送流程如下: image.png 注意: 一般在开发中不会这样做,可以在项目上预发时,添加一个类别进行无侵入处理

拓展

  1. 所有的类都继承自 NSObject, 可以在 NSObject 中实现 resolveInstanceMethod, 为什么要设计 resolveClassMethod

    如果没有 resolveClassMethod这个动态决议方法,系统在查找类方法的时候会走resolveInstanceMethod 这个动态决议方法,这样的查找流程就会比较的长,会影响系统查找的效率。基于这个原因系统提供了 resolveClassMethod方法来给类方法动态添加方法的实现,用来简化类方法的查找流程而提供给我们去实现的一个方法而已。系统其实最终都需要通过resolveInstanceMethod方法来进行方法的动态决议。

  2. 没有找到方法时,为什么会走两次 resolveInstanceMethod or resolveClassMethod?

    第一次进入的方法调用链:lookUpImpOrForward -> resolveMethod_locked -> resolveInstanceMethod, 这个很好理解, 就是之前分析的那样

    image.png 第二次进入时,通过 bt 打印一下堆栈信息:

    image.png 发现第一次执行结束后会进入 ___forwarding___ 函数中,然后再调用 methodSignatureForSelector 方法,然后再调用 class_getInstanceMethod 方法,源码查看该方法:

    image.png 发现该方法内部会调用 lookUpImpOrForward, 从而再次调用 resolveInstanceMethod 方法。resolveClassMethod 也是如此

  3. AOP 设计模式

    AOP(Aspect Oriented Programming): 面向切面编程,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 基于上述原理可以做以下两件事:

  • 收集找不到方法的崩溃信息(无侵入),创建 NSObject 的一个类别,统一处理方法找不到的崩溃:
@interface NSObject (CrashCollector)

@end

#import <objc/runtime.h>

@implementation NSObject (CrashCollector)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s", __func__);
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s", __func__);
}

@end
  • 无侵入统计每个页面停留时间埋点:
#import <objc/runtime.h>

@implementation UIViewController (TimeAnalysis)

+ (void)load {
    static dispatch_once_t onceT;

    dispatch_once(&onceT, ^{
        Method viewWillAppear = class_getInstanceMethod(self.class, @selector(viewWillAppear:));
        Method aopViewWillAppear = class_getInstanceMethod(self.class, @selector(aopViewWillAppear));
        method_exchangeImplementations(viewWillAppear, aopViewWillAppear);
        
        Method viewWillDisAppear = class_getInstanceMethod(self.class, @selector(viewWillDisappear:));
        Method aopViewWillDisAppear = class_getInstanceMethod(self.class, @selector(aopViewWillDisAppear));
        method_exchangeImplementations(viewWillDisAppear, aopViewWillDisAppear);
    });

}

- (void)aopViewWillAppear {
    NSLog(@"enter %@", self.class);
    // 方法已经交换,不会造成死循环
    [self aopViewWillAppear];
}

- (void)aopViewWillDisAppear {
    NSLog(@"leave %@", self.class);
    [self aopViewWillDisAppear];
}

@end