OC 消息转发流程分析

上一篇消息查找流程 探索了消息查找流程:快速查找慢速查找以及动态方法解析
但消息机制还不完整,如果动态方法解析之后,仍无法找到IMP,那又该如何处理呢?

消息转发

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
  • 没有找到实现, 而且动态方法解析也没有帮助,就会进入消息转发流程

消息转发实现 __objc_msgForward_impcache

__objc_msgForward_impcache是通过汇编实现,这里以 arm64为例。

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward
  • __objc_msgForward_impcachearm64下,只调用了__objc_msgForward
  • __objc_msgForward 调用了__objc_forward_handler

_objc_forward_handler

  • _objc_forward_handlerC源码中有默认实现
// Default forward handler halts the process.
__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
  • 这里仅作为参考,实际似乎并没有走这里
  • 这就说明_objc_forward_handler在某个地方被修改了
  • 在源码中全局搜索_objc_forward_handler发现了 objc_setForwardHandler()函数

objc_setForwardHandler

objc_setForwardHandler
  • 打了个断点发现函数objc_setForwardHandlerdyld初始化镜像::doImageInit的时候触发的具体实现应该放在CoreFoundation__CFInitialize中,但__CFInitialize的并没有找到关于forwardHeadler的实现(此路不通)。
  • 换一种探索方式,既然分歧点从_objc_msgForward_impcache开始,那就给_objc_msgForward_impcache下一个符号断点来窥探一二。

下符号断点来窥探闭源部分的内容

1. 给_objc_msgForward_impcache下符号断点

这里有一点要说明一下:测试Demo是在x86_64架构下运行的,和arm64有一点差异
比如:在arm64架构下只有_objc_msgForwrd,没有_objc_msgForward_stret

_objc_msgForward_impcache 断点

进来了,激动
接着往下

2. _objc_msgForward

_objc_msgForward

出现_objc_forward_handler和前面探索吻合,继续往下走

3 .forwarding_prep_0_

__forwarding_prep_0___

走到第22行,进入___forwarding___
__forwarding_prep_0___line_22

4. forwarding

___forwarding___

第51行出现了forwardingTargetForSelector:,第60行发送该消息
___forwarding___line_51

第93行出现了methodSignatureForSelector:,第104行发送该消息

___forwarding___line_93

第183行出现了forwardInvocation:,第199行发送该消息

___forwarding___line_183

第336行出现了doesNotRecognizeSelector:,第347行发送该消息

___forwarding___line_336

  • ___forwarding___中可以得出消息转发的大致流程:
    resolveInstanceMethod: > forwardingTargetForSelector: > methodSignatureForSelector: > forwardInvocation:
  • 汇编跟踪的效率太低,下面介绍一种更快捷的方法

从OC的消息发送日志入手

  • 之前提到的void instrumentObjcMessageSends(BOOL flag)函数是控制是否记录OC消息发送日志的函数
  • 这个也可以用于非源码环境的工程中,在使用前需要引用一下该函数extern void instrumentObjcMessageSends(BOOL flag);
1. 写一个Demo
//引用instrumentObjcMessageSends函数
extern void instrumentObjcMessageSends(BOOL flag);

@interface Person : NSObject
- (void)sayInstance;
+ (void)sayClass;
@end
@implementation Person 
/**
未实现
- (void)sayInstance;
+ (void)sayClass;
* 这里会报⚠️,只模拟消息转发的情况
*/
@end

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

    Person *p = [Person new];
    //开启OC消息日志
    instrumentObjcMessageSends(true);
    //发送消息
    [p sayInstance];
    //关闭OC消息日志
    instrumentObjcMessageSends(false);
    return 0;
}
2. 找到msgSend-***,分析日志
  • /tmp路径下回生成名为msgSend-***
//摘取了关键的日志信息
+ Person NSObject resolveInstanceMethod:
+ Person NSObject resolveInstanceMethod:
- Person NSObject forwardingTargetForSelector:
- Person NSObject forwardingTargetForSelector:
- Person NSObject methodSignatureForSelector:
- Person NSObject methodSignatureForSelector:
- Person NSObject class
+ Person NSObject resolveInstanceMethod:
+ Person NSObject resolveInstanceMethod:
- Person NSObject doesNotRecognizeSelector:
- Person NSObject doesNotRecognizeSelector:
- Person NSObject class
......
  • 这里出现了几个方法:
  1. resolveInstanceMethod:
    不管是实例方法还是类方法方法动态解析(未处理)一定会调用NSObject resolveInstanceMethod:,也就是说NSObject resolveInstanceMethod:执行失败标志着方法动态解析失败,也是消息转发的必经之路;其中NSObject resolveInstanceMethod之后如果还未得到解决就结束了动态方法解析流程,将进入了消息转发流程
  2. forwardingTargetForSelector:
    瞄一眼forwardingTargetForSelector:苹果官方是如何描述的:
    forwardingTargetForSelector 说明

总结一下:

  1. 如果要让forwardingTargetForSelector:生效,就需要返回一个对象作为未识别消息接收者,但是返回对象不能是self否则将进入死循环
  2. 如果实现了forwardingTargetForSelector:但是没有适合的对象作为新的接收者,那就让super处理。
  3. 如果未实现forwardingTargetForSelector:就有机会调forwardInvocation:
  1. methodSignatureForSelector:
methodSignatureForSelector: 说明

