Runtime 消息發(fā)送和轉(zhuǎn)發(fā)

一.objc_msgSend函數(shù)簡(jiǎn)介

以前去面試,有人問(wèn)了這個(gè)一個(gè)問(wèn)題

[receiver message] 

發(fā)生了什么?
一聽這個(gè)問(wèn)題,一臉懵逼。這不就是簡(jiǎn)單的調(diào)用函數(shù)么?其實(shí)吧。考官問(wèn)的就是消息發(fā)送。

[receiver message]

會(huì)被編譯器轉(zhuǎn)化為:

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

獲取到的結(jié)果就是

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

這個(gè)函數(shù)接收可變參數(shù),第一個(gè)參數(shù)是self ,第二個(gè)參數(shù)是SEL。

typedef struct objc_selector *SEL;

這個(gè)結(jié)構(gòu)體是什么結(jié)構(gòu)。源碼沒有給出

objc_selector是一個(gè)映射到方法的C字符串,需要注意的是@selector()選擇子只與函數(shù)名有關(guān)。不同類中相同名字的方法所對(duì)應(yīng)的方法選擇器是相同的,即使方法名字相同而變量類型不同也會(huì)導(dǎo)致它們具有相同的方法選擇器。由于這點(diǎn)特性,也導(dǎo)致了OC不支持函數(shù)重載。

消息執(zhí)行的基本流程如下:(后面會(huì)有時(shí)序圖)
在receiver拿到對(duì)應(yīng)的selector之后,如果自己無(wú)法執(zhí)行這個(gè)方法,那么該條消息要被轉(zhuǎn)發(fā)?;蛘吲R時(shí)動(dòng)態(tài)的添加方法實(shí)現(xiàn)。如果轉(zhuǎn)發(fā)到最后依舊沒法處理,程序就會(huì)崩潰。

總結(jié)如下
1.檢測(cè)這個(gè) selector是不是要忽略的。
2.檢查target是不是為nil。

如果這里有相應(yīng)的nil的處理函數(shù),就跳轉(zhuǎn)到相應(yīng)的函數(shù)中。
如果沒有處理nil的函數(shù),就自動(dòng)清理現(xiàn)場(chǎng)并返回。這一點(diǎn)就是為何在OC中給nil發(fā)送消息不會(huì)崩潰的原因。

3.確定不是給nil發(fā)消息之后,在該class的緩存中查找方法對(duì)應(yīng)的IMP實(shí)現(xiàn)。

如果找到,就跳轉(zhuǎn)進(jìn)去執(zhí)行。
如果沒有找到,就在方法分發(fā)表里面繼續(xù)查找,一直找到NSObject為止。

image

4.如果還沒有找到,那就需要開始消息轉(zhuǎn)發(fā)階段了。至此,發(fā)送消息Messaging階段完成。這一階段主要完成的是通過(guò)select()快速查找IMP的過(guò)程。
蘋果文檔

源碼分析

    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 代表傳入的第一個(gè)參數(shù)self ,#0 代表0 。意思是傳入的第一個(gè)參數(shù)不能是nil 。

cmp是比較指令, cmp的功能相當(dāng)于減法指令,只是不保存結(jié)果。cmp指令執(zhí)行后,將對(duì)標(biāo)志寄存器產(chǎn)生影響。其他相關(guān)指令通過(guò)識(shí)別這些被影響的標(biāo)志寄存器位來(lái)得知比較結(jié)果。
比如:mov ax,8
mov bx,3
cmp ax,bx
執(zhí)行后:ax=8,ZF=0,PF=1,SF=0,CF=0,OF=0.
通過(guò)cmp指令執(zhí)行后,相關(guān)標(biāo)志位的值就可以看出比較的結(jié)果。
cmp ax,bx的邏輯含義是比較ax,bx中的值。如果執(zhí)行后:
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 這里根據(jù)上面指針判斷的結(jié)構(gòu),要是X0數(shù)值小于或者等于 0 ,那么就執(zhí)行 LNilOrTagged 標(biāo)簽下的內(nèi)容,否則就向下執(zhí)行

LE,小于或等于,Less or Equal。
B指令(Branch)表示無(wú)條件跳轉(zhuǎn).

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,標(biāo)簽。這里就是檢測(cè),從tagger指針中獲取isa

接下來(lái)重點(diǎn)看
CacheLookup 。這里我們知道 x9 寄存器存儲(chǔ)的是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
沒找到,就調(diào)用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

這里還是先上個(gè)圖 對(duì)照?qǐng)D講能好點(diǎn)


