iOS底层探究—–消息转发

前言

我们在运行工程时,如果有工程里面的方法没实现,或者找不到,工程会直接报错,导致程序奔溃。这样对于开发者就十分不友好,同时也是十分影响用户体验。所以苹果系统就给与了一次拯救代码的机会,就是通过消息动态决议来找到一个背锅侠来进行承担。这样就使得程序不再奔溃,皆大欢喜。但是,如果这个背锅侠也不能将这个锅给背下的话,那又该如何动态处理这个奔溃的问题了?在苹果系统中,还有没有相关的其他方式的容错处理了?答案当然是有的,就是—-消息转发

资源准备

进入主题

根据前几篇文章,当调用某方法时,经过对缓存方法查找的分析,以及上篇的消息动态决议分析,可以知道:总体上,查找方法是通过sel去查找其对应的imp。先是快速查找,通过objc_msgSendcache中查询,如果找不到,然后再经过慢速查找,通过lookUpImpOrForward方法,在methodlist中查找。找到之后,直接返回对应的imp

如果找不到的话,此时返回的imp就是forward_imp。系统也不是马上就报错,而是给了一次拯救的机会,就是消息动态决议。通过消息动态决议,再重新给赋值一个新的imp

如果也没有进行消息动态决议,那么imp就直接返回nil了。为了不让系统奔溃,那么就找和这个方法相同或者相类似的方法来执行,就能防止系统奔溃,这就叫消息转发的方式。

下面就探索下消息转发的流程。

通过instrumentObjcMessageSends获取日志

为什么说instrumentObjcMessageSends方法能获取日志了?

根据objc源码,在慢查找过程中,当在methodlist中找到方法时,会把该方法通过log_and_fill_cache插入缓存中:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
。。。。

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
    
 。。。。
    
复制代码

log_and_fill_cache方法的实现里面,有个判断,当达到条件的时候,会方法执行的日志。其判断条件中,implementer是必然存在的,所以objcMsgLogEnabled的值,才是判断的关键。
5062F80C-A9BE-446D-9AD1-CA2B7BBCB374.png

下图是logMessageSend方法的实现,其中就有打印日志的文件路径:
705A2A3E-88F5-437B-AE8E-70F5A8B9D03E.png

接下来,就需要搞清楚objcMsgLogEnabled的赋值情况就行。从其定义上,就能知道是一个能够被外部方法的变量。

定义:extern bool objcMsgLogEnabled;

赋值情况:
C8CE0C0E-4D6E-404C-B707-E3798F419259.png
根据赋值情况,是通过instrumentObjcMessageSends方法所传进来的BOOL值来进行赋值的,那么是不是就意味着,我们可以把instrumentObjcMessageSends方法写成供外部调用的方法,传进BOOl值,就可以来控制日志的打印了。

打印的日志,通过前往文件夹(commamd + shift + G),输入/tmp/msgSends-%d路径,就能获取到日志文件。

案例分析

创建LGPerson类,然后再声明sayHello实例方法,方法不实现。然后通过instrumentObjcMessageSends方法,来获取sayHello执行报错的日志。根据日志来进行下一步分析。

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}
复制代码

运行工程后,直接报错
126C82B6-AB25-4440-8917-7D7CF5CE4E4F.png

去查看日志,点开文件夹,然后前往文件夹,输入上述路径即可:
F7332492-D1FB-4DF2-9FC8-2AE8080A4F05.png

打开文件,查看到日志的详情,
FC03B443-11D7-40E7-B4EE-4AD3CEC32CF1.png

forwardingTargetForSelector的重定向

通过日志,我们就知道,在消息动态决议一文中,resolveInstanceMethod方法是发送消息处理imp的。但是forwardingTargetForSelector方法就比较陌生,虽然不知道这个方法的用法,但是可以快捷键(command + shift + 0),就进入到官方文档中,全局搜索forwardingTargetForSelector方法,就能获取该方法的详情了。

通过文档的描述,知道这方法是用作重定向的。比如在本类中的某个不能实现某个方法,那么就可以重定向另外一个局部的对象。

那么就可以把这个方法在LGPerson类里面实现,此时再创建一个LGStudent类,再在这个类里面声明sayHello方法,再对其进行实现。如下面源码所示,在LGPerson类里面,在forwardingTargetForSelector方法重定向到LGStudent类的方法上:

#import "LGPerson.h"
#import "LGStudent.h"

@implementation LGPerson

