OC - 消息转发

324 阅读5分钟

前言

我们在上一篇文章 OC - 底层动态方法决议动态方法决议 有了初步了解。今天我们将一起探索消息发送的最后一个流程 消息的转发。还有朋友没有下载 objc 源码的可以在这里进行下载。今天我们需要准备 CF 源码 以及反汇编工具 Hopperida

消息的转发

lookUpImpOrForward 慢查询中,从元类中到了 imp,就会进入到 done 流程中。在done中判断共享缓存然后走到log_and_fill_cache函数中。

image.png

log_and_fill_cache 函数中添加缓存。

image.png

cls->cache.insert(sel, imp, receiver); 这行代码我们已经非常熟悉了。但是这里在执行它之前有一个判断。如果满足 slowpath(objcMsgLogEnabled && implementer) 这么一个条件就会执行 logMessageSend/tmp/msgSends-%d 路径中写入一些数据。已知 implementer 是当前的类,所以只要 objcMsgLogEnabledYES 时就会执行。接下我们看看 objcMsgLogEnabled 是在哪里进行赋值的。全局搜索。

image.png 在这个地方有一个默认赋值为 false

image.png 通过全局搜索,在 instrumentObjcMessageSends 函数中对 objcMsgLogEnabled 进行了赋值。所以我们就可以把这个方法外部扩充,然后传入 YES or NO。就能进行监听!

extern void instrumentObjcMessageSends(BOOL flag);

然后运行代码。

#import <Foundation/Foundation.h>
#import "TPerson.h"

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        instrumentObjcMessageSends(YES);
        TPerson * tp = [TPerson alloc];
        [tp formatPerson];
        
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}

/tmp/msgSends-%d 路径下就有了这么一个文件。 image.png

打开这个文件 msgSends-2060

image.png 分析这个文件,发现我们需要关注的只有头部这几个东西。resolveInstanceMethod 这个我们在 OC - 底层动态方法决议 已经了解了。forwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector 是什么我们不知道,接下来我们就一一进行探究分析。

forwardingTargetForSelector(快速转发流程)

Xcode 中打开 command + shift + o ,进搜索。

image.png

苹果在这个地方就做了详细的解释。接下来我们就在 TPerson 中进行实现。然后运行

#import "TPerson.h"
#import "TStudent.h"

@implementation TPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}
@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TPerson formatPerson]: unrecognized selector sent to instance 0x100526860

unrecognized 崩溃了。 我们 TStudent 中也添加了一个 formatPerson的方法,并且实现了方法。那么我们能不能把 TPerson 接收的消息转发给 TStudent 呢?接下来我们一起验证一下。

#import "TPerson.h"
#import "TStudent.h"

@implementation TPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
    return [TStudent alloc];
}

@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TStudent formatPerson]

程序正常运行。这个地方并没有像动态方法那样需要处理的东西很多,而且臃肿。

那么我们为啥不将 formatPerson 方法在 TPerson 中实现呢?反而要在 TStudent 中实现呢?是不是有毛病呢?因为这是一个 通过 selimp 的过程。首先是通过 objc_msgSendcache 缓存中查找,如果没有查找到。然后再慢速查找里面通过查找 methodlist 查找。如果还是没有找到。说明在内存中这个方法根本不存在。就可以通过 动态方法决议 进行补救,只需要提供一个能够实现的就可以。

只要走到 - (id)forwardingTargetForSelector:(SEL)aSelector 这个方法中,我们就往 TStudent 中添加方法。这样就能保证 TPerson 的安全运行。那么如果 TStudent 也没有实现这个方法呢。程序就会继续报错,然后走到 methodSignatureForSelector 中。

methodSignatureForSelector(慢速转发流程)

接下来我们搜索 methodSignatureForSelector 查看官方给出的解释。

image.png

接下来我们就代码实现一下,运行程序。

#import "TPerson.h"
#import "TStudent.h"

@implementation TPerson

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
    return [TStudent alloc];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s -- %@",__func__ ,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}
@end
输出:
-[TPerson forwardingTargetForSelector:] -- formatPerson
-[TStudent formatPerson]: unrecognized selector sent to instance 0x100746d00

程序崩溃了。通过崩溃日志我们发现程序并没有走到 methodSignatureForSelector 方法中。那么这是为什么呢?因为 TStudent 中并没有实现 formatPerson 方法。所有在 forwardingTargetForSelector 方法中返回 [TStudent alloc] 不对的。所有这个地方不能同时调用 forwardingTargetForSelector 方法。在官方对 methodSignatureForSelector 的解释中,我们得知 methodSignatureForSelector 方法是需要搭配 forwardInvocation: 方法一起使用的,而且是在 方法中进行解读的。注意:这个地方要对签名进行处理。

#import "TPerson.h"
#import "TStudent.h"

@implementation TPerson

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s -- %@ -- %@",__func__ ,anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
@end
输出:
-[TPerson methodSignatureForSelector:] -- formatPerson
-[TPerson forwardInvocation:] -- <TPerson: 0x10074e440> -- formatPerson
Hello, World!

程序正常运行。这就是慢速转发流程

查看 forwardInvocation 官方解释

image.png

forwardInvocation 进行操作。根据自身的需要进行相关的处理。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%s -- %@ -- %@",__func__ ,anInvocation.target, NSStringFromSelector(anInvocation.selector));
    
    TStudent * ts = [TStudent alloc];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    } else if ([ts respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:ts];
    } else {
        NSLog(@" %s -- %@", __func__ , NSStringFromSelector(anInvocation.selector));
    }
}
输出:
-[TPerson methodSignatureForSelector:] -- formatPerson
-[TPerson forwardInvocation:] -- <TPerson: 0x1060474a0> -- formatPerson
-[TStudent formatPerson]
Hello, World!

Hopper 反汇编工具

当我们在遇到 unrecognized 崩溃的时候。我们可以通过反汇编可执行文件 CoreFoundation 进行查找。

image.png

我们可以通过 image list 获取所有的镜像文件,找到 CoreFoundation 的路径。

image.png

接下来我们就在 Hopper 中打开 CoreFoundation

image.png

然后我们就全局搜索 __forwarding_prep_0

image.png

发现在 CoreFoundation__forwarding_prep_0 只有一个地方在调用。在 __forwarding_prep_0 方法中,调用了 ____forwarding___ 方法。接下来我们查看一下 ____forwarding___ 方法。

image.png

如果没有实现了 forwardingTargetForSelector: 方法就会走到 loc_64a67 里面。 如果 forwardingTargetForSelector: 方法的返回值为空就会走到 loc_64a67 里面。

image.pngloc_64a67 中判断是否有僵尸对象。

image.png 判断是否能够响应 methodSignatureForSelector: 方法。如果能够响应且返回值不等于空,就继续走r14,等于空就走到 loc_64dd7。显然这个地方不等于空。

image.png 接下来就继续往下走

image.png 然后就会判断是否能够响应 _forwardStackInvocation: 方法。 我们在工程中无法使用_forwardStackInvocation: 这个方法,显然这个方法是系统提供的一个内部层面的方法,没有对外界进行暴露。如果能够响应就会继续往下走调用 [NSInvocation requiredStackSizeForSignature:r12]; 方法。如果不能响应就直接报错。

这就是为什么在慢速转发流程中 forwardInvocation 能够被调用执行到的原因。

总结

消息转发流程图: image.png