時(shí)序圖.png

1.ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask。這里就是給x10 x11 寄存器賦值,x10 獲取到buckets,x11 獲取到x11 = mask。對(duì)應(yīng)圖中的1,2,3步驟

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

LDP指令,從內(nèi)存某地址處加載兩個(gè)字到目的寄存器中,用法:LDP Wt1, Wt2, addr。

2.and w12, w1, w11 // x12 = _cmd & mask 。 對(duì)應(yīng)圖中的4,5 步驟。這步就是獲取到要從那個(gè)地址進(jìn)行比較。

這里w1 和x1 是一樣的,w11 和 x11 一樣。
這里解釋下,_cmd 是SEL。而SEL.typedef struct objc_selector *SEL; 是個(gè)結(jié)構(gòu)體。說(shuō)明SEL 是個(gè)指針。可以轉(zhuǎn)化成八字節(jié)數(shù)字。因此就可以了mask & 了。獲取的值其實(shí)就是一個(gè)數(shù)字

3.add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) 獲取到需要開始搜尋的首地址。對(duì)應(yīng)圖中的6 和7步驟。

LSL(Logic Shift Left) 邏輯左移指令,也就是向左移位,跟算術(shù)左移(ASL=Arithmetic Shift Left)是一樣的。
。#4 代表數(shù)字4
從第三步,我們獲取了cmd 應(yīng)該存在內(nèi)存中的位置,這步的意思是到這個(gè)地方,將地址保存到x12 中。
讀者可能看到這里有點(diǎn)糊涂,解釋下。假設(shè)我們的mask 是0xf 那么。我們就會(huì)在內(nèi)存中分配oxf *buckets+2 大小的內(nèi)存。首地址就是cache。假設(shè)CMd=0x3 那么我們就應(yīng)該到 0x3 * buckets 的地方找,沒有找到就向前尋找下一個(gè)。


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 函數(shù) ,這個(gè)函數(shù)在arm64 中是i--

4.ldp x16, x17, [x12] // {x16, x17} = bucket。這里x16 獲取到 cache_key_t x17 獲取_imp 。對(duì)應(yīng)圖中的8 和9

5.1: cmp x16, x1 // if (bucket->sel != _cmd)。 這里就是比較X16 和 CMD 是否相等。對(duì)應(yīng)圖中的11.

這里1 是標(biāo)簽,可以用來(lái)跳轉(zhuǎn)的。 比如 B 1;
我們從緩存查找cmd的IMP 是根據(jù)cmd 轉(zhuǎn)換成數(shù)字,到指定位置去找,當(dāng)我們存入的時(shí)候就需要把這個(gè)cmd 保存在cache_key_t位置。這樣下次找到該地方,要是cache_key_t key 值一樣。那么直接獲取imp就行 了

6.b.ne 2f // scan more 。.檢查到cmd 和取到的值不相等。那就就要跳轉(zhuǎn)到2標(biāo)簽處執(zhí)行。對(duì)應(yīng)圖中的12.

不等于:NE=Not Equal <>
2f f 代表向front 想下找標(biāo)簽 2 。2b 中的b 代表 back 。表示向上找標(biāo)簽1

7.CacheHit $0 。這里要是在緩存中找到了。那么就調(diào)用 CacheHit 對(duì)應(yīng)圖中的13

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

這里很簡(jiǎn)單,就是調(diào)用下 imp

Br 無(wú)條件地將控制轉(zhuǎn)移到目標(biāo)指令。就是執(zhí)行命令

  1. 假設(shè)在緩存中沒有找到,那么就條跳轉(zhuǎn)到 標(biāo)簽2 處。
    CheckMiss $0 // miss if bucket->cls == 0
    這里調(diào)用了 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),如果結(jié)果為零(Zero)就轉(zhuǎn)移(只能跳到后面的指令)
如果這里x16 是0 就調(diào)用__objc_msgSend_uncached_impcache

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

  1. b.eq 3f。是首地址,那么就跳轉(zhuǎn)3f 執(zhí)行。否則就接著執(zhí)行 。執(zhí)行19步驟。

10.*ldp x16, x17, [x12, #-16]! // {x16, x17} = --bucket。 代表先將x12 和#-16 運(yùn)算在賦值給x16 和x17 。這里是代表向前偏移一個(gè)butcket. 對(duì)應(yīng)圖中的16 和18

arm匯編中存在一個(gè)神奇的可選后綴“!”,一般是在寄存器或?qū)ぶ贩绞街?,?duì)于加了嘆號(hào)的情況,訪問(wèn)內(nèi)存時(shí)先根據(jù)尋址方式更改寄存器的值,再按照該已經(jīng)更新的值訪問(wèn)內(nèi)存。

