Runtime 消息发送和转发

一.objc_msgSend函数简介

以前去面试,有人问了这个一个问题

[receiver message] 

发生了什么?
一听这个问题,一脸懵逼。这不就是简单的调用函数么?其实吧。考官问的就是消息发送。

[receiver message]

会被编译器转化为:

id objc_msgSend ( id self, SEL op, ... );

如何证明呢?
我们将

clang -rewrite-objc xxx.m 

文件命令
重写

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[NSString alloc]init];
    }
    return 0;
}

获取到的结果就是

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return 0;
}

具体看源代码地址中工程RunTimeMessageTest

这里我们看

id objc_msgSend ( id self, SEL op, ... );

这个函数接收可变参数,第一个参数是self ,第二个参数是SEL。

typedef struct objc_selector *SEL;

这个结构体是什么结构。源码没有给出

objc_selector是一个映射到方法的C字符串,需要注意的是@selector()选择子只与函数名有关。不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。由于这点特性,也导致了OC不支持函数重载。

消息执行的基本流程如下:(后面会有时序图)
在receiver拿到对应的selector之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。

总结如下
1.检测这个 selector是不是要忽略的。
2.检查target是不是为nil。

如果这里有相应的nil的处理函数,就跳转到相应的函数中。
如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因。

3.确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。

如果找到,就跳转进去执行。
如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止。

image

4.如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程。
苹果文档

源码分析

    ENTRY _objc_msgSend
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x9, x13, #ISA_MASK  // x9 = class   
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x9, [x10, x11, LSL #3]
    b   LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret
    END_ENTRY _objc_msgSend

1.cmp x0, #0。 x0 代表传入的第一个参数self ,#0 代表0 。意思是传入的第一个参数不能是nil 。

cmp是比较指令, cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
比如:mov ax,8
mov bx,3
cmp ax,bx
执行后:ax=8,ZF=0,PF=1,SF=0,CF=0,OF=0.
通过cmp指令执行后,相关标志位的值就可以看出比较的结果。
cmp ax,bx的逻辑含义是比较ax,bx中的值。如果执行后:
ZF=1则AX=BX
ZF=0则AX!=BX
CF=1则AX<BX
CF=0则AX>=BX
CF=0并ZF=0则AX>BX
CF=1或ZF=1则AX<=BX

2.b.le LNilOrTagged 这里根据上面指针判断的结构,要是X0数值小于或者等于 0 ,那么就执行 LNilOrTagged 标签下的内容,否则就向下执行

LE,小于或等于,Less or Equal。
B指令(Branch)表示无条件跳转.

3.ldr x13, [x0] // x13 = isa .将寄存器x13 存入x0 (isa)
4.and x9, x13, #ISA_MASK // x9 = class 。 这里x9 获取到class

AND位与指令
AND R0,R1,R2; R0=R1 & R2
AND R0,R1,#0xFF ;R0=R1 & 0xFF

5.CacheLookup NORMAL 检查缓存

6.看LNilOrTagged,标签。这里就是检测,从tagger指针中获取isa

接下来重点看
CacheLookup 。这里我们知道 x9 寄存器存储的是class 传入的$0 =NORMAL

 * CacheLookup NORMAL|GETIMP
 * 
 * Locate the implementation for a selector in a class method cache.
 *
 * Takes:
 *   x1 = selector
 *   x9 = class to be searched
 *
 * Kills:
 *   x10,x11,x12, x16,x17
 *
 * On exit: (found) exits CacheLookup 
 *                  with x9 = class, x17 = IMP
 *          (not found) jumps to LCacheMiss
 *
 ********************************************************************/

从注释中我们知道 x1 存入的sel ,x9 就是class
要是找到 x9 没变,x17 存入的IMP
没找到,就调用LCacheMiss


.macro CacheLookup
    // x1 = SEL, x9 = isa
    ldp x10, x11, [x9, #CACHE]  // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x16, x17, [x12]     // {x16, x17} = *bucket
1:  cmp x16, x1         // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->cls == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x16, x17, [x12, #-16]!  // {x16, x17} = *--bucket
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x16, x17, [x12]     // {x16, x17} = *bucket
1:  cmp x16, x1         // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->cls == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x16, x17, [x12, #-16]!  // {x16, x17} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro

这里还是先上个图 对照图讲能好点


时序图.png

1.ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask。这里就是给x10 x11 寄存器赋值,x10 获取到buckets,x11 获取到x11 = mask。对应图中的1,2,3步骤

/* Selected field offsets in class structure */
define SUPERCLASS 8
define CACHE 16

LDP指令,从内存某地址处加载两个字到目的寄存器中,用法:LDP Wt1, Wt2, addr。

2.and w12, w1, w11 // x12 = _cmd & mask 。 对应图中的4,5 步骤。这步就是获取到要从那个地址进行比较。

这里w1 和x1 是一样的,w11 和 x11 一样。
这里解释下,_cmd 是SEL。而SEL.typedef struct objc_selector *SEL; 是个结构体。说明SEL 是个指针。可以转化成八字节数字。因此就可以了mask & 了。获取的值其实就是一个数字

3.add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) 获取到需要开始搜寻的首地址。对应图中的6 和7步骤。

LSL(Logic Shift Left) 逻辑左移指令,也就是向左移位,跟算术左移(ASL=Arithmetic Shift Left)是一样的。
。#4 代表数字4
从第三步,我们获取了cmd 应该存在内存中的位置,这步的意思是到这个地方,将地址保存到x12 中。
读者可能看到这里有点糊涂,解释下。假设我们的mask 是0xf 那么。我们就会在内存中分配oxf *buckets+2 大小的内存。首地址就是cache。假设CMd=0x3 那么我们就应该到 0x3 * buckets 的地方找,没有找到就向前寻找下一个。


image.png
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}
#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
#end

