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

一.objc_msgSend函數(shù)簡介

以前去面試,有人問了這個一個問題

[receiver message] 

發(fā)生了什么锉桑?
一聽這個問題排霉,一臉懵逼。這不就是簡單的調(diào)用函數(shù)么民轴?其實吧攻柠。考官問的就是消息發(fā)送后裸。

[receiver message]

會被編譯器轉(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, ... );

這個函數(shù)接收可變參數(shù)微驶,第一個參數(shù)是self 浪谴,第二個參數(shù)是SEL。

typedef struct objc_selector *SEL;

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

objc_selector是一個映射到方法的C字符串苟耻,需要注意的是@selector()選擇子只與函數(shù)名有關(guān)。不同類中相同名字的方法所對應的方法選擇器是相同的扶檐,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器凶杖。由于這點特性,也導致了OC不支持函數(shù)重載款筑。

消息執(zhí)行的基本流程如下:(后面會有時序圖)
在receiver拿到對應的selector之后智蝠,如果自己無法執(zhí)行這個方法腾么,那么該條消息要被轉(zhuǎn)發(fā)¤就澹或者臨時動態(tài)的添加方法實現(xiàn)解虱。如果轉(zhuǎn)發(fā)到最后依舊沒法處理,程序就會崩潰漆撞。

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

如果這里有相應的nil的處理函數(shù)浮驳,就跳轉(zhuǎn)到相應的函數(shù)中艰匙。
如果沒有處理nil的函數(shù),就自動清理現(xiàn)場并返回抹恳。這一點就是為何在OC中給nil發(fā)送消息不會崩潰的原因员凝。

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

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

image

4.如果還沒有找到窃这,那就需要開始消息轉(zhuǎn)發(fā)階段了瞳别。至此,發(fā)送消息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 代表傳入的第一個參數(shù)self 兆解,#0 代表0 馆铁。意思是傳入的第一個參數(shù)不能是nil 。

cmp是比較指令锅睛, cmp的功能相當于減法指令埠巨,只是不保存結(jié)果。cmp指令執(zhí)行后现拒,將對標志寄存器產(chǎn)生影響辣垒。其他相關(guān)指令通過識別這些被影響的標志寄存器位來得知比較結(jié)果。
比如:mov ax,8
mov bx,3
cmp ax,bx
執(zhí)行后:ax=8,ZF=0,PF=1,SF=0,CF=0,OF=0.
通過cmp指令執(zhí)行后印蔬,相關(guān)標志位的值就可以看出比較的結(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 標簽下的內(nèi)容陵究,否則就向下執(zhí)行

LE眠饮,小于或等于,Less or Equal铜邮。
B指令(Branch)表示無條件跳轉(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,標簽松蒜。這里就是檢測扔茅,從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
沒找到,就調(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

這里還是先上個圖 對照圖講能好點


時序圖.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指令弧可,從內(nèi)存某地址處加載兩個字到目的寄存器中蔑匣,用法: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; 是個結(jié)構(gòu)體。說明SEL 是個指針≈枋樱可以轉(zhuǎn)化成八字節(jié)數(shù)字闰挡。因此就可以了mask & 了。獲取的值其實就是一個數(shù)字

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

LSL(Logic Shift Left) 邏輯左移指令,也就是向左移位蜘拉,跟算術(shù)左移(ASL=Arithmetic Shift Left)是一樣的萨西。
。#4 代表數(shù)字4
從第三步旭旭,我們獲取了cmd 應該存在內(nèi)存中的位置谎脯,這步的意思是到這個地方,將地址保存到x12 中持寄。
讀者可能看到這里有點糊涂源梭,解釋下。假設(shè)我們的mask 是0xf 那么稍味。我們就會在內(nèi)存中分配oxf *buckets+2 大小的內(nèi)存废麻。首地址就是cache。假設(shè)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 函數(shù) 烛愧,這個函數(shù)在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 是標簽顶吮,可以用來跳轉(zhuǎn)的社牲。 比如 B 1;
我們從緩存查找cmd的IMP 是根據(jù)cmd 轉(zhuǎn)換成數(shù)字,到指定位置去找悴了,當我們存入的時候就需要把這個cmd 保存在cache_key_t位置搏恤。這樣下次找到該地方,要是cache_key_t key 值一樣湃交。那么直接獲取imp就行 了

6.b.ne 2f // scan more 熟空。.檢查到cmd 和取到的值不相等。那就就要跳轉(zhuǎn)到2標簽處執(zhí)行搞莺。對應圖中的12.

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

7.CacheHit $0 才沧。這里要是在緩存中找到了迈喉。那么就調(diào)用 CacheHit 對應圖中的13

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

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

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

  1. 假設(shè)在緩存中沒有找到挨摸,那么就條跳轉(zhuǎn)到 標簽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 運算在賦值給x16 和x17 。這里是代表向前偏移一個butcket. 對應圖中的16 和18

arm匯編中存在一個神奇的可選后綴“诽偷!”,一般是在寄存器或?qū)ぶ贩绞街蠓枥ぃ瑢τ诩恿藝@號的情況报慕,訪問內(nèi)存時先根據(jù)尋址方式更改寄存器的值,再按照該已經(jīng)更新的值訪問內(nèi)存压怠。

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

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這個函數(shù)的意思了

1其實是執(zhí)行了兩邊緩存檢查
2.第一次檢查是將地址便宜到cmd& mask 所在位置向前查找到first
3.要是沒有找到,那么就執(zhí)行第二遍檢查栖忠,將地址移動到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*/);
}