11.b 1b // loop. 跳轉(zhuǎn)到后面的1 標(biāo)簽處執(zhí)行

12.** add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)** . 賦值 。這里對(duì)應(yīng)圖中的19 20。將緩存指針移動(dòng)到最后。

13.** ldp x16, x17, [x12] // {x16, x17} = bucket*。對(duì)應(yīng)推重的21 、22

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

其實(shí)看完源碼大概能了解CacheLookup這個(gè)函數(shù)的意思了

1其實(shí)是執(zhí)行了兩邊緩存檢查
2.第一次檢查是將地址便宜到cmd& mask 所在位置向前查找到first
3.要是沒有找到,那么就執(zhí)行第二遍檢查,將地址移動(dòng)到mask位置,就是結(jié)尾,接著向前查找到首地址。

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

這個(gè)函數(shù)是在緩存中沒有找到。就調(diào)用到這個(gè)函數(shù)了。這里主要是
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver) 函數(shù)調(diào)用

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

重點(diǎn)分析這個(gè)函數(shù)

源碼

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


消息發(fā)送

這個(gè)圖就是上面_class_lookupMethodAndLoadCache3 的路程框圖

1.紅色部分.這部分很簡(jiǎn)單,就是檢測(cè)類或者實(shí)例變量是否實(shí)例化。沒有實(shí)例化就分別實(shí)例化變量或者類。并且cache=NO,不會(huì)執(zhí)行查詢緩存操作。
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 ,不是模擬器的,忽略下面這幾個(gè)方法。
3.第三部分。叫淺綠色的吧。這就是從類中查詢方法。
查詢流程都是

cache->methedList->superCache->superMethodList

直到super 是nil為止。(這里我們知道不管元類還是類本身,他們的superclass 最終都指向nil)。
要是找到方法,就先把imp 存入緩存中。返回IMP。
這里有個(gè)存入緩存操作。我們看看源碼,IMP到底是怎么存入緩存的。存入緩存的最終函數(shù)是static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)

4.要是沒有找到IMP,那么就要執(zhí)行一次消息轉(zhuǎn)發(fā)了。(藍(lán)色部分這部分在下面講)
5.要是消息轉(zhuǎn)發(fā)還是沒有獲取到IMP ,那么就把IMP標(biāo)記為_objc_msgForward_impcache。存入緩存中。返回IMP。

_class_resolveMethod

這里我們把這個(gè)方法單獨(dú)拿出來(lái)看。因?yàn)樵谶@里我們可以動(dòng)態(tài)的添加方法。我們從上圖能看出來(lái),當(dāng)調(diào)用完這個(gè)函數(shù)的時(shí)候還會(huì)執(zhí)行一遍緩存或者方法列表遍歷一次。

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

當(dāng)cls 不是元類調(diào)用_class_resolveInstanceMethod 方法。
當(dāng)cls 是元類的時(shí)候, 調(diào)用_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;
}

這個(gè)函數(shù)挺關(guān)鍵的。這里就直接講了

這個(gè)函數(shù)不復(fù)雜,就是調(diào)用了下lookUpImpOrForward 方法。
這個(gè)方法的具體流程圖在上面的圖中。
不過(guò)這里我們看傳入的參數(shù)。
bool initialize 控制的邏輯很簡(jiǎn)單,No,就需要再實(shí)例化類了。只影響紅色部分
bool cache 代表是否要查詢緩存,也是圖中的紅色部分。
bool resolve 是NO ,就不用走了動(dòng)態(tài)加載了。影響圖中的藍(lán)色部分。

_class_resolveInstanceMethod

我們看不是元類怎么動(dòng)態(tài)加載方法的

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.查找我們是否實(shí)現(xiàn)了方法SEL_resolveInstanceMethod 。這里我們給lookUpImpOrNil傳入的參數(shù)是initialize=NO.(不需要實(shí)例化方法)。cache= YES ,需要查詢緩存,resolver=NO,(不需要?jiǎng)討B(tài)加載)。這里的SEL_resolveInstanceMethod 就代表我們寫的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 。這里也解釋了,為什么是個(gè)+ 方法,我們用的是cls->ISA();元類的方法列表中查詢。沒有實(shí)現(xiàn)這個(gè)方法,那么久直接返回了。

