美文网首页
消息转发

消息转发

作者: 浅墨入画 | 来源:发表于2021-08-04 14:29 被阅读0次

instrumentObjcMessageSends函数补充

instrumentObjcMessageSends可以打印出指定区域内调用的所有方法。并且以文件的形式输出,输出路径为:\tmp\msgSends。找到msgSends-xxxx文件,此文件内就是全部调用的函数了。

<!-- msgSends-xxxx文件 -->
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
......

消息转发流程

快速转发(resolveInstanceMethod)

msgSends-xxxx文件看到了熟悉的resolveInstanceMethod,这个方法是动态方法决议。接着看到下一个方法forwardingTargetForSelector就变成了探究最新的线索,既然没有源码,那么就查看一下官方文档,在Developer Decumentation中查找

image.png

由上面文档可知forwardingTargetForSelector为快速转发流程的一个执行者,如果使用此方法对某一个函数进行重定向,将非常有效。forwardingTargetForSelector会返回一个对象,成为未识别的消息的第一继承者,即方法的重定向。如果你想要更多的操作自由,请用forwardInvocation进行操作。

探索一
<!-- LGPerson.h文件 -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)sayHello;
@end
NS_ASSUME_NONNULL_END

<!-- LGPerson.m文件 -->
#import "LGPerson.h"
@implementation LGPerson
// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super forwardingTargetForSelector:aSelector];
}
@end

<!-- main.m文件 -->
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
    }
    return 0;
}

//控制台打印
2021-08-03 20:51:34.917264+0800 002-instrumentObjcMessageSends辅助分析[12247:10008920] -[LGPerson forwardingTargetForSelector:] - sayHello
2021-08-03 20:51:34.920136+0800 002-instrumentObjcMessageSends辅助分析[12247:10008920] -[LGPerson sayHello]: unrecognized selector sent to instance 0x100524f20

崩溃之前打印了-[LGPerson forwardingTargetForSelector:] - sayHello,证明在此过程可以对方法进行重定向,以防止程序crash

探索二

添加LGStudent类,在LGStudent.m文件中实现sayHello实例方法

<!-- LGStudent.h文件 -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGStudent : NSObject
- (void)sayHello;
@end
NS_ASSUME_NONNULL_END

<!-- LGStudent.m文件 -->
#import "LGStudent.h"
@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s",__func__);
}
@end

<!-- LGPerson.m文件 -->
#import "LGPerson.h"
#import "LGStudent.h"
@implementation LGPerson
// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [LGStudent alloc];
}
@end

//控制台打印
2021-08-03 21:04:10.823489+0800 002-instrumentObjcMessageSends辅助分析[12333:10017517] -[LGPerson forwardingTargetForSelector:] - sayHello
2021-08-03 21:04:10.824215+0800 002-instrumentObjcMessageSends辅助分析[12333:10017517] -[LGStudent sayHello]

发现LGPerson的实例方法sayHelloLGStudent实现了

探索三
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    // LGPerson没有实现,让LGStudent来实现sayHello方法,动态添加新方法
    LGStudent * student = [LGStudent alloc];
    //动态插入方法
    return [super forwardingTargetForSelector:aSelector];
}
慢速转发(methodSignatureForSelector)

如果LGStudent也不想实现这个方法,那么此时就会进入到慢速转发。也就是msgSends-xxxx文件中的methodSignatureForSelector方法,在Developer Decumentation中查看文档

image.png

给定参数sel返回一个方法的签名,跟forwardInvocation关联使用。可以在Developer Decumentation中查看forwardInvocation方法。

探索一

LGStudent类中的sayHello方法实现屏蔽掉,LGPerson.m添加慢速转发方法

<!-- LGPerson.m文件 -->
@implementation LGPerson
// 快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [LGStudent alloc];
}
// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}
@end

//控制台打印
2021-08-03 21:41:50.839340+0800 002-instrumentObjcMessageSends辅助分析[12578:10038368] -[LGPerson forwardingTargetForSelector:] - sayHello
2021-08-03 21:41:50.840679+0800 002-instrumentObjcMessageSends辅助分析[12578:10038368] -[LGStudent sayHello]: unrecognized selector sent to instance 0x10381a800

程序崩溃打印了forwardingTargetForSelector,为什么没有执行消息慢速转发呢?
原因是执行到消息快速转发这一步变成了LGStudent,接下来就会去查找LGStudent的消息慢速转发,而不会执行LGPerson的慢速转发方法。

