Objective-C 消息发送与转发机制原理

消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

本文不讲述开发者在消息发送和转发流程中需要做的事,而是讲述原理。能够很好地阅读本文的前提是你对Objective-C Runtime已经有一定的了解,关于什么是消息,Class 的结构,selector、IMP、元类等概念将不再赘述。本文用到的源码为 objc4-680 和 CF-1153.18,逆向 CoreFoundation.framework 的系统版本为 macOS 10.11.5,汇编语言架构为 x86_64。

八面玲珑的 objc_msgSend

此函数是消息发送必经之路,但只要一提objc_msgSend,都会说它的伪代码如下或类似的逻辑,反正就是获取 IMP 并调用:

id objc_msgSend(id self, SEL _cmd, ...) {

Classclass=object_getClass(self);

IMP imp = class_getMethodImplementation(class,_cmd);

returnimp ? 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:

.macroMethodTableLookup

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。除此之外还有其他原因,比如其可变参数用汇编处理起来最方便,因为找到 IMP 地址后参数都在栈上。要是用 C++ 传递可变参数那就悲剧了,prologue 机制会弄乱地址(比如 i386 上为了存储ebp向后移位 4byte),最后还要用 epilogue 打扫战场。此外还好考虑不同类型参数排列组合映射不同方法签名(method signature)的问题,那 switch 语句得老长了。。。而且汇编程序执行效率高,在 Objective-C Runtime 中调用频率较高的函数好多都用汇编写的。

使用 lookUpImpOrForward 快速查找 IMP

上一节中说到的_class_lookupMethodAndLoadCache3函数其实只是简单的调用了lookUpImpOrForward函数:

IMP _class_lookupMethodAndLoadCache3(idobj, SEL sel, Class cls)

{

returnlookUpImpOrForward(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接着做了如下两件事:

如果使用缓存(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 添加操作就会被忽略掉。

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 伪代码

回过头来会发现objc_msgSend的伪代码描述得很传神啊,因为class_getMethodImplementation的实现如下:

IMP class_getMethodImplementation(Class cls, SEL sel)

{

IMP imp;

if(!cls  ||  !sel)returnnil;

imp = lookUpImpOrNil(cls, sel,nil,YES/*initialize*/,YES/*cache*/,YES/*resolver*/);

// Translate forwarding function to C-callable external version

if(!imp) {

return_objc_msgForward;

}

returnimp;

}

lookUpImpOrNil函数获取不到 IMP 时就返回_objc_msgForward,后面会讲到它。lookUpImpOrNil跟lookUpImpOrForward的功能很相似,只是将lookUpImpOrForward实现中的_objc_msgForward_impcache替换成了nil:

IMP lookUpImpOrNil(Class cls, SEL sel, idinst,

boolinitialize,boolcache,boolresolver)

{

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才能被外部调用。但在Mac OS XmacOS 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)forforwarding

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:

IMPclass_getMethodImplementation_stret(Class cls,SELsel)

{

IMP imp = class_getMethodImplementation(cls,sel);

// Translate forwardingfunctiontostruct-returningversion

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-stretversion

movq__objc_forward_handler(%rip), %r11

jmp*%r11

END_ENTRY__objc_msgForward

ENTRY__objc_msgForward_stret

//Struct-returnversion

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(idself, 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

structstret {inti[100]; };

__attribute__((noreturn))structstret

objc_defaultForwardStretHandler(idself, 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函数,实现也是简单粗暴,就是赋值啊:

voidobjc_setForwardHandler(void*fwd,void*fwd_stret)

{

_objc_forward_handler = fwd;

#ifSUPPORT_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() 函数源码

在早期版本的 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 toinstance0x1006001a0

2016-06-14 12:50:15.387 MessageForward[67364:7174239] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MFObject sendMessage]: unrecognized selector sent toinstance0x1006001a0'

*** Firstthrowcall 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(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {

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);

}

returnrax;

}

int___forwarding_prep_1___(intarg0,intarg1,intarg2,intarg3,intarg4,intarg5) {

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);

}

returnrax;

}

在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:

0000000000085080pushrbp; XREF=___CFInitialize+138

0000000000085081movrbp,rsp

0000000000085084subrsp,0xd0

000000000008508bmovqword[ss:rsp+0xb0],rax

0000000000085093movqqword[ss:rsp+0xa0],xmm7

000000000008509cmovqqword[ss:rsp+0x90],xmm6

00000000000850a5movqqword[ss:rsp+0x80],xmm5

00000000000850aemovqqword[ss:rsp+0x70],xmm4

00000000000850b4movqqword[ss:rsp+0x60],xmm3

00000000000850bamovqqword[ss:rsp+0x50],xmm2

00000000000850c0movqqword[ss:rsp+0x40],xmm1

00000000000850c6movqqword[ss:rsp+0x30],xmm0

00000000000850ccmovqword[ss:rsp+0x28],r9

00000000000850d1movqword[ss:rsp+0x20],r8

00000000000850d6movqword[ss:rsp+0x18],rcx

00000000000850dbmovqword[ss:rsp+0x10],rdx

00000000000850e0movqword[ss:rsp+0x8],rsi

00000000000850e5movqword[ss:rsp],rdi

00000000000850e9movrdi,rsp; argument #1 for method ____forwarding___

00000000000850ecmovrsi,0x0; argument #2 for method ____forwarding___

00000000000850f3call____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);

}

// 调用 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]

-[NSInvocationinvokeUsingIMP:]

-[NSInvocation invokeSuper]

doesNotRecognizeSelector方法其实在 libobj.A.dylib 中已经废弃了,而是在 CF 框架中实现,而且也不是开源的。从函数调用栈可以发现doesNotRecognizeSelector之后会抛出异常,而 Runtime 中废弃的实现知识打日志后直接杀掉进程(__builtin_trap())。下面是 CF 中实现的伪代码:

void-[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {

r14 = ___CFFullMethodName([selfclass],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 = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];

objc_exception_throw(rax);

return;

}

void+[NSObjectdoesNotRecognizeSelector:](void*self,void* _cmd,void* arg2) {

r14 = ___CFFullMethodName([selfclass],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 = [NSExceptionexceptionWithName:@"NSInvalidArgumentException"reason:rax userInfo:0x0];

objc_exception_throw(rax);

return;

}

也就是说我们可以 overridedoesNotRecognizeSelector或者捕获其抛出的异常。在这里还是大有文章可做的。

总结

我将整个实现流程绘制出来,过滤了一些不会进入的分支路径和跟主题无关的细节:

消息发送与转发路径流程图

介于国内关于这块知识的好多文章描述不够准确和详细,或是对消息转发的原理描述理解不够深刻,或是侧重贴源码而欠思考,所以我做了一个比较全面详细的讲解。

参考文献

Why objc_msgSend Must be Written in Assembly

Hmmm, What’s that Selector?

A Look Under the Hood of objc_msgSend()

Printing Objective-C Invocations in LLDB

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

推荐阅读更多精彩内容