這個函數(shù)是在緩存中沒有找到狸相。就調(diào)用到這個函數(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;
}

重點分析這個函數(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ā)送

這個圖就是上面_class_lookupMethodAndLoadCache3 的路程框圖

1.紅色部分.這部分很簡單捐川,就是檢測類或者實例變量是否實例化脓鹃。沒有實例化就分別實例化變量或者類。并且cache=NO古沥,不會執(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 ,不是模擬器的,忽略下面這幾個方法纯衍。
3.第三部分栋齿。叫淺綠色的吧。這就是從類中查詢方法。
查詢流程都是

cache->methedList->superCache->superMethodList

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

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

_class_resolveMethod

這里我們把這個方法單獨拿出來看溅漾。因為在這里我們可以動態(tài)的添加方法山叮。我們從上圖能看出來,當調(diào)用完這個函數(shù)的時候還會執(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);
        }
    }
}

當cls 不是元類調(diào)用_class_resolveInstanceMethod 方法屁倔。
當cls 是元類的時候, 調(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;
}

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

這個函數(shù)不復雜汰现,就是調(diào)用了下lookUpImpOrForward 方法。
這個方法的具體流程圖在上面的圖中叔壤。
不過這里我們看傳入的參數(shù)瞎饲。
bool initialize 控制的邏輯很簡單,No,就需要再實例化類了炼绘。只影響紅色部分
bool cache 代表是否要查詢緩存嗅战,也是圖中的紅色部分。
bool resolve 是NO 俺亮,就不用走了動態(tài)加載了驮捍。影響圖中的藍色部分。

_class_resolveInstanceMethod

我們看不是元類怎么動態(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.查找我們是否實現(xiàn)了方法SEL_resolveInstanceMethod 脚曾。這里我們給lookUpImpOrNil傳入的參數(shù)是initialize=NO.(不需要實例化方法)东且。cache= YES ,需要查詢緩存,resolver=NO,(不需要動態(tài)加載)本讥。這里的SEL_resolveInstanceMethod 就代表我們寫的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 珊泳。這里也解釋了鲁冯,為什么是個+ 方法,我們用的是cls->ISA();元類的方法列表中查詢色查。沒有實現(xiàn)這個方法薯演,那么久直接返回了。

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

  1. 再查詢一遍有沒有動態(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.這里因為是元類才能調(diào)用這個方法晶通,給lookUpImpOrNil調(diào)用傳入的cls 就是自己了璃氢。不用獲取isa。對應的是+號录择。
2.這里要注意下拔莱。調(diào)用objc_msgSend 方法的時候碗降,第一個參數(shù)應該傳入self隘竭,objc_msgSend 會獲取self的isa 指針接著調(diào)用方法。
但是我們在_class_resolveClassMethod中讼渊,cls是元類动看,他的isa就是根元類了。這調(diào)用就不對了爪幻。因此我們需要獲取一個對象菱皆,讓對象的isa是cls 就可以了。調(diào)用了_class_getNonMetaClass()方法挨稿。從這里我們終于知道了傳入的id inst 仇轻,參數(shù)是干嘛的了。就是為了元類調(diào)用方法需要該參數(shù)把元類包裝一下啦奶甘。
3其他的就同上面了篷店。

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

我們知道要是查詢緩存和動態(tài)加載函數(shù)都沒有找到方法,我們會在緩存中存入一個IMP=_objc_msgForward_impcache臭家,這樣就保證內(nèi)存中肯定有了IMP 疲陕。觀察上圖消息objc_msgSend 方法,在有IMP的時候執(zhí)行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

很簡單蹄殃,就是跳轉(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)給我們指定了一個默認地址你踩。在這個函數(shù)會有打印語句诅岩,這個_objc_fatal.就會干掉我們的進程讳苦。
看到這里我們很懵逼,這豈不是每次調(diào)用到消息轉(zhuǎn)發(fā)都會崩潰么按厘?
因此每次調(diào)用到_objc_msgForward 方法 医吊,他指向的地址是objc_defaultForwardHandler 。調(diào)用該函數(shù)就崩潰了逮京。
但是實際沒有崩潰卿堂。為什么呢?肯定是有地方在執(zhí)行_objc_forward_handler的時候懒棉,可以修改_objc_forward_handler指針的指向草描。那就是下面這個函數(shù)了。

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

從這個函數(shù)看策严。我們可以設(shè)置_objc_forward_handler 指針指向穗慕。
但是什么時候調(diào)用這個函數(shù)呢?
這個沒弄過你想可以看這篇文章
這里調(diào)用objc_setForwardHandler 方法是在__CFInitialize()方法中妻导,該方法是在CF runtime 連接到進程時初始化調(diào)用的逛绵。
調(diào)用** objc_setForwardHandler** 方法,我們傳入兩個參數(shù)** __CF_forwarding_prep_0倔韭,forwarding_prep_1**术浪。
這兩個是函數(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;
}

在這兩個函數(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, ...);
    }
  }

  // 僵尸對象
  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 注冊過
  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);
}

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

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

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