探索二
<!-- LGPerson.m文件 -->
@implementation LGPerson
// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}
@end

//控制台打印
2021-08-03 21:50:17.643313+0800 002-instrumentObjcMessageSends辅助分析[12634:10043869] -[LGPerson methodSignatureForSelector:] - sayHello
2021-08-03 21:50:17.644181+0800 002-instrumentObjcMessageSends辅助分析[12634:10043869] -[LGPerson sayHello]: unrecognized selector sent to instance 0x100563050

这里已经打印了methodSignatureForSelector,结果依然崩溃。原因是未配合forwardInvocation方法使用。

探索三

添加forwardInvocation方法,与methodSignatureForSelector方法成对使用

<!-- LGPerson.m文件 -->
@implementation LGPerson
// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [super methodSignatureForSelector:aSelector];
}

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

//控制台打印
2021-08-03 21:54:06.061891+0800 002-instrumentObjcMessageSends辅助分析[12650:10046182] -[LGPerson methodSignatureForSelector:] - sayHello
2021-08-03 21:54:06.062656+0800 002-instrumentObjcMessageSends辅助分析[12650:10046182] -[LGPerson sayHello]: unrecognized selector sent to instance 0x10052d290

依然崩溃,原因是未返回签名

探索四
<!-- LGPerson.m文件 -->
@implementation LGPerson
// 慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(sayHello)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

//控制台打印
2021-08-03 22:00:56.467916+0800 002-instrumentObjcMessageSends辅助分析[12694:10051106] -[LGPerson methodSignatureForSelector:] - sayHello

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

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

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

关于事务

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

事务验证

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

//控制台打印
2021-08-03 22:10:48.735821+0800 002-instrumentObjcMessageSends辅助分析[12731:10056027] -[LGPerson methodSignatureForSelector:] - sayHello
2021-08-03 22:10:48.736625+0800 002-instrumentObjcMessageSends辅助分析[12731:10056027] <LGPerson: 0x10058fa30> - sayHello

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

慢速转发流程防止崩溃

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

forwardInvocation方法优化
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%@ - %@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    LGStudent *s = [LGStudent alloc];
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invoke];
    }else if ([s respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:s];
    }else{
        NSLog(@"%s - %@",__func__,NSStringFromSelector(anInvocation.selector));
    }
}

//控制台打印
2021-08-03 22:30:41.252499+0800 002-instrumentObjcMessageSends辅助分析[12853:10067942] -[LGPerson methodSignatureForSelector:] - sayHello
2021-08-03 22:30:41.253172+0800 002-instrumentObjcMessageSends辅助分析[12853:10067942] <LGPerson: 0x100644f30> - sayHello
2021-08-03 22:30:41.253282+0800 002-instrumentObjcMessageSends辅助分析[12853:10067942] -[LGStudent sayHello]

hoper反汇编/IDA反汇编

  • 运行程序崩溃,通过lldb动态调试指令bt,查看堆栈信息
image.png

通过堆栈信息查找崩溃前都调用了哪些函数?发现当快速查找和慢速查找都失效的时候会调用doesNotRecognizeSelector函数,该函数属于coreFoundation框架,同时此方法的上两个方法也是coreFoundation库内的函数,我们就以此为入口探索......

image.png
  • 下载并打开CoreFoundation源码,搜索___forwarding___、forwarding
image.png

各种搜索都没找到,最后得出结论CoreFoundation并没有完全的开源

  • 去系统里面找CoreFoundation动态库

CoreFoundation动态库资源和官网源码
Hopper反汇编工具

  • 打开hopper选择Try the Demo,然后将上一步的可执行文件CoreFoundation拖入hopper进行反汇编,选择x86(64 bits),全局搜索_CF_forwarding_prep_0___forwarding___
image.png
  • 查看__forwarding_prep_0___的汇编伪代码,跳转至___forwarding___
image.png
  • 通过伪代码的方式分析___forwarding___,首先是查看是否实现forwardingTargetForSelector方法,如果没有响应,跳转至loc_64a67即快速转发没有响应,进入慢速转发流程
