美文网首页
Runtime 探析(2) 消息发送与转发

Runtime 探析(2) 消息发送与转发

作者: 浮萍向北 | 来源:发表于2018-12-29 21:18 被阅读0次

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


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

源码解析

因为 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。

使用 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)之前的逻辑。

lookUpImpOrForward 接着做了如下两件事:

如果使用缓存(cache 参数为 YES),那就调用 cache_getImp 方法从缓存查找 IMPcache_getImp 是用汇编语言写的,也可以在 objc-msg-x86_64.s 找到,其依然用了之前说过的 CacheLookup 宏。因为 _class_lookupMethodAndLoadCache3 调用 lookUpImpOrForwardcache 参数为 NO,这步直接略过。
如果是第一次用到这个类且 initialize 参数为 YES(initialize && !cls->isInitialized()),需要进行初始化工作,也就是开辟一个用于读写数据的空间。先对 runtimeLock 写操作加锁,然后调用 clsinitialize 方法。如果 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
对于第 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

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返回nil,就代表在父类中的缓存中找到,于是需要再调用一次_class_resolveInstanceMethod方法。保证给sel添加上了对应的IMP
回到lookUpImpOrForward方法中,如果也没有找到IMP的实现,那么method resolver也没用了,只能进入消息转发阶段。进入这个阶段之前,imp变成_objc_msgForward_impcache。最后再加入缓存中。

消息转发Message Forwarding

     STATIC_ENTRY __objc_msgForward_impcache
 // Method cache version

 // THIS IS NOT A CALLABLE C FUNCTION
 // Out-of-band condition register is NE for stret, EQ otherwise.

 MESSENGER_START
 nop
 MESSENGER_END_SLOW
 
 jne __objc_msgForward_stret
 jmp __objc_msgForward

 END_ENTRY __objc_msgForward_impcache //内部的函数指针,只存储于上节提到的类的方法缓存中
 
 
 ENTRY __objc_msgForward
 // Non-stret version

 movq __objc_forward_handler(%rip), %r11
 jmp *%r11

 END_ENTRY __objc_msgForward

在执行_objc_msgForward之后会调用__objc_forward_handler函数。

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

Objc2.0中会有一个objc_defaultForwardHandler,看源码实现我们可以看到熟悉的语句。当我们给一个对象发送一个没有实现的方法的时候,如果其父类也没有这个方法,则会崩溃,报错信息类似于这样:unrecognized selector sent to instance,然后接着会跳出一些堆栈信息。这些信息就是从这里而来。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(Method:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

当前的SEL无法找到相应的IMP的时候,开发者可以通过重写- (id)forwardingTargetForSelector:(SEL)aSelector方法来“偷梁换柱”,把消息的接受者换成一个可以处理该消息的对象。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

这一步是替消息找备援接收者,如果这一步返回的是nil,那么补救措施就完全的失效了,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。

接下来未识别的方法崩溃之前,系统会做一次完整的消息转发。

实现此方法之后,若发现某调用不应由本类处理,则会调用超类的同名方法。如此,继承体系中的每个类都有机会处理该方法调用的请求,一直到NSObject根类。如果到NSObject也不能处理该条消息,那么就是再无挽救措施了,只能抛出“doesNotRecognizeSelector”异常了。

forwardInvocationDemo

相关文章

网友评论

      本文标题:Runtime 探析(2) 消息发送与转发

      本文链接:https://www.haomeiwen.com/subject/vjtmlqtx.html