消息发送和转发流程能够归纳为:消息发送(Messaging)是 Runtime 经过 selector 快速查找 IMP 的过程,有了函数指针就能够执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,若是不做转发处理,则会打日志和抛出异常。 ###八面玲珑的 objc_msgSend 此函数是消息发送必经之路,但只要一提 objc_msgSend,都会说它的伪代码以下或相似的逻辑,反正就是获取 IMP 并调用:c++
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 是用汇编语言写的,针对不一样架构有不一样的实现。以下为 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。bash
其实在 objc-msg-x86_64.s 中包含了多个版本的 objc_msgSend 方法,它们是根据返回值的类型和调用者的类型分别处理的:架构
上一节中说到的 _class_lookupMethodAndLoadCache3 函数其实只是简单的调用了 lookUpImpOrForward 函数:app
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:ide
1 runtimeLock.assertUnlocked();
复制代码
runtimeLock 本质上是对 Darwin 提供的线程读写锁 pthread_rwlock_t 的一层封装,提供了一些便捷的方法。 lookUpImpOrForward 接着作了以下两件事: 若是使用缓存(cache 参数为 YES),那就调用 cache_getImp 方法从缓存查找 IMP。cache_getImp 是用汇编语言写的,也能够在 objc-msg-x86_64.s 找到,其依然用了以前说过的 CacheLookup 宏。由于 _class_lookupMethodAndLoadCache3 调用 lookUpImpOrForward 时 cache 参数为 NO,这步直接略过。 若是是第一次用到这个类且 initialize 参数为 YES(initialize && !cls->isInitialized()),须要进行初始化工做,也就是开辟一个用于读写数据的空间。先对 runtimeLock 写操做加锁,而后调用 cls 的 initialize 方法。若是 sel == initialize 也不要紧,虽然 initialize 还会被调用一次,但不会起做用啦,由于 cls->isInitialized() 已是 YES 啦。 继续在类的继承体系中查找 考虑到运行时类中的方法可能会增长,须要先作读操做加锁,使得方法查找和缓存填充成为原子操做。添加 category 会刷新缓存,以后若是旧数据又被重填到缓存中,category 添加操做就会被忽略掉。
1 runtimeLock.read();
复制代码
以后的逻辑整理以下: 若是 selector 是须要被忽略的垃圾回收用到的方法,则将 IMP 结果设为 _objc_ignored_method,这是个汇编程序入口,能够理解为一个标记。对此种状况进行缓存填充操做后,跳到第 7 步;不然执行下一步。 查找当前类中的缓存,跟以前同样,使用 cache_getImp 汇编程序入口。若是命中缓存获取到了 IMP,则直接跳到第 7 步;不然执行下一步。 在当前类中的方法列表(method list)中进行查找,也就是根据 selector 查找到 Method 后,获取 Method 中的 IMP(也就是 method_imp 属性),并填充到缓存中。查找过程比较复杂,会针对已经排序的列表使用二分法查找,未排序的列表则是线性遍历。若是成功查找到 Method 对象,就直接跳到第 7 步;不然执行下一步。 在继承层级中递归向父类中查找,状况跟上一步相似,也是先查找缓存,缓存没中就查找方法列表。这里跟上一步不一样的地方在于缓存策略,有个 _objc_msgForward_impcache 汇编程序入口做为缓存中消息转发的标记。也就是说若是在缓存中找到了 IMP,但若是发现其内容是 _objc_msgForward_impcache,那就终止在类的继承层级中递归查找,进入下一步;不然跳到第 7 步。 当传入 lookUpImpOrForward 的参数 resolver 为 YES 而且是第一次进入第 5 步时,时进入动态方法解析;不然进入下一步。这步消息转发前的最后一次机会。此时释放读入锁(runtimeLock.unlockRead()),接着间接地发送 +resolveInstanceMethod 或 +resolveClassMethod 消息。这至关于告诉程序员『赶忙用 Runtime 给类里这个 selector 弄个对应的 IMP 吧』,由于此时锁已经 unlock 了因此不会缓存结果,甚至还须要软性地处理缓存过时问题可能带来的错误。这里的业务逻辑稍微复杂些,后面会总结。由于这些工做都是在非线程安全下进行的,完成后须要回到第 1 步再次查找 IMP。 此时不只没查找到 IMP,动态方法解析也不奏效,只能将 _objc_msgForward_impcache 当作 IMP 并写入缓存。这也就是以前第 4 步中为什么查找到 _objc_msgForward_impcache 就代表了要进入消息转发了。 读操做解锁,并将以前找到的 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 的伪代码描述得很传神啊,由于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,因此若是不考虑返回值类型为结构体的状况,用那几行伪代码来表示复杂的汇编实现仍是挺恰当的。
_objc_msgForward_impcache 只是个内部的函数指针,只存储于上节提到的类的方法缓存中,须要被转化为 _objc_msgForward 和 _objc_msgForward_stret 才能被外部调用。但在 Mac OS X 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。
在 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 替换掉): 反编译后的 __CFInitialize() 汇编代码 反编译后的 __CFInitialize() 汇编代码 然而在源码中对应的代码却被删掉啦: 苹果提供的 __CFInitialize() 函数源码 苹果提供的 __CFInitialize() 函数源码 在早期版本的 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);
}
复制代码
这么一大坨代码就是整个消息转发路径的逻辑,归纳以下: 先调用 forwardingTargetForSelector 方法获取新的 target 做为 receiver 从新执行 selector,若是返回的内容不合法(为 nil 或者跟旧 receiver 同样),那就进入第二步。 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardInvocation 执行 NSInvocation 对象,并将结果返回。若是对象没实现 methodSignatureForSelector 方法,进入第三步。 调用 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 调用了 invoking 函数,因此上面的伪代码直接把 invoking 函数的逻辑也『翻译』过来了。除了 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 或者捕获其抛出的异常。在这里仍是大有文章可作的。
我将整个实现流程绘制出来,过滤了一些不会进入的分支路径和跟主题无关的 yulingtianxia.com/resources/M…