这里需要看cache_next 函数 ,这个函数在arm64 中是i--

4.ldp x16, x17, [x12] // {x16, x17} = bucket。这里x16 获取到 cache_key_t x17 获取_imp 。对应图中的8 和9

5.1: cmp x16, x1 // if (bucket->sel != _cmd)。 这里就是比较X16 和 CMD 是否相等。对应图中的11.

这里1 是标签,可以用来跳转的。 比如 B 1;
我们从缓存查找cmd的IMP 是根据cmd 转换成数字,到指定位置去找,当我们存入的时候就需要把这个cmd 保存在cache_key_t位置。这样下次找到该地方,要是cache_key_t key 值一样。那么直接获取imp就行 了

6.b.ne 2f // scan more 。.检查到cmd 和取到的值不相等。那就就要跳转到2标签处执行。对应图中的12.

不等于:NE=Not Equal <>
2f f 代表向front 想下找标签 2 。2b 中的b 代表 back 。表示向上找标签1

7.CacheHit $0 。这里要是在缓存中找到了。那么就调用 CacheHit 对应图中的13

.macro CacheHit
    MESSENGER_END_FAST
.if $0 == NORMAL
    br  x17         // call imp
.else
    b   LGetImpHit
.endif

这里很简单,就是调用下 imp

Br 无条件地将控制转移到目标指令。就是执行命令

  1. 假设在缓存中没有找到,那么就条跳转到 标签2 处。
    CheckMiss $0 // miss if bucket->cls == 0
    这里调用了 CheckMiss
    .macro CheckMiss
.if $0 == NORMAL            // miss if bucket->cls == 0
    cbz x16, __objc_msgSend_uncached_impcache
.else
    cbz x16, LGetImpMiss
.endif
.endmacro

这里我们知道$0 是 NORMAL

CBZ 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
如果这里x16 是0 就调用__objc_msgSend_uncached_impcache

9.cmp x12, x10 // wrap if bucket == buckets
检查x12 是不是首地址。

  1. b.eq 3f。是首地址,那么就跳转3f 执行。否则就接着执行 。执行19步骤。

10.*ldp x16, x17, [x12, #-16]! // {x16, x17} = --bucket。 代表先将x12 和#-16 运算在赋值给x16 和x17 。这里是代表向前偏移一个butcket. 对应图中的16 和18

arm汇编中存在一个神奇的可选后缀“!”,一般是在寄存器或寻址方式之后,对于加了叹号的情况,访问内存时先根据寻址方式更改寄存器的值,再按照该已经更新的值访问内存。

11.b 1b // loop. 跳转到后面的1 标签处执行

12.** add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)** . 赋值 。这里对应图中的19 20。将缓存指针移动到最后。

13.** ldp x16, x17, [x12] // {x16, x17} = bucket*。对应推重的21 、22

14 cmp x16, x1。同上面的步骤
15.b.ne 2f 。同上面
16ldp x16, x17, [x12, #-16]! 同上面

其实看完源码大概能了解CacheLookup这个函数的意思了

1其实是执行了两边缓存检查
2.第一次检查是将地址便宜到cmd& mask 所在位置向前查找到first
3.要是没有找到,那么就执行第二遍检查,将地址移动到mask位置,就是结尾,接着向前查找到首地址。

_class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

这个函数是在缓存中没有找到。就调用到这个函数了。这里主要是
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver) 函数调用

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    // be added but ignored indefinitely because the cache was re-filled 
    // with the old value after the cache flush on behalf of the category.
 retry:
    runtimeLock.read();

    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.

    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }

    // Try superclass caches and method lists.

    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  imp != (IMP)&_objc_ignored_method));

    // paranoia: never let uncached leak out
    assert(imp != _objc_msgSend_uncached_impcache);

    return imp;
}