2.要是實(shí)現(xiàn)了改方法,接著執(zhí)行,調(diào)用下改方法。

  1. 再查詢一遍有沒有動(dòng)態(tài)加載上方法。要是在resolveInstanceMethod 方法中我們給sel 增加了IMP ,這里調(diào)用就將其加入到緩存中了。

_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.這里因?yàn)槭窃惒拍苷{(diào)用這個(gè)方法,給lookUpImpOrNil調(diào)用傳入的cls 就是自己了。不用獲取isa。對(duì)應(yīng)的是+號(hào)。
2.這里要注意下。調(diào)用objc_msgSend 方法的時(shí)候,第一個(gè)參數(shù)應(yīng)該傳入self,objc_msgSend 會(huì)獲取self的isa 指針接著調(diào)用方法。
但是我們?cè)?strong>_class_resolveClassMethod中,cls是元類,他的isa就是根元類了。這調(diào)用就不對(duì)了。因此我們需要獲取一個(gè)對(duì)象,讓對(duì)象的isa是cls 就可以了。調(diào)用了_class_getNonMetaClass()方法。從這里我們終于知道了傳入的id inst ,參數(shù)是干嘛的了。就是為了元類調(diào)用方法需要該參數(shù)把元類包裝一下啦。
3其他的就同上面了。

消息轉(zhuǎn)發(fā)

我們知道要是查詢緩存和動(dòng)態(tài)加載函數(shù)都沒有找到方法,我們會(huì)在緩存中存入一個(gè)IMP=_objc_msgForward_impcache,這樣就保證內(nèi)存中肯定有了IMP 。觀察上圖消息objc_msgSend 方法,在有IMP的時(shí)候執(zhí)行13步驟。

    CacheHit $0         // call or return imp

objc_msgForward

我們看看這個(gè)_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

很簡(jiǎn)單,就是跳轉(zhuǎn)到了__objc_msgForward 放方法

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

這里執(zhí)行_objc_msgForward 方法調(diào)用了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);
}

系統(tǒng)給我們指定了一個(gè)默認(rèn)地址。在這個(gè)函數(shù)會(huì)有打印語(yǔ)句,這個(gè)_objc_fatal.就會(huì)干掉我們的進(jìn)程。
看到這里我們很懵逼,這豈不是每次調(diào)用到消息轉(zhuǎn)發(fā)都會(huì)崩潰么?
因此每次調(diào)用到_objc_msgForward 方法 ,他指向的地址是objc_defaultForwardHandler 。調(diào)用該函數(shù)就崩潰了。
但是實(shí)際沒有崩潰。為什么呢?肯定是有地方在執(zhí)行_objc_forward_handler的時(shí)候,可以修改_objc_forward_handler指針的指向。那就是下面這個(gè)函數(shù)了。

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

從這個(gè)函數(shù)看。我們可以設(shè)置_objc_forward_handler 指針指向。
但是什么時(shí)候調(diào)用這個(gè)函數(shù)呢?
這個(gè)沒弄過(guò)你想可以看這篇文章
這里調(diào)用objc_setForwardHandler 方法是在__CFInitialize()方法中,該方法是在CF runtime 連接到進(jìn)程時(shí)初始化調(diào)用的。
調(diào)用** objc_setForwardHandler** 方法,我們傳入兩個(gè)參數(shù)** __CF_forwarding_prep_0,forwarding_prep_1**。
這兩個(gè)是函數(shù)指針

int __CF_forwarding_prep_0(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    rax = ____forwarding___(rsp, 0x0);
    if (rax != 0x0) { // 轉(zhuǎn)發(fā)結(jié)果不為空,將內(nèi)容返回
            rax = *rax;
    }
    else { // 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 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) {// 轉(zhuǎn)發(fā)結(jié)果不為空,將內(nèi)容返回
            rax = *rax;
    }
    else {// 轉(zhuǎn)發(fā)結(jié)果為空,調(diào)用 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;
}

在這兩個(gè)函數(shù)中調(diào)用了** forwarding** 函數(shù)

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

  // 調(diào)用 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, ...);
    }
  }

  // 僵尸對(duì)象
  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>
  }

  // 調(diào)用 methodSignatureForSelector 獲取方法簽名后再調(diào)用 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 是否已經(jīng)在 Runtime 注冊(cè)過(guò)
  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);
}