总结一下:

  1. methodSignatureForSelector: 实现并返回方法签名,是否进行转发的先决条件。
  2. 返回了一个方法签名NSMethodSignature *,有了这个方法签名就会创建NSInvocation
  1. doesNotRecognizeSelector:
  • 官方的解释:处理一些接收者无法识别的消息
  • 抛出异常unrecognized selector sent to instance
  1. forwardInvocation:
    forwardingTargetForSelector:文档上提到了这么一个方法有点奇怪,实际上消息日志上并没有记录,不慌先看看forwardInvocation:的官方说明
    forwardInvocation: 说明

总结一下:

  1. forwardInvocation: 的作用是将消息转发给其它对象
  2. forwardInvocation: 使用前必须实现methodSignatureForSelector:
  3. forwardInvocation: 是从methodSignatureForSelector:中获取需要转发的NSInvocation对象
3. 验证猜想

现在Demo的现状运行就会报错

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person sayInstance]: unrecognized selector sent to instance 0x10052ddb0'

Person中实现消息转发的方法:

@implementation Person

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

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

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
    NSLog(@"%s SEL %@",__func__,NSStringFromSelector(aSelector));
    [super forwardInvocation:invocation];
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"%s SEL %@",__func__,NSStringFromSelector(aSelector));
    [super doesNotRecognizeSelector:aSelector];
}

@end

输出

***[7058:192271] -[Person forwardingTargetForSelector:] SEL sayInstance
***[7058:192271] -[Person methodSignatureForSelector:] SEL sayInstance
***[7058:192271] -[Person forwardInvocation:] SEL sayInstance
***[7058:192271] -[Person doesNotRecognizeSelector:] SEL sayInstance
***[7058:192271] -[Person sayInstance]: unrecognized selector sent to instance 0x1005af3d0
***[7058:192271] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person sayInstance]: unrecognized selector sent to instance 0x1005af3d0'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff385f2d63 __exceptionPreprocess + 250
    1   libobjc.A.dylib                     0x00007fff6e4e1bd4 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff3867d206 -[NSObject(NSObject) __retain_OA] + 0
    3   forwrdingDemo                       0x0000000100001cb3 -[Person doesNotRecognizeSelector:] + 131
    4   forwrdingDemo                       0x0000000100001c0e -[Person forwardInvocation:] + 174
    5   CoreFoundation                      0x00007fff385992c5 ___forwarding___ + 829
    6   CoreFoundation                      0x00007fff38598ef8 _CF_forwarding_prep_0 + 120
    7   forwrdingDemo                       0x0000000100001d37 main + 71
    8   libdyld.dylib                       0x00007fff6f840405 start + 1
    9   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
  • 调用顺序
    1.forwardingTargetForSelector:
    2.methodSignatureForSelector:
    3.forwardInvocation:
    4.doesNotRecognizeSelector
4. 使用forwardingTargetForSelector:将消息转发给指定对象

写一个新的类Friend实现了 sayInstance方法

@interface Friend : NSObject
@end

@implementation Friend
- (void)sayInstance {
    NSLog(@"%s",__func__);
}
@end

PersonforwardingTargetForSelector:方法中,将[Friend new]作为sayInstance的消息接收者

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s SEL %@",__func__,NSStringFromSelector(aSelector));
    if (sel_isEqual(aSelector, @selector(sayInstance))) {
        return [Friend new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

输出

***[7145:202044] -[Person forwardingTargetForSelector:] SEL sayInstance
***[7145:202044] -[Friend sayInstance]
Program ended with exit code: 0
  • sayInstance转发成功,Friend执行了sayInstance
  • forwardingTargetForSelector:中指定了Friend处理sayInstance,结束了消息转发流程
5. 使用forwardInvocation:将消息转发给指定对象

先将PersonforwardingTargetForSelector:复原

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

在实现methodSignatureForSelector:,对sayInstance进行方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

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

最后实现forwardInvocation:转发消息

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
    NSLog(@"%s SEL %@",__func__,NSStringFromSelector(aSelector));
    if ([[Friend new] respondsToSelector:aSelector])
        [invocation invokeWithTarget:[Friend new]];
    else
        [super forwardInvocation:invocation];
}

输出

***[7734:232196] -[Person forwardingTargetForSelector:] SEL sayInstance
***[7734:232196] -[Person methodSignatureForSelector:] SEL sayInstance
***[7734:232196] -[Person forwardInvocation:] SEL sayInstance
***[7734:232196] -[Friend sayInstance]
Program ended with exit code: 0
  • sayInstance转发成功,Friend执行了sayInstance
  • methodSignatureForSelector:中提供了sayInstance创建NSInvocation对象的方法签名
  • forwardInvocation:中做了简单的转发
  • NSInvocation可以根据需求改变入参或者返回参数,这也是forwardInvocation:转发的特点

附上消息转发的流程图


消息转发的流程图

总结

  1. objc_msgSend 从缓存查找imp
  2. 慢速递归查找方法列表
  3. 方法动态解析
    3.1 resolveInstanceMethod: 实例方法解析
    3.2 resolveClassMethod: 类方法解析
    3.3 由于isa指向的独特性,实例方法类方法最终都会掉[NSObject resolveInstanceMethod:]
    3.4 [NSObject resolveInstanceMethod:]方法调用结束,标志着方法动态解析结束
  4. 消息转发
    4.1 forwardingTargetForSelector: 交给其它对象处理
    4.2 methodSignatureForSelector: 提供转发对象NSInvocation需要的方法签名;
    4.3 forwardInvocation: 处理事务(将消息转发给其它对象)
    4.4 forwardingTargetForSelector:forwardInvocation:的区别在于:前者参数和返回值需要和原方法一致,二后者没有这个限制
  5. doesNotRecognizeSelector: 处理接收者无法识别的消息(抛出异常)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容