重点分析这个函数

源码

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP imp = nil;
    Method meth;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    if (!cls->isRealized()) {
        rwlock_writer_t lock(runtimeLock);
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    // The lock is held to make method-lookup + cache-fill atomic 
    // with respect to method addition. Otherwise, a category could 
    // be added but ignored indefinitely because the cache was re-filled 
    // with the old value after the cache flush on behalf of the category.
 retry:
    runtimeLock.read();

    // Ignore GC selectors
    if (ignoreSelector(sel)) {
        imp = _objc_ignored_method;
        cache_fill(cls, sel, imp, inst);
        goto done;
    }

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.

    meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }

    // Try superclass caches and method lists.

    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // Superclass method list.
        meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    // paranoia: look for ignored selectors with non-ignored implementations
    assert(!(ignoreSelector(sel)  &&  imp != (IMP)&_objc_ignored_method));

    // paranoia: never let uncached leak out
    assert(imp != _objc_msgSend_uncached_impcache);

    return imp;
}


消息发送

这个图就是上面_class_lookupMethodAndLoadCache3 的路程框图

1.红色部分.这部分很简单,就是检测类或者实例变量是否实例化。没有实例化就分别实例化变量或者类。并且cache=NO,不会执行查询缓存操作。
2.白色部分。 这里检查sel是否被忽略掉了,


/* ignored selector support */

/* Non-GC: no ignored selectors
   GC (i386 Mac): some selectors ignored, remapped to kIgnore
   GC (others): some selectors ignored, but not remapped 
*/

static inline int ignoreSelector(SEL sel)
{
#if !SUPPORT_GC
    return NO;
#elif SUPPORT_IGNORED_SELECTOR_CONSTANT
    return UseGC  &&  sel == (SEL)kIgnore;
#else
    return UseGC  &&  
        (sel == @selector(retain)       ||  
         sel == @selector(release)      ||  
         sel == @selector(autorelease)  ||  
         sel == @selector(retainCount)  ||  
         sel == @selector(dealloc));
#endif
}

苹果的注释,GC ,不是模拟器的,忽略下面这几个方法。
3.第三部分。叫浅绿色的吧。这就是从类中查询方法。
查询流程都是

cache->methedList->superCache->superMethodList

直到super 是nil为止。(这里我们知道不管元类还是类本身,他们的superclass 最终都指向nil)。
要是找到方法,就先把imp 存入缓存中。返回IMP。
这里有个存入缓存操作。我们看看源码,IMP到底是怎么存入缓存的。存入缓存的最终函数是static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)

4.要是没有找到IMP,那么就要执行一次消息转发了。(蓝色部分这部分在下面讲)
5.要是消息转发还是没有获取到IMP ,那么就把IMP标记为_objc_msgForward_impcache。存入缓存中。返回IMP。

_class_resolveMethod

这里我们把这个方法单独拿出来看。因为在这里我们可以动态的添加方法。我们从上图能看出来,当调用完这个函数的时候还会执行一遍缓存或者方法列表遍历一次。

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

当cls 不是元类调用_class_resolveInstanceMethod 方法。
当cls 是元类的时候, 调用_class_resolveClassMethod 方法

/
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;
}

这个函数挺关键的。这里就直接讲了

这个函数不复杂,就是调用了下lookUpImpOrForward 方法。
这个方法的具体流程图在上面的图中。
不过这里我们看传入的参数。
bool initialize 控制的逻辑很简单,No,就需要再实例化类了。只影响红色部分
bool cache 代表是否要查询缓存,也是图中的红色部分。
bool resolve 是NO ,就不用走了动态加载了。影响图中的蓝色部分。

_class_resolveInstanceMethod

我们看不是元类怎么动态加载方法的

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

1.查找我们是否实现了方法SEL_resolveInstanceMethod 。这里我们给lookUpImpOrNil传入的参数是initialize=NO.(不需要实例化方法)。cache= YES ,需要查询缓存,resolver=NO,(不需要动态加载)。这里的SEL_resolveInstanceMethod 就代表我们写的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 。这里也解释了,为什么是个+ 方法,我们用的是cls->ISA();元类的方法列表中查询。没有实现这个方法,那么久直接返回了。

2.要是实现了改方法,接着执行,调用下改方法。

  1. 再查询一遍有没有动态加载上方法。要是在resolveInstanceMethod 方法中我们给sel 增加了IMP ,这里调用就将其加入到缓存中了。

_class_resolveClassMethod

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

1.这里因为是元类才能调用这个方法,给lookUpImpOrNil调用传入的cls 就是自己了。不用获取isa。对应的是+号。
2.这里要注意下。调用objc_msgSend 方法的时候,第一个参数应该传入self,objc_msgSend 会获取self的isa 指针接着调用方法。
但是我们在_class_resolveClassMethod中,cls是元类,他的isa就是根元类了。这调用就不对了。因此我们需要获取一个对象,让对象的isa是cls 就可以了。调用了_class_getNonMetaClass()方法。从这里我们终于知道了传入的id inst ,参数是干嘛的了。就是为了元类调用方法需要该参数把元类包装一下啦。
3其他的就同上面了。