考題

下面的代碼會豌蟋?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;
}

答案很簡單廊散,都調(diào)用了。這里就是考試了有個類和元類的superClass 的指向梧疲,superClass的最后指向是NSObject根類允睹≡俗迹可以調(diào)用

Runtime中的優(yōu)化

1.方法列表的緩存

在消息發(fā)送過程中,查找IMP的過程缭受,會優(yōu)先查找緩存胁澳。這個緩存會存儲最近使用過的方法都緩存起來。這個cache和CPU里面的cache的工作方式有點類似米者。原理是調(diào)用的方法有可能經(jīng)常會被調(diào)用韭畸。如果沒有這個緩存,直接去類方法的方法鏈表里面去查找蔓搞,查詢效率實在太低胰丁。所以查找IMP會優(yōu)先搜索飯方法緩存,如果沒有找到喂分,接著會在虛函數(shù)表中尋找IMP锦庸。如果找到了,就會把這個IMP存儲到緩存中備用蒲祈。

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

2.虛函數(shù)表

虛函數(shù)表也稱為分派表梆掸,是編程語言中常用的動態(tài)綁定支持機制扬卷。在OC的Runtime運行時系統(tǒng)庫實現(xiàn)了一種自定義的虛函數(shù)表分派機制。這個表是專門用來提高性能和靈活性的沥潭。這個虛函數(shù)表是用來存儲IMP類型的數(shù)組邀泉。每個object-class都有這樣一個指向虛函數(shù)表的指針嬉挡。

3.dyld共享緩存

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

dyld是一種系統(tǒng)服務咸这,用于定位和加載動態(tài)庫。它含有共享緩存魔眨,能夠使多個進程共用這些動態(tài)庫媳维。dyld共享緩存中含有一個選擇器表酿雪,從而能使運行時系統(tǒng)能夠通過使用緩存訪問共享庫和自定義類的選擇器。

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末指黎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子州丹,更是在濱河造成了極大的恐慌醋安,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墓毒,死亡現(xiàn)場離奇詭異茬故,居然都是意外死亡,警方通過查閱死者的電腦和手機蚁鳖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門磺芭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人醉箕,你說我怎么就攤上這事钾腺。” “怎么了讥裤?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵放棒,是天一觀的道長。 經(jīng)常有香客問我己英,道長间螟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任损肛,我火速辦了婚禮厢破,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘治拿。我一直安慰自己摩泪,他們只是感情好,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布劫谅。 她就那樣靜靜地躺著见坑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捏检。 梳的紋絲不亂的頭發(fā)上荞驴,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機與錄音贯城,去河邊找鬼熊楼。 笑死,一個胖子當著我的面吹牛冤狡,可吹牛的內(nèi)容都是我干的孙蒙。 我是一名探鬼主播项棠,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挎峦!你這毒婦竟也來了香追?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坦胶,失蹤者是張志新(化名)和其女友劉穎透典,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顿苇,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡峭咒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了纪岁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凑队。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幔翰,靈堂內(nèi)的尸體忽然破棺而出漩氨,到底是詐尸還是另有隱情,我是刑警寧澤遗增,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布叫惊,位于F島的核電站,受9級特大地震影響做修,放射性物質(zhì)發(fā)生泄漏霍狰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一饰及、第九天 我趴在偏房一處隱蔽的房頂上張望蔗坯。 院中可真熱鬧,春花似錦旋炒、人聲如沸步悠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至答姥,卻和暖如春铣除,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹦付。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工尚粘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人敲长。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓郎嫁,卻偏偏與公主長得像秉继,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子泽铛,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容

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