011-消息的动态转发流程(下)

通过这篇文章可以获得什么

instrumentObjcMessageSends辅助分析方法的介绍

作用

可以打印出指定区域内调用的所有的方法

使用方式

// 外部引用定义
extern void instrumentObjcMessageSends(BOOL flag);

//使用区域,将想要查找的方法用instrumentObjcMessageSends圈起来
//开始监听传递参数为YES,结束监听传递参数为NO
FFPerson *person = [FFPerson alloc];
instrumentObjcMessageSends(YES);
[person likeGirls];
instrumentObjcMessageSends(NO);
复制代码

如果查看结果

此方法将指定函数开始结束之间调用的函数全部以文件的形式输出,存储路径为:\tmp\msgSends,找到msgSends-xxxx的文件,此文件内就是全部调用的函数了。

查找流程图解:
msgSends-00.jpg

源码所在位置

当objcMsgLogEnabled为true的时候,将记录这中间调用的所有的方法

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
复制代码

为什么会关注到instrumentObjcMessageSends

由于消息的动态方法决议之后所有可以看到源码的流程现都已经走完了,那么探索也就中断了,还好有那个骚气的男人发现了这个方法,让探索由可知的源码向未开源的源码探索......

一般探索思路

通过instrumentObjcMessageSends开启上帝视角

快速转发

通过上面msgSends-16848文件看到了熟悉的resolveInstanceMethod,这个方法是动态方法决议,看到下一个方法forwardingTargetForSelector就变成了探究最新的线索,既然没有源码,那么就查看一下官方文档,在Developer Decumentation中查找
forwardingTargetForSelector.png
通过此文档可以得知forwardingTargetForSelector为快速转发流程的一个执行者,如果使用此方法对某一个函数进行重定向,那么将非常的有效。forwardingTargetForSelector会返回一个对象,成为未识别的消息的第一继承者,即方法的重定向。如果你想要更多的操作自由,请用forwardInvocation进行操作。

阶段一:代码初步论证

案例代码:

@interface FFPerson : NSObject
- (void)likeGirls;
@end

@implementation FFPerson

//likeGirls方法未实现

//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FFPerson *person = [FFPerson alloc];
        [person likeGirls];   
    }
    return 0;
}
复制代码

控制台打印:

2021-07-04 23:58:17.970363+0800 002-instrumentObjcMessageSends辅助分析[17126:1382603] -[FFPerson forwardingTargetForSelector:] - likeGirls
2021-07-04 23:58:17.972060+0800 002-instrumentObjcMessageSends辅助分析[17126:1382603] -[FFPerson likeGirls]: unrecognized selector sent to instance 0x10055fa30
复制代码

结果是崩溃,但是在崩溃之前打印了-[FFPerson forwardingTargetForSelector:] - likeGirls,证明了上述的探索方向是OK的,在此过程可以对方法进行重定向,以防止程序crash

阶段二:对代码进行了进一步改动

创建一个FFBoys的类,FFBoys.h文件中不声明任何方法,在FFBoys.m文件中实现likeGirls实例方法

@interface FFBoys : NSObject
//为声明任何方法
@end

@implementation FFBoys
//实现了likeGirls方法
- (void)likeGirls {
    NSLog(@"%s",__func__);
}
@end

@interface FFPerson : NSObject
//声明了likeGirls实例方法
- (void)likeGirls;
@end

@implementation FFPerson
//并未实现likeGirls实例方法

//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    //此刻将FFPerson的实例方法likeGirls发送到了FFBoys,让FFBoys来实现
    return [FFBoys alloc];
}
@end
复制代码

控制台打印结果:

2021-07-05 00:07:17.270104+0800 002-instrumentObjcMessageSends辅助分析[17170:1388173] -[FFPerson forwardingTargetForSelector:] - likeGirls
2021-07-05 00:07:17.270736+0800 002-instrumentObjcMessageSends辅助分析[17170:1388173] -[FFBoys likeGirls]
复制代码

可以很清晰的得知FFPerson的实例方法likeGirlsFFBoys实现了。

疑问点一:为什么好好的方法,不在FFPerson类中自己实现,还要启用快速转发,由FFBoys来实现呢?

阶段三:定制一个背锅侠,动态添加新方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    FFBoys * boys = [FFBoys alloc];
    //动态插入方法
    return [super forwardingTargetForSelector:aSelector];
}
复制代码

慢速转发

FFBoys这个背锅侠不想背锅的时候,他也不想实现这个方法,那么此时就会进入到慢速转发了。也就是msgSends-16848methodSignatureForSelector方法,使用相同的方式,那么就查看一下官方文档,在Developer Decumentation中查找:
methodSignatureForSelector.png
给定一个sel,返回一个方法的签名,跟forwardInvocation关联使用,那么就查看一下官方文档,在Developer Decumentation中查找:
forwardInvocation.png

阶段一:methodSignatureForSelector

案例代码

@implementation FFPerson

//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    FFBoys * boys = [FFBoys alloc];
    //动态插入方法
    return [super forwardingTargetForSelector:aSelector];
}

//慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    
    return  [super methodSignatureForSelector:aSelector];
}

@end
复制代码

控制台打印结果:

2021-07-05 00:42:17.146674+0800 002-instrumentObjcMessageSends辅助分析[17283:1407758] -[FFPerson forwardingTargetForSelector:] - likeGirls
2021-07-05 00:42:17.147110+0800 002-instrumentObjcMessageSends辅助分析[17283:1407758] -[FFPerson methodSignatureForSelector:] - likeGirls
2021-07-05 00:42:17.147231+0800 002-instrumentObjcMessageSends辅助分析[17283:1407758] -[FFPerson likeGirls]: unrecognized selector sent to instance 0x103851710
复制代码