消息转发

我们知道要是查询缓存和动态加载函数都没有找到方法,我们会在缓存中存入一个IMP=_objc_msgForward_impcache,这样就保证内存中肯定有了IMP 。观察上图消息objc_msgSend 方法,在有IMP的时候执行13步骤。

    CacheHit $0         // call or return imp

objc_msgForward

我们看看这个_objc_msgForward_impcache IMP 干嘛了

    STATIC_ENTRY __objc_msgForward_impcache

    MESSENGER_START
    nop
    MESSENGER_END_SLOW

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

很简单,就是跳转到了__objc_msgForward 放方法

    ENTRY __objc_msgForward
    adrp    x17, __objc_forward_handler@PAGE
    ldr x17, [x17, __objc_forward_handler@PAGEOFF]
    br  x17
    
    END_ENTRY __objc_msgForward

这里执行_objc_msgForward 方法调用了objc_defaultForwardHandler 方法。这里

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

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

系统给我们指定了一个默认地址。在这个函数会有打印语句,这个_objc_fatal.就会干掉我们的进程。
看到这里我们很懵逼,这岂不是每次调用到消息转发都会崩溃么?
因此每次调用到_objc_msgForward 方法 ,他指向的地址是objc_defaultForwardHandler 。调用该函数就崩溃了。
但是实际没有崩溃。为什么呢?肯定是有地方在执行_objc_forward_handler的时候,可以修改_objc_forward_handler指针的指向。那就是下面这个函数了。

void objc_setForwardHandler (void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

从这个函数看。我们可以设置_objc_forward_handler 指针指向。
但是什么时候调用这个函数呢?
这个没弄过你想可以看这篇文章
这里调用objc_setForwardHandler 方法是在__CFInitialize()方法中,该方法是在CF runtime 连接到进程时初始化调用的。
调用** objc_setForwardHandler** 方法,我们传入两个参数** __CF_forwarding_prep_0forwarding_prep_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;
}

在这两个函数中调用了** forwarding** 函数

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 方法。

到这里消息转发就结束了。

流程图如下
发送消息和消息转发

考题

下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
 + (void)foo;
 - (void)foo;
 @end
 @implementation NSObject (Sark)
 - (void)foo
 {
    NSLog(@"IMP: -[NSObject(Sark) foo]");
 }
 @end
 int main(int argc, const char * argv[]) {
  @autoreleasepool {
      [NSObject foo];
      [[NSObject new] foo];
}
return 0;
}

答案很简单,都调用了。这里就是考试了有个类和元类的superClass 的指向,superClass的最后指向是NSObject根类。可以调用

Runtime中的优化

1.方法列表的缓存

在消息发送过程中,查找IMP的过程,会优先查找缓存。这个缓存会存储最近使用过的方法都缓存起来。这个cache和CPU里面的cache的工作方式有点类似。原理是调用的方法有可能经常会被调用。如果没有这个缓存,直接去类方法的方法链表里面去查找,查询效率实在太低。所以查找IMP会优先搜索饭方法缓存,如果没有找到,接着会在虚函数表中寻找IMP。如果找到了,就会把这个IMP存储到缓存中备用。

基于这个设计,使Runtime系统能能够执行快速高效的方法查询操作。

2.虚函数表

虚函数表也称为分派表,是编程语言中常用的动态绑定支持机制。在OC的Runtime运行时系统库实现了一种自定义的虚函数表分派机制。这个表是专门用来提高性能和灵活性的。这个虚函数表是用来存储IMP类型的数组。每个object-class都有这样一个指向虚函数表的指针。

3.dyld共享缓存

在我们的程序中,一定会有很多自定义类,而这些类中,很多SEL是重名的,比如alloc,init等等。Runtime系统需要为每一个方法给定一个SEL指针,然后为每次调用个各个方法更新元数据,以获取唯一值。这个过程是在应用程序启动的时候完成。为了提高这一部分的执行效率,Runtime会通过dyld共享缓存实现选择器的唯一性。

dyld是一种系统服务,用于定位和加载动态库。它含有共享缓存,能够使多个进程共用这些动态库。dyld共享缓存中含有一个选择器表,从而能使运行时系统能够通过使用缓存访问共享库和自定义类的选择器。

关于dyld的知识可以看看这篇文章dyld: Dynamic Linking On OS X

这里的dyld 共享缓存,会后续jiang'j

源代码地址
借鉴博客
借鉴博客
苹果文档

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

推荐阅读更多精彩内容