這么一大坨代碼就是整個(gè)消息轉(zhuǎn)發(fā)路徑的邏輯,概括如下:
1.先調(diào)用 forwardingTargetForSelector 方法獲取新的 target 作為 receiver 重新執(zhí)行 selector,如果返回的內(nèi)容不合法(為 nil 或者跟舊 receiver 一樣),那就進(jìn)入第二步。
2.調(diào)用 methodSignatureForSelector 獲取方法簽名后,判斷返回類型信息是否正確,再調(diào)用 forwardInvocation 執(zhí)行 NSInvocation 對(duì)象,并將結(jié)果返回。如果對(duì)象沒實(shí)現(xiàn) methodSignatureForSelector 方法,進(jìn)入第三步。
3.調(diào)用 doesNotRecognizeSelector 方法。

到這里消息轉(zhuǎn)發(fā)就結(jié)束了。

流程圖如下
發(fā)送消息和消息轉(zhuǎn)發(fā)

考題

下面的代碼會(huì)?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;
}

答案很簡(jiǎn)單,都調(diào)用了。這里就是考試了有個(gè)類和元類的superClass 的指向,superClass的最后指向是NSObject根類??梢哉{(diào)用

Runtime中的優(yōu)化

1.方法列表的緩存

在消息發(fā)送過(guò)程中,查找IMP的過(guò)程,會(huì)優(yōu)先查找緩存。這個(gè)緩存會(huì)存儲(chǔ)最近使用過(guò)的方法都緩存起來(lái)。這個(gè)cache和CPU里面的cache的工作方式有點(diǎn)類似。原理是調(diào)用的方法有可能經(jīng)常會(huì)被調(diào)用。如果沒有這個(gè)緩存,直接去類方法的方法鏈表里面去查找,查詢效率實(shí)在太低。所以查找IMP會(huì)優(yōu)先搜索飯方法緩存,如果沒有找到,接著會(huì)在虛函數(shù)表中尋找IMP。如果找到了,就會(huì)把這個(gè)IMP存儲(chǔ)到緩存中備用。

基于這個(gè)設(shè)計(jì),使Runtime系統(tǒng)能能夠執(zhí)行快速高效的方法查詢操作。

2.虛函數(shù)表

虛函數(shù)表也稱為分派表,是編程語(yǔ)言中常用的動(dòng)態(tài)綁定支持機(jī)制。在OC的Runtime運(yùn)行時(shí)系統(tǒng)庫(kù)實(shí)現(xiàn)了一種自定義的虛函數(shù)表分派機(jī)制。這個(gè)表是專門用來(lái)提高性能和靈活性的。這個(gè)虛函數(shù)表是用來(lái)存儲(chǔ)IMP類型的數(shù)組。每個(gè)object-class都有這樣一個(gè)指向虛函數(shù)表的指針。

3.dyld共享緩存

在我們的程序中,一定會(huì)有很多自定義類,而這些類中,很多SEL是重名的,比如alloc,init等等。Runtime系統(tǒng)需要為每一個(gè)方法給定一個(gè)SEL指針,然后為每次調(diào)用個(gè)各個(gè)方法更新元數(shù)據(jù),以獲取唯一值。這個(gè)過(guò)程是在應(yīng)用程序啟動(dòng)的時(shí)候完成。為了提高這一部分的執(zhí)行效率,Runtime會(huì)通過(guò)dyld共享緩存實(shí)現(xiàn)選擇器的唯一性。

dyld是一種系統(tǒng)服務(wù),用于定位和加載動(dòng)態(tài)庫(kù)。它含有共享緩存,能夠使多個(gè)進(jìn)程共用這些動(dòng)態(tài)庫(kù)。dyld共享緩存中含有一個(gè)選擇器表,從而能使運(yùn)行時(shí)系統(tǒng)能夠通過(guò)使用緩存訪問(wèn)共享庫(kù)和自定義類的選擇器。

關(guān)于dyld的知識(shí)可以看看這篇文章dyld: Dynamic Linking On OS X

這里的dyld 共享緩存,會(huì)后續(xù)jiang'j

源代碼地址
借鑒博客
借鑒博客
蘋果文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,084評(píng)論 0 9
  • 消息發(fā)送和轉(zhuǎn)發(fā)流程可以概括為:消息發(fā)送(Messaging)是 Runtime 通過(guò) selector 快速查找 ...
    lylaut閱讀 1,993評(píng)論 2 3
  • 我們常常會(huì)聽說(shuō) Objective-C 是一門動(dòng)態(tài)語(yǔ)言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,351評(píng)論 0 7
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡(jiǎn)介 Runt...
    樂樂的簡(jiǎn)書閱讀 2,251評(píng)論 0 9
  • 螞蟻,你們有密集恐懼癥嗎?
    吧吶閱讀 229評(píng)論 0 1

友情鏈接更多精彩內(nèi)容