结果依然是崩溃,这里已经打印了methodSignatureForSelector,由于并未返回签名,并且未配合forwardInvocation方法使用。

阶段二:forwardInvocation

案例代码:

@implementation FFPerson

//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    FFBoys * boys = [FFBoys alloc];
    //动态插入方法
    return [super forwardingTargetForSelector:aSelector];
}

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
}

@end
复制代码

我这里通过判断sel是否是likeGirls,并为其添加了方法签名,然后添加了forwardInvocation方法,但是并未实现任何事务,这里不会崩溃

事务的概念

关于事务,methodSignatureForSelector方法通过创建并返回一个方法签名之后,进入forwardInvocation,这个方法就是用来处理事务的,当苹果一系列的转发流程来到这里,通过方法签名生成了一个事务,那么这个事务是可做可不做的,会保存anInvocation比如当你想起我的时候,你问我,在吗,我会回答你,我很好,一直在等你。

代码验证:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ - %@",anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
复制代码

控制台打印

2021-07-05 00:56:28.493284+0800 002-instrumentObjcMessageSends辅助分析[17350:1416427] -[FFPerson forwardingTargetForSelector:] - likeGirls
2021-07-05 00:56:28.493856+0800 002-instrumentObjcMessageSends辅助分析[17350:1416427] -[FFPerson methodSignatureForSelector:] - likeGirls
2021-07-05 00:56:28.494254+0800 002-instrumentObjcMessageSends辅助分析[17350:1416427] <FFPerson: 0x104016a30> - likeGirls
复制代码

通过打印证明了likeGirls这个事务的存在,只要你想我在的时候我一直都在,这就是forwardInvocation的涵义,当你不想找到我的时候,当前forwardInvocation就会当作什么都没发生过。此事务就会被流失,此为慢速转发流程

疑问点二:慢速转发流程与快速转发流程的区别是什么?

真实场景防止崩溃

将慢速转发流程methodSignatureForSelector + forwardInvocation,放在NSObject的分类中实现,那么程序就不会因为找不到方法而崩溃了。只是假象的消失了,问题还是真实存在的,并且对内存资源造成了更多的浪费,意味着在这个流程中必然经历了很多的多余的方法和不必要的流程

慢速转发注意点:

要响应您的对象本身无法识别的方法,除了 forwardInvocation: 之外,您还必须覆盖 methodSignatureForSelector:。 转发消息的机制使用从 methodSignatureForSelector: 获得的信息来创建要转发的 NSInvocation 对象。 您的覆盖方法必须为给定的选择器提供适当的方法签名,或者通过预先制定一个或通过向另一个对象询问一个。

forwardInvocation完成代码

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ - %@",anInvocation.target, NSStringFromSelector(anInvocation.selector));
    FFBoys *boys = [FFBoys alloc];
    //如果自己可以处理,直接调用
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    }
    //判断FFBoys是否可以处理,如果可以,直接调用
    else if ([boys respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:boys];
    }
    //没有任何人去实现,那么这里将错误上报
    else {
        NSLog(@"发生了错误的方法:%@ - %@",anInvocation.target, NSStringFromSelector(anInvocation.selector));
    }
}
复制代码

逆向探索方式

通过lldb动态调试指令bt,打印当前的堆栈
carsh时的堆栈信息.png

这里可以得知调用likeGirls依然是崩溃的,但是通过堆栈信息可以找打崩溃前都调用了那些函数doesNotRecognizeSelector,此个函数时当快速查找慢速查找都失效的时候会调用的方法,是属于coreFoundation框架下的,同时此方法的上两个方法同时也是coreFoundation库内的函数,不由得感觉到有鬼,有可能是线索

找Apple的CoreFoundation源码

[Souece Browser地址:opensource.apple.com/tarballs/CF…]

CF.png

找到这里,我觉得我又行了又站起来了。开始搞CoreFoundation源码

源码搜索:___forwarding___forwarding

forwarding.png
源码搜索:_CF_forwarding_prep_0
_CF_forwarding_prep_0.png

经过一阵瞎逼操作,啥也没找到,最后得出结论CoreFoundation并没有完全的开源,Apple爸爸还是你爸爸。哎~不给你看。

去系统里面偷一个CoreFoundation反汇编调试

CoreFoundation动态库资源获取

CoreFoundation动态库资源和官网源码

Hopper Disassembler反汇编工具介绍

官网地址:https://www.hopperapp.com/

Hopper.png

基础功能介绍

Hopper基础功能.png

探索开始,搜索_CF_forwarding_prep_0与___forwarding___

forwardAndPrep0.png

这里已经在对动态库CoreFoundation反汇编中发现了_CF_forwarding_prep_0___forwarding___,并且发现了调用顺序跟报错的堆栈式一致的。但是看反汇编的难度比较大,Hopper给我们提供一个更好的方式,通过伪代码的方式分析一下___forwarding___都做了什么?

Hopper伪代码分解CoreFoundation的消息转发流程

__forward__分解.png

自此,消息的快速转发与慢速转发整体流程已经探索完成。

消息的动态转发机制流程图

动态消息转发流程图.png

解答在源码探索过程中提出的疑问点:

为什么好好的方法,不在FFPerson类中自己实现,还要启用快速转发,由FFBoys来实现呢?

方法的查找过程中,经过前面一些列的过程,到动态方法决议,这里方法找不到也没有关系,再到forwardingTargetForSelector,你告诉我谁有这个方法谁有相同的方法,即找一个背锅侠

慢速转发流程与快速转发流程的区别是什么?

慢速转发流程相对于快速转发流程,对于开发者来讲更加的自由灵活的感觉。开发者只要提供一个签名,这个签名可以通过方法直接得到的,接下来通过forwardInvocation可以想处理也可以不处理`。

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