阅读 315

iOS runtime之objc_msgSend消息转发

码字不易,求一波点赞,关注。拜谢!!!

前言

OC方法查找如果快速查找流程、慢速查找流程都没有找到对应的imp,并且动态方法决议也没有动态添加对应的imp,就会进入消息转发流程, 前面已经分析了objc_msgSend快速查找流程objc_msgSend慢速查找流程iOS runtime之objc_msgSend动态方法决议,本文就来探索消息转发流程。

准备工作

1: 消息转发流程探索

1.1: 消息转发源码流程探索

// 篇幅原因,只截取部分代码
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    // 指定消息转发的imp
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    ...
    
    for (...) {
        ...
        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            // 按照继承链(cls->supercls->nil)一直查找到nil都没有查找到sel对应的IMP
            // 且动态方法决议也没起作用,就开始消息转发
            imp = forward_imp;
            break;
        }
        ...
    }
    ...
}
复制代码
  • 根据lookUpImpOrForward函数的源码可知消息转发的imp默认是_objc_msgForward_impcache,全局搜索,在C++源码里未找到相关实现,根据之前探索的经验,进入汇编文件objc-msg-arm64.s查找到了相关源码__objc_msgForward_impcache

image.png

  • __objc_msgForward_impcache跳转__objc_msgForward,然后又取值__objc_forward_handler存入p17,最后调用p17。全局搜索__objc_forward_handler,未找到实现部分,根据经验去掉下划线搜索,在C++文件objc-runtime.mm中找到了相关源码。

image.png

image.png

  • 默认void *_objc_forward_handler = (void*)objc_defaultForwardHandlerobjc_defaultForwardHandler函数指针里面就是经典的unrecognized selector sent to xxx报错。

  • 这里全局搜索unrecognized selector sent to,共找到三个实现的地方,全部加上断点,运行代码,却发现直至崩溃都没有进入相关位置,而且三个位置的报错信息都只有instance,没有class,注释也显示被CoreFoundation替换了。

  • 直到注意到objc_setForwardHandler函数,并打断点输出传入的fwd参数,才恍然大悟,程序运行起来后将_objc_forward_handler赋值为了CoreFoundation_CF_forwarding_prep_0函数。

1.2: 案例验证

还是前文的单身狗没有女朋友案例,创建SDSingleDog类,声明-girlfriend+getMarried方法,都不实现,运行代码,单身狗开始找女朋友,没找到,他就崩溃了。

@interface SDSingleDog : NSObject

- (void)girlfriend;

+ (void)getMarried;

@end

@implementation SDSingleDog

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        SDSingleDog *singleDog = [SDSingleDog alloc];
        [singleDog girlfriend];
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

*************************** 运行结果 ***************************

2021-07-12 16:30:02.439981+0800 KCObjcBuild[4559:142606] -[SDSingleDog girlfriend]: unrecognized selector sent to instance 0x1018adac0
复制代码

运行结果图解:

image.png

从经典的unrecognized selector sent to xxx报错信息可以看出:

  • 在抛出异常之前有_CF_forwarding_prep_0函数和___forwarding___函数的调用,根据iOS runtime之objc_msgSend动态方法决议猜测,这应该是消息转发触发的相关流程。
  • _CF_forwarding_prep_0函数、___forwarding___函数和doesNotRecognizeSelector方法都为CoreFoundation库里的源码。

由于CoreFoundation源码并未完全开源,前文iOS runtime之objc_msgSend动态方法决议我们已经通过日志辅助调试的方式知道了消息转发的相关方法forwardingTargetForSelector:methodSignatureForSelector:,下面我们将由此展开消息转发的分析。

2: 快速转发

2.1: 快速转发API分析

通过日志辅助功能知道了forwardingTargetForSelector:方法,但是源码里默认实现只是返回nil

image.png

为了快速了解此方法,同时按住command + shift + 0,唤出开发文档,搜索forwardingTargetForSelector:方法,查看相关文档。

image.png

根据开发文档可知forwardingTargetForSelector:方法就是给未识别的消息返回一个应首先指向的对象,让这个对象去接收这个消息(不能为nilself),如果这个对象无法处理,也会递归父类查找,查看继承链上的父类是否可以处理。

2.2: 案例验证快速转发

声明SDSingleDog类和XJStudent类,SDSingleDog声明- (void)girlfriend方法,但不实现,XJStudent类实现- (void)girlfriend方法,在main函数中用SDSingleDog类的对象调用- (void)girlfriend方法(SDSingleDog类和XJStudent类可以不是继承关系)。

