Objective-C中的消息发送总结
2017-01-17 13:30
357 查看
关于OC中的消息发送的实现,在去年也看过一次,当时有点不太理解,但是今年再看却很容易理解。
我想这跟知识体系的构建有关,如果你不认识有砖、水泥等这些建筑的基本组成部分,那么我们应该很难理解建筑是怎么建造出来的吧?
学习新知识,应该也是同样的道理!
原文:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/
消息发送和转发流程可以概括为:消息发送(Messaging)是Runtime通过selector 快速查找IMP的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Frowarding)是在查找IMP失败后一系列转发流程的慢速通道,如果不做转发处理,则会打日志和抛出异常。
本文不讲述开发者在消息发送和转发流程中需要做的事,而是讲述原理。能够很好地阅读本文的前提是你对 Objective-C Runtime 已经有一定的了解,关于什么是消息,Class的结构,Selector、IMP、元类等概念将不再赘述。本文用到的源码为objc-680 和 CF-1153.18,逆向CoreFoundation.framework的系统版本为macOS10.11.5,汇编语言架构为x86_64。
这里面包含一些有意义的宏:
从上面的代码可以看出方法查找IMP的工作交给了OC中的
*
*
*
*
当需要发送消息时,编译器会生成中间代码,根据情况分别调用
这也是为什么
关于 Calling Convention,可以去看Bang 的文章动态调用C函数的 Calling Convention一节
注意
1.如果使用缓存(
2.如果是第一次用到这个类且
之后的逻辑整理如下:
1.如果 selector 是需要被忽略的垃圾回收用到的方法,则将 IMP 结果设为
2.查找当前类中的缓存,跟之前一样,使用
3.在当前类中的方法列表(method list)中进行查找,也就是根据 selector 查找到 Method 后,获取 Method 中的 IMP(也就是
4.在继承层级中递归向父类中查找,情况跟上一步类似,也是先查找缓存,缓存没中就查找方法列表。这里跟上一步不同的地方在于缓存策略,有个
5.当传入
6.此时不仅没查找到IMP,动态方法解析也不奏效,只能将
7.读操作解锁,并将之前找到的IMP返回。(无论是正经IMP还是不正经的
对于第5步,其实是直接调用
最后
上一节最后降到如果没找到IMP,就会将
为何根据状态寄存器的值来判断转换成哪个函数指针呢?回过头来看看
再看看返回值为结构体的
稍微懂变成的人一眼就看明白了,不懂的看注释也懂了,我就不墨迹了。现在总算是把消息转发前的逻辑绕回来构成闭环了。
上一节中提到
也就是说
这两个handler 函数的区别从字面上就能看出来,不再赘述。
也就是说,消息转发过程是先将
因为默认的Handler干的事儿就是打日志触发crash,我们想要实现消息转发,就需要替换掉Handler并赋值给
汇编语言还是比较好理解的,红色标出的那三个指令就是把
然而在源码中对应的代码却被删掉啦:
在早起版本的CF源码中,还是可以看到
这个日志场景熟悉的不能再熟悉了,可以看出
在
消息转发的逻辑几乎都卸载
这么一大坨代码就是整个消息转发路径的逻辑,概况如下:
1.先调用
2.调用
3.调用
也就是说我们可以override
介于国内关于这块知识的好多文章描述不够准确和详细,或是对消息转发的原理描述理解不够深刻,或是侧重贴源码而欠思考,所以我做了一个比较全面详细的讲解。
Hmmm, What’s that Selector?
A Look Under the Hood of objc_msgSend()
Printing Objective-C Invocations in LLDB
我想这跟知识体系的构建有关,如果你不认识有砖、水泥等这些建筑的基本组成部分,那么我们应该很难理解建筑是怎么建造出来的吧?
学习新知识,应该也是同样的道理!
资料
今年再看 消息发送机制时,也翻了很多文章,本来想自己总结一遍的,但是感觉这篇 Objective-C 消息发送与转发机制原理 实在写的太好了,就直接转载了。原文:http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/
消息发送和转发流程可以概括为:消息发送(Messaging)是Runtime通过selector 快速查找IMP的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Frowarding)是在查找IMP失败后一系列转发流程的慢速通道,如果不做转发处理,则会打日志和抛出异常。
本文不讲述开发者在消息发送和转发流程中需要做的事,而是讲述原理。能够很好地阅读本文的前提是你对 Objective-C Runtime 已经有一定的了解,关于什么是消息,Class的结构,Selector、IMP、元类等概念将不再赘述。本文用到的源码为objc-680 和 CF-1153.18,逆向CoreFoundation.framework的系统版本为macOS10.11.5,汇编语言架构为x86_64。
八面玲珑的objc_msgSend
此函数是消息发送必经之路,但只要一提到objc_msgSend,都会说它的伪代码如下或类似的逻辑,反正就是获取IMP并调用:
id objc_msgSend(id self, SEL _cmd, ...) { Class class = object_getClass(self); IMP imp = class_getMethodImplementation(class, _cmd); return imp ? imp(self, _cmd, ...) : 0; }
源码解析
为啥老用伪代码?因为objc_msgSend使用汇编语言写的,针对不同架构有不同的实现(我们可以在objc-680的Source目录下看到多个objc-msg-xxxx的汇编实现文件)。如下为
x86_64架构下的源码,可以在 objc-msg-x86_64.s 文件中找到,关键代码如下:
ENTRY _objc_msgSend MESSENGER_START NilTest NORMAL GetIsaFast NORMAL // r11 = self->isa CacheLookup NORMAL // calls IMP on success NilTestSupport NORMAL GetIsaSupport NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r11 MethodTableLookup %a1, %a2 // r11 = IMP cmp %r11, %r11 // set eq (nonstret) for forwarding jmp *%r11 // goto *imp END_ENTRY _objc_msgSend
这里面包含一些有意义的宏:
NilTest宏,判断被发送消息的对象是否为
nil的。如果为
nil,那就直接返回
nil。这就是为啥也可以对
nil发消息。
GetIsaFast宏可以【快速地】获取到对象的
isa指针地址(放到
r11寄存器,
r10会被重写;在arm架构上是直接赋值到
r9)。
CacheLookup这个宏是在类的缓存中查找selector对应的IMP(放到
r10)并执行。如果缓存没中,那就得到Class的方法表中查找了。
MethodTableLookup宏是重点,负责在缓存没命中时在方法表中负责查找IMP:
.macro MethodTableLookup MESSENGER_END_SLOW SaveRegisters // _class_lookupMethodAndLoadCache3(receiver, selector, class) movq $0, %a1 movq $1, %a2 movq %r11, %a3 call __class_lookupMethodAndLoadCache3 // IMP is now in %rax movq %rax, %r11 RestoreRegisters .endmacro
从上面的代码可以看出方法查找IMP的工作交给了OC中的
_class_lookupMethodAndLoadCache3函数,并将IMP返回(从
r11挪到
rax)。最后在
objc_msgSend中调用IMP。
为什么使用汇编语言
其实在objc-msg-x86_64.s中包含了多个版本的objc_msgSend方法,它们是根据返回值的类型和调用者的类型分别处理的:
*
objc_msgSendSuper:向父类发消息,返回值类型为 id
*
objc_msgSend_fpret:返回值类型为 floating-point,其中包含 objc_msgSend_fp2ret 入口处理返回值类型为 long double 的情况
*
objc_msgSend_stret:返回值为结构体
*
objc_msgSendSuper_stret:向父类发消息,返回值类型为结构体
当需要发送消息时,编译器会生成中间代码,根据情况分别调用
objc_msgSend,
objc_msgSend_stret,
objc_msgSendSuper, 或
objc_msgSendSuper_stret其中之一。
这也是为什么
objc_msgSend要用汇编语言而不是 OC、C或C++语言来实现,因为单独的一个方法满足不了多种类型返回值,有的方法返回
id,有的返回
int.考虑到不同类型参数返回值排列组合映射不同方法签名(method signature)的问题,那switch语句得老长了。。。这些原因可以总结为 Calling Convention (调用惯例),也就是说函数调用者与被调用者必须约定好参数与返回值在不同架构处理器上的存取规则,比如参数是以何种顺序存储在栈上,或是存储在哪些寄存器上。除此之外还有其他原因,比如其可变参数用汇编处理起来最方便,因为找到IMP地址后参数都在栈上。要是用C++传递可变参数那就被拒了,prologue机制会弄乱地址(比如i386上为了存储ebp 向后移位bbyte),最后还要用epilogue打扫战场。而且汇编程序执行效率高,在Objective-C Runtime中调用频率较高的函数好多都用汇编编写的。
objc_msgSend_fpret后面fpret 其实是float point return 的缩写;stret 就是struct return的缩写,其他同理。
关于 Calling Convention,可以去看Bang 的文章动态调用C函数的 Calling Convention一节
使用 lookUpImpOrForward 快速查找 IMP
上一节说到的_class_lookupMethodAndLoadCache3函数其实只是简单的调用了
lookUpImpOrForward函数:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
注意
lookUpImpOrForward调用时使用缓存参数传入为
NO,因为之前已经尝试过查找缓存了。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)实现了一套查找IMP的标准路径,也就是在消息转发(Forward)之前的逻辑。
优化缓存查找&类的初始化
先对debug模式下的assert进行unlock:runtimeLock.assertUnlocked();
runtimeLock本质上是对Darwin提供的线程读写锁
pthread_rwlock_t的一层封装,提供了一些便捷的方法。
lookUpImpOrForward接着做了如下两件事:
1.如果使用缓存(
cache参数为
YES),那就调用
cache_getImp方法从缓存查找IMP。
cache_getImp是用汇编语言写的,也可以在 objc-msg-x86_64.s中找到,其依然用了之前说过的
CacheLookup宏。因为
_class_lookupMethodAndLoadCache3调用
lookUpImpOrForward时,
cache参数为
NO,这步直接略过。
2.如果是第一次用到这个类且
initialize参数为
YES(
initialize && !cls->isInitialized()),需要进行初始化工作,也就是开辟一个用于读写数据的空间。先对
runtimeLock写操作加锁,然后调用
cls的
initialize方法。如果
sel == initialize也没关系,虽然
initialize还会被调用一次,但不会起作用啦,因为
cls->isInitialized()已经是 YES 啦。
继续在类的继承体系中查找
考虑到运行时类中的方法可能会增加,需要先做读操作加锁,使得方法查找和缓存填充成原子操作。添加category 会刷新缓存,之后如果旧数据又被重填到缓存中,category 添加操作就会被忽略掉。runtimeLock.read();
之后的逻辑整理如下:
1.如果 selector 是需要被忽略的垃圾回收用到的方法,则将 IMP 结果设为
_objc_ignored_method,这是个汇编程序入口,可以理解为一个标记。对此种情况进行缓存填充操作后,跳到第 7 步;否则执行下一步。
2.查找当前类中的缓存,跟之前一样,使用
cache_getImp汇编程序入口。如果命中缓存获取到了 IMP,则直接跳到第 7 步;否则执行下一步。
3.在当前类中的方法列表(method list)中进行查找,也就是根据 selector 查找到 Method 后,获取 Method 中的 IMP(也就是
method_imp属性),并填充到缓存中。查找过程比较复杂,会针对已经排序的列表使用二分法查找,未排序的列表则是线性遍历。如果成功查找到 Method 对象,就直接跳到第 7 步;否则执行下一步。
4.在继承层级中递归向父类中查找,情况跟上一步类似,也是先查找缓存,缓存没中就查找方法列表。这里跟上一步不同的地方在于缓存策略,有个
_objc_msgForward_impcache汇编程序入口作为缓存中消息转发的标记。也就是说如果在缓存中找到了 IMP,但如果发现其内容是
_objc_msgForward_impcache,那就终止在类的继承层级中递归查找,进入下一步;否则跳到第 7 步。
5.当传入
lookUpImpOrForward的参数
resolver为
YES并且是第一次进入第5步时,进入动态方法解析;否则进入下一步。这步是消息转发前的最后一次机会。此时释放读入锁(runtimeLock.unlockRead()),接着间接地发送
+resolveInstanceMethod或
+resolveClassMethod消息。这相当于告诉程序员『赶紧用 Runtime 给类里这个 selector 弄个对应的 IMP 吧』,因为此时锁已经unlock了所以不会缓存结果,甚至还需要软性地处理缓存过期问题可能带来的错误。这里的业务逻辑稍微复杂些,后面会总结。因为这些工作都是在非线程安全下进行的,完成后需要回到第1步再次查找IMP.
6.此时不仅没查找到IMP,动态方法解析也不奏效,只能将
_objc_msgForward_impcache当做IMP并写入缓存。这也就是之前第4步中为何查找到
_objc_msgForward_impcache就表明了要进入消息转发了。
7.读操作解锁,并将之前找到的IMP返回。(无论是正经IMP还是不正经的
_objc_msgForward_impcache)这步还偏执地做了一些脑洞略大的assert,很有趣。
对于第5步,其实是直接调用
_class_resolveMethod函数,在这个函数中实现了复杂的方法解析逻辑。如果cls是元类则会发送
+resolveClassMethod,然后根据
lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)函数的结果来判断是否发送
+resolveInstanceMethod;如果不是元类,则只需要发送
+resolveInstanceMethod消息。这里调用
+resolveInstanceMethod或
+resolveClassMethod时,再次用到了
objc_msgSend,而且第三个参数正是传入
lookUpImpOrForward的那个
sel。在发送方法即系消息之后还会调用
lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)来判断是否已经添加上
sel对应的IMP了,打印出结果。
最后
lookUpImpOrForward方法也会把真正的IMP或者需要消息转发的
_objc_msgForward_impcache返回,并最终传递到
objc_msgSend中。而
_objc_msgForward_impcache会在转化成
_objc_msgForward或
_objc_msgForward_stret,这个后面会讲解原理。
回顾objc_msgSend伪代码
回过头来会发现objc_msgSend的伪代码描述的很传神,因为
class_getMethodImplementation的实现如下:
IMP class_getMethodImplementation(Class cls, SEL sel) { IMP imp; if (!cls || !sel) return nil; imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/); // Translate forwarding function to C-callable external version if (!imp) { return _objc_msgForward; } return imp; }
lookUpImpOrNil函数获取不到 IMP 时就返回
_objc_msgForward,后面会讲到它。
lookUpImpOrNil跟
lookUpImpOrForward的功能很相似,只是将
lookUpImpOrForward实现中的
_objc_msgForward_impcache替换成了
nil:
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); if (imp == _objc_msgForward_impcache) return nil; else return imp; }
lookUpImpOrNil方法可以查找到selector对应的IMP或是nil,如果不考虑返回值类型为结构体的情况,用那几行伪代码来表示复杂的汇编实现还是挺恰当的。
forwarding 中路漫漫的消息转发
objc_msgForward_impcache 的转换
_objc_msgForward_impcache只是个内部的函数指针,只存储于上节提到的类的方法缓存中,需要被转化为
_objc_msgForward和
_objc_msgForward_stret才能被外部调用。但在 macOS 10.6及更早版本的libobjc.A.dylib中是不能直接调用的。况且我们根本不会直接用到它。带
stret后缀的函数依旧是返回值为结构体的版本。
上一节最后降到如果没找到IMP,就会将
_objc_msgForward_impcache返回到
objc_msgSend函数,而正是因为它是用汇编语言写的,所以将内部使用的
_objc_msgForward_impcache转化成外部可调用的
_objc_msgForward或
_objc_msgForward_stret也是由汇编代码来完成。实现原理很简单,就是增加个静态入口
__objc_msgForward_impcache,然后根据此时CPU的状态寄存器的内容来决定转换成哪个。如果是
NE(not Equal)则转换成
_objc_msgForward_stret,反之是EQ(Equal)则转换成
_objc_msgForward:
jne __objc_msgForward_stret jmp __objc_msgForward
为何根据状态寄存器的值来判断转换成哪个函数指针呢?回过头来看看
objc_msgSend中调用完
MethodTableLookup之后干了什么:
MethodTableLookup %a1, %a2 // r11 = IMP cmp %r11, %r11 // set eq (nonstret) for forwarding jmp *%r11 // goto *imp
再看看返回值为结构体的
objc_msgSend_stret这里的逻辑:
MethodTableLookup %a2, %a3 // r11 = IMP test %r11, %r11 // set ne (stret) for forward; r11!=0 jmp *%r11 // goto *imp
稍微懂变成的人一眼就看明白了,不懂的看注释也懂了,我就不墨迹了。现在总算是把消息转发前的逻辑绕回来构成闭环了。
上一节中提到
class_getMethodImplementation函数的实现,在查找不到IMP时返回
_objc_msgForward,而
_objc_msgForward_stret正好对应着
class_getMethodImplementation_stret:
IMP class_getMethodImplementation_stret(Class cls, SEL sel) { IMP imp = class_getMethodImplementation(cls, sel); // Translate forwarding function to struct-returning version if (imp == (IMP)&_objc_msgForward /* not _internal! */) { return (IMP)&_objc_msgForward_stret; } return imp; }
也就是说
_objc_msgForward*系列本质都是函数指针,都用汇编语言实现,都可以与IMP类型的值作比较。
_objc_msgForward和
_objc_msgForward_stret声明在
message.h文件中。
_objc_msgForward_impcache在早起版本的Runtime中叫做
_objc_msgForward_internal。
objc_msgForward 也只是个入口
从汇编编码可以很容易看出_objc_msgForward和
_objc_msgForward_stret会分别调用
_objc_forward_handler和
_objc_forward_handler_stret:
ENTRY __objc_msgForward // Non-stret version movq __objc_forward_handler(%rip), %r11 jmp *%r11 END_ENTRY __objc_msgForward ENTRY __objc_msgForward_stret // Struct-return version movq __objc_forward_stret_handler(%rip), %r11 jmp *%r11 END_ENTRY __objc_msgForward_stret
这两个handler 函数的区别从字面上就能看出来,不再赘述。
也就是说,消息转发过程是先将
_objc_msgForward_impcache强转成
_objc_msgForward或
_objc_msgForward_stret,再分别调用
_objc_forward_handler或
_objc_forward_handler_stret。
objc_setForwardHandler 设置了消息转发的回调
在Objective-C 2.0之前,默认的_objc_forward_handler或
_objc_forward_handler_stret都是
nil,而新版本的默认实现是这样的:
// 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; #if SUPPORT_STRET struct stret { int i[100]; }; __attribute__((noreturn)) struct stret objc_defaultForwardStretHandler(id self, SEL sel) { objc_defaultForwardHandler(self, sel); } void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler; #endif
objc_defaultForwardHandler中的
_objc_fatal作用就是打日志并调用
__builtin_trap()触发crash,可以看到我们最熟悉的那句
unrecognized selector sent to instance日志。
__builtin_trap()在杀掉进程的同事还能生成日志,比调用
exit()更好。
objc_defaultForwardStretHandler就是装模作样搞个形式主义,把
objc_defaultForwardHandler包了一层。
__attribute__((noreturn))属性通知编译器函数从不返回值,当遇到类型函数需要返回值而却不可能运行到返回值处就已经退出来的情况,该属性可以避免出现错误信息。这里正适合此属性,因为要求返回结构体。
因为默认的Handler干的事儿就是打日志触发crash,我们想要实现消息转发,就需要替换掉Handler并赋值给
_objc_forward_handler或
_objc_forward_handler_stret,赋值的过程就需要用到
objc_setForwardHandler函数,实现也是简单粗暴,就是赋值啊:
void objc_setForwardHandler(void *fwd, void *fwd_stret) { _objc_forward_handler = fwd; #if SUPPORT_STRET _objc_forward_stret_handler = fwd_stret; #endif }
逆向工程助力刨根问底
重头戏在于对objc_setForwardHandler的调用,以及之后的消息转发调用栈。这回不是在 Objective-C Runtime (libobjc.dylib)中啦,而是在Core Foundation(CoreFoundation.framework)中。虽然CF是开源的,但有意思的是苹果故意在开源的代码中删除了在
CFRuntime.c文件
__CFInitialize()中调用
objc_setForwardHandler的代码。
__CFInitialize()函数是在CF Runtime连接到进程时初始化调用的。从反编译得到的汇编代码中可以很容易跟 C 源码对比出来,我用红色标出了同一段代码的差异。
汇编语言还是比较好理解的,红色标出的那三个指令就是把
__CF_forwarding_prep_0和
___forwarding_prep_1___作为参数调用
objc_setForwardHandler方法(那么值钱那两个DefaultHandler 卵用都没有咯,反正不出意外会被 CF 替换掉):
然而在源码中对应的代码却被删掉啦:
在早起版本的CF源码中,还是可以看到
__CF_forwarding_prep_0和
___forwarding_prep_1___的声明的,但是不会有实现源码,也没有对
objc_setForwardHandler的调用。这些细节从函数调用栈中无法看出,只能逆向工程看汇编指令。但从函数调用栈可以看出
__CF_forwarding_prep_0和
___forwarding_prep_1___这两个Forward Handler做了啥:
2016-06-14 12:50:15.385 MessageForward[67364:7174239] -[MFObject sendMessage]: unrecognized selector sent to instance 0x1006001a0 2016-06-14 12:50:15.387 MessageForward[67364:7174239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MFObject sendMessage]: unrecognized selector sent to instance 0x1006001a0' *** First throw call stack: ( 0 CoreFoundation 0x00007fff8fa554f2 __exceptionPreprocess + 178 1 libobjc.A.dylib 0x00007fff98396f7e objc_exception_throw + 48 2 CoreFoundation 0x00007fff8fabf1ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205 3 CoreFoundation 0x00007fff8f9c5571 ___forwarding___ + 1009 4 CoreFoundation 0x00007fff8f9c50f8 _CF_forwarding_prep_0 + 120 5 MessageForward 0x0000000100000f1f main + 79 6 libdyld.dylib 0x00007fff8bc2c5ad start + 1 7 ??? 0x0000000000000001 0x0 + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
这个日志场景熟悉的不能再熟悉了,可以看出
_CF_forwarding_prep_0函数调用了
___forwarding___函数,接着又调用了
doesNotRecognizeSelector方法,最后抛出异常。但是靠这些是无法说服看客的,还得靠逆向工程反编译后再反汇编成伪代码来一探究竟,刨根问底。
__CF_forwarding_prep_0和
___forwarding_prep_1___函数都调用了
___forwarding___,只是传入参数不同。
___forwarding___有两个参数,第一个参数为将要被转发消息的栈指针(可以简单理解为IMP),第二个参数标记是否返回结构体。
__CF_forwarding_prep_0第二个参数传入
0,
___forwarding_prep_1___传入的是 1,从函数名都能看得出来。下面是这两个函数的伪代码:
int __CF_forwarding_prep_0(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { rax = ____forwarding___(rsp, 0x0); if (rax != 0x0) { // 转发结果不为空,将内容返回 rax = *rax; } else { // 转发结果为空,调用 objc_msgSend(id self, SEL _cmd,...); rsi = *(rsp + 0x8); rdi = *rsp; rax = objc_msgSend(rdi, rsi); } return rax; } int ___forwarding_prep_1___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) { rax = ____forwarding___(rsp, 0x1); if (rax != 0x0) {// 转发结果不为空,将内容返回 rax = *rax; } else {// 转发结果为空,调用 objc_msgSend_stret(void * st_addr, id self, SEL _cmd, ...); rdx = *(rsp + 0x10); rsi = *(rsp + 0x8); rdi = *rsp; rax = objc_msgSend_stret(rdi, rsi, rdx); } return rax; }
在
x86_64架构中,
rax寄存器一般是作为返回值,
rsp寄存器是栈指针。在调用
objc_msgSend函数时,参数
arg0(self), arg1(_cmd), arg2, arg3, arg4, arg5分别使用寄存器
rdi, rsi, rdx, rcx, r8, r9的值。在调用
objc_msgSend_stret时第一个参数为
st_addr,其余参数依次后移。为了能够打包出
NSInvocation实例并传入后续的
forwardInvocation:方法,在调用
___forwarding___函数之前会先将所有参数压入栈中。因为寄存器
rsp为栈指针指向栈顶,所以
rsp的内容就是
self啦,因为
x86_64是小端,栈增长方向是由高地址到低地址,所以从栈顶往下移动一个指针需要加
0x8(64bit)。而将参数入栈的顺序是从后往前的,也就是说
arg0是最后一个入栈的,位于栈顶:
__CF_forwarding_prep_0: 0000000000085080 push rbp ; XREF=___CFInitialize+138 0000000000085081 mov rbp, rsp 0000000000085084 sub rsp, 0xd0 000000000008508b mov qword [ss:rsp+0xb0], rax 0000000000085093 movq qword [ss:rsp+0xa0], xmm7 000000000008509c movq qword [ss:rsp+0x90], xmm6 00000000000850a5 movq qword [ss:rsp+0x80], xmm5 00000000000850ae movq qword [ss:rsp+0x70], xmm4 00000000000850b4 movq qword [ss:rsp+0x60], xmm3 00000000000850ba movq qword [ss:rsp+0x50], xmm2 00000000000850c0 movq qword [ss:rsp+0x40], xmm1 00000000000850c6 movq qword [ss:rsp+0x30], xmm0 00000000000850cc mov qword [ss:rsp+0x28], r9 00000000000850d1 mov qword [ss:rsp+0x20], r8 00000000000850d6 mov qword [ss:rsp+0x18], rcx 00000000000850db mov qword [ss:rsp+0x10], rdx 00000000000850e0 mov qword [ss:rsp+0x8], rsi 00000000000850e5 mov qword [ss:rsp], rdi 00000000000850e9 mov rdi, rsp ; argument #1 for method ____forwarding___ 00000000000850ec mov rsi, 0x0 ; argument #2 for method ____forwarding___ 00000000000850f3 call ____forwarding___
消息转发的逻辑几乎都卸载
___forwarding___函数中了,实现比较复杂,反编译出的伪代码也不是很直观。我对
arigrant.com的结果完善如下:
int __forwarding__(void *frameStackPointer, int isStret) { id receiver = *(id *)frameStackPointer; SEL sel = *(SEL *)(frameStackPointer + 8); const char *selName = sel_getName(sel); Class receiverClass = object_getClass(receiver); // 调用 forwardingTargetForSelector: if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { id forwardingTarget = [receiver forwardingTargetForSelector:sel]; if (forwardingTarget && forwarding != receiver) { if (isStret == 1) { int ret; objc_msgSend_stret(&ret,forwardingTarget, sel, ...); return ret; } return objc_msgSend(forwardingTarget, sel, ...); } } // 僵尸对象 const char *className = class_getName(receiverClass); const char *zombiePrefix = "_NSZombie_"; size_t prefixLen = strlen(zombiePrefix); // 0xa if (strncmp(className, zombiePrefix, prefixLen) == 0) { CFLog(kCFLogLevelError, @"*** -[%s %s]: message sent to deallocated instance %p", className + prefixLen, selName, receiver); <breakpoint-interrupt> } // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; if (methodSignature) { BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct; if (signatureIsStret != isStret) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", selName, signatureIsStret ? "" : not, isStret ? "" : not); } if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; [receiver forwardInvocation:invocation]; void *returnValue = NULL; [invocation getReturnValue:&value]; return returnValue; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message", receiver, className); return 0; } } } SEL *registeredSel = sel_getUid(selName); // selector 是否已经在 Runtime 注册过 if (sel != registeredSel) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", sel, selName, registeredSel); } // doesNotRecognizeSelector else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { [receiver doesNotRecognizeSelector:sel]; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort", receiver, className); } // The point of no return. kill(getpid(), 9); }
这么一大坨代码就是整个消息转发路径的逻辑,概况如下:
1.先调用
forwardingTargetForSelector方法获取新的target作为receiver重新执行selector,如果返回的内容不合法(为 nil 或旧receiver 一样),那就进入第一步。
2.调用
methodSignatureForSelector获取方法签名后,判断返回类型信息是否正确,再调用
forwardInvocation执行
NSInvocation对象,并将结果返回。如果对象没实现
methodSignatureForSelector方法,进入第三步。
3.调用
doesNotRecognizeSelector方法。
doesNotRecognizeSelector之前其实还有个判断selector 在Runtime 中是否注册过的逻辑,但在我们正常发消息的时候,不会出现此问题。但如果手动创建一个
NSInvocation对象并调用
invoke,并将第二个参数设置成一个不存在的selector,那就会导致这个问题,并输入日志”does not match selector known to Objective C runtime”。较真的读者可能会有疑问:为何这段逻辑判断用不到却还存在着?难道除了
__CF_forwarding_prep_0和
___forwarding_prep_1___函数还有其他函数调用
___forwarding___么?莫非消息转发还有其他路径?其实并不是!原因是
___forwarding___调用了
___forwarding___函数,以下方法也会调用
___invoking___函数:
-[NSInvocation invoke] -[NSInvocation invokeUsingIMP:] -[NSInvocation invokeSuper]
doesNotRecognizeSelector方法其实在libobj.A.dylib 中已经废弃了,而是在CF框架中实现,而且也不是开源的。从函数调用栈可以发现
doesNotRecognizeSelector之后会抛出异常,而Runtime 中废弃的实现只是打印日志后直接杀掉进程(
__builtin_trap())。下面是CF中实现的伪代码:
void -[NSObject doesNotRecognizeSelector:](void * self, void * _cmd, void * arg2) { r14 = ___CFFullMethodName([self class], self, arg2); _CFLog(0x3, @"%@: unrecognized selector sent to instance %p", r14, self, r8, r9, stack[2048]); rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault, 0x0, @"%@: unrecognized selector sent to instance %p")); if (*(int8_t *)___CFOASafe != 0x0) { ___CFRecordAllocationEvent(); } rax = _objc_rootAutorelease(rbx); rax = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0]; objc_exception_throw(rax); return; } void +[NSObject doesNotRecognizeSelector:](void * self, void * _cmd, void * arg2) { r14 = ___CFFullMethodName([self class], self, arg2); _CFLog(0x3, @"%@: unrecognized selector sent to class %p", r14, self, r8, r9, stack[2048]); rbx = _CFMakeCollectable(_CFStringCreateWithFormat(___kCFAllocatorSystemDefault, 0x0, @"%@: unrecognized selector sent to class %p")); if (*(int8_t *)___CFOASafe != 0x0) { ___CFRecordAllocationEvent(); } rax = _objc_rootAutorelease(rbx); rax = [NSException exceptionWithName:@"NSInvalidArgumentException" reason:rax userInfo:0x0]; objc_exception_throw(rax); return; }
也就是说我们可以override
doesNotRecognizeSelector或者捕获其爆出的异常。在这里还是大有文章可做的。
总结
我将整个流程绘制出来,过滤了一些不会进入的分支路径和跟主题无关的细节:介于国内关于这块知识的好多文章描述不够准确和详细,或是对消息转发的原理描述理解不够深刻,或是侧重贴源码而欠思考,所以我做了一个比较全面详细的讲解。
参考文献
Why objc_msgSend Must be Written in AssemblyHmmm, What’s that Selector?
A Look Under the Hood of objc_msgSend()
Printing Objective-C Invocations in LLDB
相关文章推荐
- Objective-C的内省(Introspection)用法小结
- C/C++在Java、Android和Objective-C三大平台下实现混合编程
- Objective-C中常用的结构体NSRange,NSPoint,NSSize(CGSize),NSRect实例分析
- Objective-C中使用NSString类操作字符串的方法小结
- Objective-C 宏定义详细介绍
- Objective-C中NSNumber与NSDictionary的用法简介
- Objective-C实现冒泡排序算法的简单示例
- Objective-C中NSLog输出格式大全
- Objective-C实现自定义的半透明导航
- 浅析Objective-C的程序结构及面向对象的编程方式
- Objective-C的入门学习笔记
- Objective-C实现无限循环轮播器
- 全面解析Objective-C中的block代码块的使用
- Swift调用Objective-C编写的API实例
- Swift、Objective-C、Cocoa混合编程设置指南
- Objective-c代码如何移植为Swift代码 Objective-c代码转移到Swift过程介绍
- Swift调用Objective-C代码
- Objective-C 代码与Javascript 代码相互调用实例
- Objective-C中字符串NSString的常用操作方法总结
- Swift和Objective-C 混编注意事项