//快速转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
    return [LGStudent alloc];
}
复制代码

运行工程,根据打印结果可以看出,是调用的LGStudent类的sayHello方法:
96B6DBD0-E6C2-4CB3-A84C-C03DADAD717B.png

  • 也就是说,在LGPerson类里面调用sayHello方法没找到,那么就重定向到LGStudent类的sayHello方法上,让这个方法来行驶LGPerson类想要的功能。

forwardingTargetForSelector使用

到了这里,就有童鞋问了,为啥不是LGPerson类自己来实现这个功能了,反而还要绕这么一圈?就是说LGStudent类罢工不干。那么就得LGPerson类自己来了。从日志上看,接下来就是methodSignatureForSelector方法的实现了。

快速转发forwardingTargetForSelector,没有转发出去,那接下来,就只能是自己的慢速转发了methodSignatureForSelector。同样的快捷键(command + shift + 0),就进入到官方文档中,然后再全局搜索。

从官方文档上知道,这个方法是进行方法签名的,还要搭配forwardInvocation方法使用。在LGPerson类里面实现:

#import "LGPerson.h"
#import "LGStudent.h"

@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 {
    NSLog(@"%@ -- %@",anInvocation.target,NSStringFromSelector(anInvocation.selector));
}

@end
复制代码

运行看结果:
F5C4D9CD-2755-4F9E-A800-F2217EBE28F7.png

  • 在系统里面,所有的方法或者函数,都统称为事务。在methodSignatureForSelector中,只是把LGPerson类sayHello方法保存在anInvocation里面,相当于处理休眠状态。当要调用时,通过forwardInvocation方法,来实现sayHello的功能。

  • 相比于forwardingTargetForSelectormethodSignatureForSelector没有实现sayHello方法,而是只保留的sayHello方法的职能。

与此同时,forwardInvocation方法还能指定事务的子处理者,可以是本身,也能是其他类,如下代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    LGStudent *s = [LGStudent alloc];
    if ([self respondsToSelector:anInvocation.selector]) {//自己处理
        [anInvocation invoke];
    }else if ([s respondsToSelector:anInvocation.selector]){//指定LGStudent类处理
        [anInvocation invokeWithTarget:s];
    }else{
        NSLog(@"%s - %@",__func__,NSStringFromSelector(anInvocation.selector));
    }
}
复制代码

看到这里的童鞋,可能就有疑问了,我要是知道这些个方法,以及用途,也能进行处理,如果不知道了?那该怎么办?

没办法,只能使用终极大招—–汇编

hoper反汇编CoreFoundation

那么现在,把刚刚使用的工具方法都给注释掉

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person sayHello];
    }
    return 0;
}

复制代码

根据打印结果,使用bt汇编指令,查看堆栈的内容。
804806DC-8132-4263-B0E4-486635B1FC78.png
如上图所示,从main.m开始,想要发生doesNotRecognizeSelector(号位置),必然要经过号位置,那么这两处位置,就是我们寻找答案的关键所在。而且都存在于CoreFoundation库里面。

因为我们的程序执行之后,就回变成可执行文件。然后我们把这个可执行文件还原回来,就是反汇编

接下来,就用工具Hopper Disassebler v4,打开CoreFoundationmachO文件,获取里面的汇编代码,再索引forwarding,查看伪代码,得到如下图:
52B3164C-2E9B-4115-A847-5E4C0E2CEF1E.png

  • 根据伪代码就知道,当查找方法时,如果在该类执行了forwardingTargetForSelector方法,就会通过_objc_msgSend再次发送对应消息;如果没有执行就进入goto loc_64a67,走methodSignatureForSelector方法;

A3C23872-1B22-434C-87D8-2111E04F6E14.png

  • 执行methodSignatureForSelector方法后,再经过不断的地址平移,到达forwardInvocation方法

3D3EF800-5705-4459-A01D-3528920E4501.png

小结

  • 慢速查找过程中,在本类中,先进行消息动态决议,如果找到imp,就处理消息;如果还是没有找到imp,就进行消息快速转发

  • 消息快速转发forwardingTargetForSelector,如果找到imp,就处理消息;如果没有找到imp,就进行消息慢速转发

  • 消息慢速转发methodSignatureForSelector,如果做了签名操作,就返回签名,再执行forwardInvocation方法,再处理消息;如果没有签名操作,那么就直接报错了。

消息转发机制流程图

未命名文件-7.png

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