int ____forwarding___(int arg0, int arg1) {
    rsi = arg1;
    rdi = arg0;
    r15 = rdi;
    var_30 = *___stack_chk_guard;
    rcx = COND_BYTE_SET(NE);
    // 根据参数arg1判断是通过_objc_msgSend_stret或者_objc_msgSend发送消息
    if (rsi != 0x0) {
            r12 = _objc_msgSend_stret;
    }
    else {
            r12 = _objc_msgSend;
    }
    rax = rcx;
    rbx = *(r15 + rax * 0x8);
    rcx = *(r15 + rax * 0x8 + 0x8);
    var_140 = rcx;
    r13 = rax * 0x8;
    // 如果判断为0,跳转至loc_649bb
    if ((rbx & 0x1) == 0x0) goto loc_649bb;

loc_6498b:
    // 判断指针taggedpointer
    rcx = *_objc_debug_taggedpointer_obfuscator;
    rcx = rcx ^ rbx;
    rax = rcx >> 0x1 & 0x7;
    if (rax == 0x7) {
            rcx = rcx >> 0x4;
            rax = (rcx & 0xff) + 0x8;
    }
    if (rax == 0x0) goto loc_64d48;

loc_649bb:
    var_148 = r13;
    var_138 = r12;
    var_158 = rsi;
    // 获取当前类
    rax = object_getClass(rbx);
    r12 = rax;
    r13 = class_getName(rax);
    r14 = @selector(forwardingTargetForSelector:);
    // 判断当前类是否可以响应方法forwardingTargetForSelector
    if (class_respondsToSelector(r12, r14) == 0x0) goto loc_64a67;

loc_649fc:
    rdi = rbx;
    rax = _objc_msgSend(rdi, r14);
    if ((rax == 0x0) || (rax == rbx)) goto loc_64a67;
  • 跳转至loc_64a67,在其下方判断是否响应methodSignatureForSelector方法,如果没有响应跳转至loc_64dd7直接报错
loc_64a67:
    var_138 = rbx;
    if (strncmp(r13, "_NSZombie_", 0xa) == 0x0) goto loc_64dc1;

loc_64a8a:
    rbx = @selector(methodSignatureForSelector:);
    r14 = var_138;
    var_148 = r15;
    if (class_respondsToSelector(r12, rbx) == 0x0) goto loc_64dd7;

loc_64ab2:
    rax = _objc_msgSend(r14, rbx);
    rbx = var_158;
    if (rax == 0x0) goto loc_64e3c;
  • 如果获取methodSignatureForSelector的方法签名为nil,也是直接报错
loc_64dd7:
    rbx = class_getSuperclass(r12);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[2003]);
    }
    else {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[2003]);
    }
    goto loc_64e3c;

loc_64dc1:
    ____forwarding___.cold.1(var_138, r13, var_140, rcx, r8);
    goto loc_64dd7;
}
  • 如果methodSignatureForSelector返回值不为空,则在forwardInvocation方法中对invocation进行处理
loc_64c19:
    r15 = @selector(forwardInvocation:);
    if (class_respondsToSelector(object_getClass(r14), r15) == 0x0) goto loc_64ec2;

loc_64c3b:
    rax = [NSInvocation _invocationWithMethodSignature:r12 frame:var_148];
    r13 = rax;
    _objc_msgSend(0x0, r15);
    var_140 = 0x0;
    r14 = 0x0;
    goto loc_64c76;

loc_64ec2:
    rdi = var_130;
    ____forwarding___.cold.3(rdi, r14);
    goto loc_64ed1;

通过上面查找可以验证,消息转发的方法有3个

  • 快速转发forwardingTargetForSelector
  • 慢速转发methodSignatureForSelector,forwardInvocation
消息转发流程图
消息转发流程图

动态方法决议执行两次的探究

修改LGPerson.m文件代码如下

<!-- LGPerson.m文件 -->
@implementation LGPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s - %@",__func__,NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}
@end

<!-- main.m文件 -->
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
    }
    return 0;
}

//控制台打印
2021-08-04 12:11:14.125273+0800 002-instrumentObjcMessageSends辅助分析[14509:10261254] +[LGPerson resolveInstanceMethod:] - sayHello
2021-08-04 12:11:14.126655+0800 002-instrumentObjcMessageSends辅助分析[14509:10261254] +[LGPerson resolveInstanceMethod:] - sayHello
2021-08-04 12:11:14.126965+0800 002-instrumentObjcMessageSends辅助分析[14509:10261254] -[LGPerson sayHello]: unrecognized selector sent to instance 0x10050ed40