@implementation SDSingleDog

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (@selector(girlfriend) == aSelector) {
        return [[XJStudent alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

@implementation XJStudent

- (void)girlfriend
{
    NSLog(@"%s, The student have girlfriend", __func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SDSingleDog *singleDog = [SDSingleDog alloc];
        [singleDog girlfriend];
    }
    return 0;
}

************************ 运行结果 ************************

2021-07-13 00:25:55.525656+0800 KCObjcBuild[4424:102160] -[XJStudent girlfriend], The student have girlfriend
复制代码
  • 消息成功被转发给了XJStudent类的对象。SDSingleDog类和XJStudent类没有任何关系,但是经过重定向之后,程序正常运行。

这种方法使对象有机会在更昂贵的常规转发forwardInvocation:接管之前重定向发送给它的未知消息。当您只想将消息重定向到另一个对象时,这非常有用,并且可以比常规转发快一个数量级,因此也被称为快速转发

3: 慢速转发

3.1: 慢速转发API分析

如果消息快速转发没有实现,就会进入慢速转发(也叫常规转发)流程,同样同时按住command + shift + 0,唤出开发文档,搜索forwardInvocation:方法,查看相关文档。

image.png

methodSignatureForSelector.jpg

根据文档可知若要响应对象本身无法识别的方法,除了forwardInvocation:,还必须重写methodSignatureForSelector:。转发消息的机制使用从methodSignatureForSelector:获取的信息来创建要转发的NSInvocation对象。重写methodSignatureForSelector:方法必须为给定的selector提供适当的方法签名NSMethodSignature,可以是预先制定一个方法签名,也可以是向另一个对象请求一个方法签名。如果methodSignatureForSelector:方法返回的是nil,就不会调用forwardInvocation:

3.2: 案例验证慢速转发

SDSingleDog类的methodSignatureForSelector:方法直接返回[super methodSignatureForSelector:aSelector],即nil

@implementation SDSingleDog

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [super methodSignatureForSelector:aSelector];
}

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

@end

************************ 运行结果 ************************

2021-07-13 11:51:04.491402+0800 KCObjcBuild[1693:50929] -[SDSingleDog girlfriend]: unrecognized selector sent to instance 0x101931220
复制代码
  • methodSignatureForSelector:方法直接返回nil,慢速转发终止,程序崩溃,抛出unrecognized selector sent to xxx异常。
@implementation SDSingleDog

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (@selector(girlfriend) == aSelector) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

@end

************************ 运行结果 ************************

2021-07-13 14:49:32.360478+0800 KCObjcBuild[2748:93742] ___target = <SDSingleDog: 0x1058a8360>, sel = girlfriend, methodSignature = <NSMethodSignature: 0xb78f2e4cb57ba1c7>
复制代码
  • methodSignatureForSelector:方法根据需要返回NSMethodSignature对象,就会调用forwardInvocation:方法进行消息转发,anInvocation参数里保存了NSMethodSignature方法签名,被转发过来的selector,以及方法接收者target。此时程序不会再崩溃,可以根据需要进行消息转发,如果被转发的对象也不能响应消息,它可以在自己的forwardInvocation:方法里继续转发。
@implementation SDSingleDog

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (@selector(girlfriend) == aSelector) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"___target = %@, sel = %@, methodSignature = %@", anInvocation.target, NSStringFromSelector(anInvocation.selector), anInvocation.methodSignature);
    SEL aSelector = [anInvocation selector];
    if (aSelector == @selector(girlfriend)) {
        XJBoy *boy = [[XJBoy alloc]init];
        anInvocation.target = boy;
        [anInvocation invoke];
    }
    
}

@end

@implementation XJBoy

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (@selector(girlfriend) == aSelector) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"___target = %@, sel = %@, methodSignature = %@", anInvocation.target, NSStringFromSelector(anInvocation.selector), anInvocation.methodSignature);
    SEL aSelector = [anInvocation selector];
    if (aSelector == @selector(girlfriend)) {
        XJPerson *person = [[XJPerson alloc]init];
        anInvocation.target = person;
        anInvocation.selector = @selector(loveEveryone);
        [anInvocation invoke];
    }
}

@end

@implementation XJPerson

- (void)loveEveryone
{
    NSLog(@"%s, Everyone loves me, I love everyone", __func__);
}

@end

************************ 运行结果 ************************

2021-07-13 15:22:01.636226+0800 KCObjcBuild[3262:114582] ___target = <SDSingleDog: 0x10090df20>, sel = girlfriend, methodSignature = <NSMethodSignature: 0x9f2189f206557a37>
2021-07-13 15:22:01.637765+0800 KCObjcBuild[3262:114582] ___target = <XJBoy: 0x10071a760>, sel = girlfriend, methodSignature = <NSMethodSignature: 0x9f2189f206557a37>
2021-07-13 15:22:01.637832+0800 KCObjcBuild[3262:114582] -[XJPerson loveEveryone], Everyone loves me, I love everyone
复制代码
  • SDSingleDog无法响应girlfriend消息,将消息转发给XJBoyXJBoy也无法响应,继续转发给XJPerson,并将selector更改为loveEveryone,最后在XJPerson查找到了相应的方法,消息转发成功。