通过控制台打印信息,我们发现动态决议方法resolveInstanceMethod执行了两次,这是为什么呢?下面使用objc4-818.2源码进行探索

  • 在慢速查找流程中,我们了解到resolveInstanceMethod方法的执行是通过lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod来到resolveInstanceMethod源码,在源码中通过发送resolve_sel消息触发,如下所示
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;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 发送resolve_sel消息
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self 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 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));
        }
    }
}
  • resolveInstanceMethod方法的bool resolved = msg(cls, resolve_sel, sel);处加一个断点,通过bt打印第二次调用动态决议的堆栈信息
image.png
  • 查看上面的hopper反汇编代码,配合上面堆栈信息,发现会执行到class_respondsToSelector_inst -> _lookUpImpTryCache,也就是消息转发完都没有找到,会来到_lookUpImpTryCache
loc_64e3c:
    rax = sel_getName(var_140);
    r14 = rax;
    rax = sel_getUid(rax);
    if (rax != var_140) {
            r8 = rax;
            _CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, r14, r8, r9, stack[2003]);
    }
    rbx = @selector(doesNotRecognizeSelector:);
    if (class_respondsToSelector(object_getClass(var_138), rbx) == 0x0) {
            ____forwarding___.cold.2(var_138);
    }
    rax = _objc_msgSend(var_138, rbx);
    asm{ ud2 };
    return rax;

loc_64dd7:
    rbx = class_getSuperclass(r12);
    r14 = object_getClassName(r14);
    if (rbx == 0x0) {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[2003]);
    }
    else {
            _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[2003]);
    }
    goto loc_64e3c;
  • 通过hopper反汇编CoreFoundation的可执行文件,查看methodSignatureForSelector方法的伪代码
  • 通过methodSignatureForSelector伪代码进入___methodDescriptionForSelector的实现
  • 进入___methodDescriptionForSelector的伪代码实现,结合汇编的堆栈打印,可以看到在___methodDescriptionForSelector这个方法中调用了objc4-818class_getInstanceMethod
  • 这一点可以通过代码调试来验证,在class_getInstanceMethod方法处加一个断点,在执行了methodSignatureForSelector方法后,返回了签名,说明方法签名是生效的,苹果在走到invocation之前,给了开发者一次机会再去查询,所以走到class_getInstanceMethod这里,又去走了一遍方法查询sayHello,然后会再次走到动态方法决议
objc_msgSend发送消息的流程总结
  • 快速查找流程 在类的缓存cache中查找指定方法的实现
  • 慢速查找流程 如果缓存中没有找到,则在类的方法列表中查找,如果还是没找到,则去父类链的缓存和方法列表中查找
  • 动态方法决议 如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即重写resolveInstanceMethod/resolveClassMethod 方法
  • 消息转发 如果动态方法决议还是没有找到,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发

如果转发之后也没有,则程序崩溃unrecognized selector sent to instance

相关文章

  • Runtime

    相关简单介绍 消息机制消息传递机制消息转发机制-动态添加方法消息转发机制-快速转发消息转发机制-慢速转发消息转发机...

  • 消息转发机制(动态消息转发)

    例子分析 1)在给程序添加消息转发功能以前,必须覆盖两个方法,即methodSignatureForSelecto...

  • Runtime 消息转发

    目录 消息转发背景知识 消息转发使用方式 消息转发常见问题 消息转发背景知识 1.消息转发的定义Objective...

  • 消息转发

    参考:https://www.jianshu.com/p/76ed71216cde

  • 消息转发

    执行一个没有实现的方法,程序会在运行时挂掉并抛出 unrecognized selector sent to … ...

  • 消息转发

    OC中的方法调用,其实都是转化成objc_msgSend函数调用 1.信息发送 2.动态方法解析 /// 对象消息解析

  • 消息转发

    1. 消息查找 Objective-C 具有很强的动态性,它将静态语言在编译和链接时期做的工作,放置到运行时来处理...

  • 消息转发

    一张图说明消息转发 例如我们进行这样一个操作:People这个类并没有实现perfectGotoSchool这个函...

  • 消息转发

  • 消息转发

    title: 消息转发date: 2017-07-06 15:32:45tags: Method resoluti...

网友评论

      本文标题:消息转发

      本文链接:https://www.haomeiwen.com/subject/ntorvltx.html