forwardInvocation:方法就像不能识别的消息的转发调度中心,它能够为一个无法被识别的消息指定新的接收者,或者将这个消息翻译成另外一个消息,或者干脆“吃掉”这个消息,所以即使不处理也不会崩溃。

4: 消息转发总结和流程图

4.1: 消息转发总结

  • 快速转发:通过forwardingTargetForSelector:方法给未识别的消息返回一个应首先指向的对象,让这个对象去接收这个消息(不能为nilself),执行这个对象的查找流程(方法快速查找->方法慢速查找,也会递归父类直到nil),如果没找到就进入慢速转发流程。
  • 慢速转发:通过methodSignatureForSelector:方法和forwardInvocation:方法共同实现,如果methodSignatureForSelector:方法返回nil,慢速查找流程结束;如果返回正确的方法签名,forwardInvocation:方法就可以进行消息转发,可以指定新的消息接收者,可以更改selector,被指定的新接收者可以再次进行转发。

4.2: 消息转发流程图

消息转发流程图.jpg

5: 反汇编探索CoreFoundation

前面分析过CoreFoundation并未完全开源,关于消息转发的源码无从看起,为了探索相应的流程,我们使用反汇编的方式来探索。

首先获取CoreFoundation的可执行文件,新建arm64架构工程运行起来后,随便打个断点断住,然后使用lldbimage list指令输出所有镜像文件的路径,找到CoreFoundation对应的路径,打开Finder,同时按住command + shift + G,呼出前往文件夹功能,把CoreFoundation的路径粘贴进去回车,就可以拿到了。

image.png

5.1: Hopper探索CoreFoundation

image.png

Hopper打开CoreFoundation的可执行文件,搜索_forwarding_prep_0函数,发现全局只有一个,而且跟崩溃信息显示的一样会调用___forwarding___函数,双击进入___forwarding___函数。

image.png

快速转发流程

  • 如果forwardingTargetForSelector:方法未实现,跳转loc_64a67流程。
  • 如果forwardingTargetForSelector:方法返回nilself,跳转loc_64a67流程。

image.png

慢速转发流程

  • 如果methodSignatureForSelector:方法未实现,跳转loc_64dd7流程。
  • 如果methodSignatureForSelector:方法返回nil,跳转loc_64e3c流程。
  • 如果methodSignatureForSelector:方法返回方法签名,按照流程继续往下面走。

image.png

  • loc_64dd7流程:直接输出报错,然后跳转loc_64e3c流程。
  • loc_64e3c流程:调用doesNotRecognizeSelector:方法。

image.png image.png

  • doesNotRecognizeSelector:方法根据消息接收者是类还是对象来输出相应的错误信息,然后抛出异常。

image.png

  • 如果methodSignatureForSelector:方法返回方法签名,就根据方法签名生成NSInvocation类的对象,然后调用forwardInvocation:方法。

5.2: ida探索CoreFoundation

image.png

ida打开CoreFoundation的可执行文件,在左侧Function Name菜单栏右键选择Quick filter搜索_CF_forwarding_prep_0函数,点按F5转换成伪代码模式查看,发现全局只有一个,而且跟崩溃信息显示的一样会调用___forwarding___函数,双击进入___forwarding___函数。

image.png

快速转发流程

  • forwardingTargetForSelector:方法的处理,未实现、返回nilself跳转慢速转发流程,否则进行快速转发。

image.png

慢速转发流程

  • methodSignatureForSelector:未实现,报错调用doesNotRecognizeSelector:方法抛出异常。

image.png

  • methodSignatureForSelector:方法返回nil,就跳转LABEL_50调用doesNotRecognizeSelector:方法抛出异常。
  • 如果methodSignatureForSelector:方法返回方法签名,就根据方法签名生成NSInvocation类的对象,然后调用forwardInvocation:方法。

5.3: 反汇编总结

  • 根据两个反汇编工具对于CoreFoundation的探索,发现消息转发流程和我们猜测的大致相同。
  • 在我们不知道底层实现,并且没有相关源码时,反汇编是一种很好的的探索方式。

6: 总结

  • 方法的查找是一个复杂且繁琐的过程,有快速查找,慢速查找,苹果为了程序的稳定性,还做了非常多的努力,比如动态方法决议,快速消息转发,慢速消息转发等,可谓用心良苦。关于objc_msgSend的探索到这里基本也就结束了。
文章分类
iOS
文章标签