iOS - Runtime 中 Class、消息機(jī)制、super 關(guān)鍵字

image.png

Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言,這就意味著消息傳遞和類(lèi)以及對(duì)象的創(chuàng)建都在運(yùn)行時(shí)完成,這個(gè)核心的庫(kù)是由 C\C++ 和匯編編寫(xiě)的,保證其系統(tǒng)運(yùn)行的高效性。

isa

這個(gè)老朋友我們見(jiàn)了無(wú)數(shù)次了,在 arm64 架構(gòu)之前,isa 僅僅是一個(gè)普通的指針,存儲(chǔ) Class、Meta-Class 對(duì)象的地址。
在 arm64 后,isa 變成了聯(lián)合體(union)類(lèi)型。這個(gè)類(lèi)型可以像 struct 那樣存儲(chǔ)更多的信息。

我們可在 objc 源碼中看到 isa 的結(jié)構(gòu)并非是 Class 類(lèi)型而是聯(lián)合體:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

ISA_BITFIELD定義是這樣的:

# define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 19

這種表現(xiàn)形式是位域。

存儲(chǔ)的某些信息是不需要一個(gè)完整的字節(jié)的,僅僅需要 1 個(gè)或幾個(gè)二進(jìn)制位,就可以通過(guò)位域來(lái)存儲(chǔ)。位域的形式為:類(lèi)型說(shuō)明符(int、unsigned int 或 signed int)位域名: 位域長(zhǎng)度,如:int a: 8;

位域中的字段

通過(guò)位域來(lái)存儲(chǔ)更豐富的信息,正是蘋(píng)果對(duì)內(nèi)存優(yōu)化的體現(xiàn),上節(jié)中位域列表的各個(gè)字段的含義為:

nonpointer:0 表示普通指針,存儲(chǔ)類(lèi)對(duì)象及元類(lèi)對(duì)象的地址,1 表示優(yōu)化后的指針,通過(guò)位域列表存儲(chǔ)更多信息。

has_assoc:是否設(shè)置過(guò)關(guān)聯(lián)對(duì)象,若沒(méi)有,則 release 時(shí)更快。

has_cxx_dtor:是否有 C++ 的析構(gòu)函數(shù),若沒(méi)有,release 時(shí)更快。

shiftcls:存儲(chǔ)類(lèi)對(duì)象和元類(lèi)對(duì)象的內(nèi)存地址。

magic:用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化。

weakly_referenced:是否被若引用指向。

deallocating:對(duì)象是否正在釋放。

extra_rc:存儲(chǔ)的值為引用計(jì)數(shù)器減 1。

has_sidetable_rc:引用計(jì)數(shù)器是否過(guò)大無(wú)法存儲(chǔ)在 isa 中,若為 1,那么引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫 SideTable 的類(lèi)的屬性中。

做個(gè)簡(jiǎn)單的驗(yàn)證,假如有 Test 類(lèi),無(wú)屬性,在另一個(gè)類(lèi)中使用它:

Test* t = [[Test alloc] init];
NSLog(@"%@", t);

在第二句加斷點(diǎn),進(jìn)入 LLDB 調(diào)試環(huán)境借助命令:
print/x t->isa
得到打印:

(Class) $0 = 0x000001a10000cdc1 Test

將該地址復(fù)制到系統(tǒng)計(jì)算器中:


image

最后一位為 1 說(shuō)明 nonpointer 位為 1,說(shuō)明該 isa 指針是 arm64 優(yōu)化過(guò)后的指針,存儲(chǔ)了更多信息。

倒數(shù)第二位為 0,說(shuō)明 has_assoc 位為 0,說(shuō)明該類(lèi)未設(shè)置關(guān)聯(lián)對(duì)象,例子中我沒(méi)有給 Test 類(lèi)設(shè)置關(guān)聯(lián)對(duì)象。

倒數(shù)第三位為 0,說(shuō)明 has_cxx_dtor 位為 0,說(shuō)明該類(lèi)沒(méi)有析構(gòu)函數(shù)。(析構(gòu)函數(shù)類(lèi)似 dealloc 函數(shù))

接下來(lái)的 33 位,如圖:


image.png

表示字段 shiftcls,存放著類(lèi)對(duì)象地址或者元類(lèi)對(duì)象的值。

接下來(lái)的 6 位 01 1010 表示字段 magic,表示對(duì)象已經(jīng)初始化成功,執(zhí)行完 allocinit 后它的值為 1a,在源碼中也有體現(xiàn):

#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

接下來(lái)的一位為 0,為 weakly_referenced 位,表示該對(duì)象未被弱引用指向過(guò)。

接下來(lái)一位為 0,為 deallocating 位,表示該對(duì)象沒(méi)有正在被釋放。

接下來(lái)一位為 0,為 has_sidetable_rc 位,表示引用計(jì)數(shù)存儲(chǔ)在后 19 位,若引用計(jì)數(shù)并沒(méi)有存在后 19 位的時(shí)候該位為 1.

最后十九位為 0,為 extra_rc 位,用來(lái)存放引用計(jì)數(shù) - 1。所以都是 0。

Objective-C 對(duì)象的分類(lèi)以及 isa、superclass 指針 中提到,在 arm64 架構(gòu)下,isa 需要和 ISA_MASK 位運(yùn)算一次才能得到真正的類(lèi)對(duì)象或者元類(lèi)對(duì)象地址,正是因?yàn)?isa 優(yōu)化后存儲(chǔ)了更多的信息,只有中間的 33 位是類(lèi)對(duì)象或者元類(lèi)對(duì)象地址,所以需要對(duì) ISA_MASK 進(jìn)行一次位運(yùn)算。

Class

Objective-C 中類(lèi)對(duì)象和元類(lèi)對(duì)象都能用 Class 表示,或者通俗點(diǎn)說(shuō),元類(lèi)對(duì)象是特殊的類(lèi)對(duì)象。在底層為 objc_class。

在 objc 源碼中可看到 objc_class 的結(jié)構(gòu):

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;    
    class_data_bits_t bits;    
}

objc_object 中有:

Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

所以可簡(jiǎn)化為:

struct objc_class : objc_object {
    Class isa;
    Class superclass;
    cache_t cache;  // 方法緩存
    class_data_bits_t bits;  // 用于獲取具體類(lèi)信息 
    ...
}

其中 bits 和 FAST_DATA_MASK 進(jìn)行 & 運(yùn)算可得到 class_rw_t,class_rw_t 的結(jié)構(gòu)為:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; // 方法列表
    property_array_t properties; // 屬性列表
    protocol_array_t protocols; // 協(xié)議列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
    ...
}

其中 class_ro_t 是一個(gè)只讀的結(jié)構(gòu)體:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; // 實(shí)例對(duì)象占用的內(nèi)存空間
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; // 類(lèi)名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成員變量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    ...
};

為研究 Class 里面的結(jié)構(gòu),我們可自己實(shí)現(xiàn) Class 的底層機(jī)制,包括 class_ro_t、class_rw_t、緩存列表、協(xié)議列表等等等,篇幅過(guò)長(zhǎng)不貼出代碼。接下來(lái)的例子中將使用這份代碼進(jìn)行轉(zhuǎn)換。

和 objc 源碼不同的是,方法列表、屬性列表、協(xié)議列表這些二維數(shù)組的成員用了一維數(shù)組代替。

class_rw_t

class_rw_t 中里面的方法列表、屬性列表、協(xié)議列表都是二維數(shù)組,并且是可讀可寫(xiě)的,包含了本類(lèi)和分類(lèi)中的內(nèi)容。

方法列表的二維數(shù)組,同理屬性和協(xié)議列表的二維數(shù)組:


image.png

這樣可以動(dòng)態(tài)增加方法或者修改方法,并且二維數(shù)組的每個(gè)方法列表都有可能是一個(gè)分類(lèi)的方法列表。

class_ro_t

class_ro_t 中的 baseMethodListbaseProtocols、ivars、baseProperties 是一維數(shù)組的,只讀,包含了類(lèi)的初始內(nèi)容。
也就是說(shuō)本類(lèi)的協(xié)議、屬性、方法等信息在這個(gè)一維數(shù)組里面。

image.png

這份不變的 baseMethodList 和 class_rw_t 中最后一個(gè)元素是一樣的,在 runtime 初始化的過(guò)程中,會(huì)根據(jù)類(lèi)的初始信息來(lái)創(chuàng)建 class_rw_t 的成員:

static Class realizeClass(Class cls)
{
    ...
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }
    ...
}

method_t

method_t 是對(duì)方法/函數(shù)的封裝,也是個(gè)結(jié)構(gòu)體:

struct method_t {
    SEL name; // 函數(shù)名
    const char *types; // 編碼(返回值類(lèi)型、參數(shù)類(lèi)型)
    MethodListIMP imp; // 指向函數(shù)的指針(函數(shù)地址)
};

IMP 代表函數(shù)的具體實(shí)現(xiàn):

using MethodListIMP = IMP;

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

SEL 代表方法/函數(shù)名,一般叫做選擇器,底層和 char* 類(lèi)似,可以通過(guò) @selector()sel_registerName() 獲得。可以通過(guò) sel_getName()NSStringFromSelector() 轉(zhuǎn)成字符串。

那么可得知,不同類(lèi)中相同名字的方法,所對(duì)應(yīng)的方法選擇器是相同的。

我們?cè)?Test 類(lèi)中添加實(shí)例方法 test():

- (void)test {
    NSLog(@"%s", __func__); //加斷點(diǎn)
}

然后運(yùn)行:

Test* t = [[Test alloc] init];
v_objc_class* tCls = (__bridge v_objc_class*)[Test class];
class_rw_t* data = tCls->data();
[t test]; // 加斷點(diǎn)

進(jìn)入調(diào)試環(huán)境看到 data 中的 test() 信息:


image.png

打印得:

Printing description of data->methods->first.imp:
(IMP) imp = 0x00000001002ce654 (Test_3`-[Test test] at Test.m:13)

來(lái)到第二個(gè)斷點(diǎn) Debug->Debug Workflow->Always Show Disassembly

image.png

發(fā)現(xiàn)畫(huà)圈部分就是這個(gè)函數(shù)的起始地址:0x00000001002ce654

types

types 包含了函數(shù)的返回值、參數(shù)編碼的字符串:

返回值 參數(shù)1 參數(shù)2 ... 參數(shù)n

在上節(jié)的調(diào)試環(huán)境 data 信息截圖可看到 types 是:

v16@0:8

這樣的形式,其中

解釋
v 代表返回值是 void
16(第一個(gè)數(shù)字) 表示所有參數(shù)所占字節(jié)數(shù)
@ 第一個(gè)參數(shù),id 類(lèi)型
0 表示第一個(gè)參數(shù)(id)從 0 開(kāi)始
: 代表 SEL
8 表示 SEL 從 8 開(kāi)始

以上就是 objc 通過(guò)字符串來(lái)描述一個(gè)函數(shù)的返回值及參數(shù)信息。

Type Encoding

iOS 中提供了一個(gè)叫 @encode 的指令,可以將具體的類(lèi)型表示成字符串編碼,如打?。?/p>

NSLog(@"%s", @encode(int));
NSLog(@"%s", @encode(NSString));
NSLog(@"%s", @encode(id));
NSLog(@"%s", @encode(void));

結(jié)果:

i
{NSString=#}
@
v

完整的編碼表:

編碼 釋義
c A char
i An int
s A short
l A longl is treated as a 32-bit quantity on 64-bit programs
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

方法緩存

在 objc_class 的結(jié)構(gòu)體中,cache_t 類(lèi)型的 cache 成員是用來(lái)緩存方法的,它通過(guò)哈希表來(lái)緩存曾經(jīng)調(diào)用過(guò)的方法,可以提高查找速度。

Objective-C 對(duì)象的分類(lèi)以及 isa、superclass 指針一文中,得知實(shí)例方法或者類(lèi)方法都是通過(guò) isa 指針找到類(lèi)對(duì)象或者元類(lèi)對(duì)象的方法列表,遍歷,有則調(diào)用,沒(méi)有則通過(guò) superclass 指針在父類(lèi)中找方法列表,遍歷,有則調(diào)用,沒(méi)有則繼續(xù)向上找... 若一個(gè)函數(shù)調(diào)用很多次,造成的開(kāi)銷(xiāo)是很大的,所以在函數(shù)第一次調(diào)用的時(shí)候,會(huì)緩存到 cache 中,這樣就不用每次都層層尋找而是從哈希表中取出直接調(diào)用。

cache_t 的結(jié)構(gòu)為:

struct cache_t {
    struct bucket_t *_buckets; // 哈希表
    mask_t _mask; // 哈希表長(zhǎng)度 - 1
    mask_t _occupied; // 已經(jīng)緩存的方法數(shù)量
}

bucket_t 是一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)為:

struct bucket_t {
    cache_key_t _key; // SEL 作為 key
    MethodCacheIMP _imp; // 函數(shù)內(nèi)存地址
}

緩存方法查找原理

這里有個(gè)很高效的算法:目標(biāo)函數(shù)和 _mask 進(jìn)行 & 運(yùn)算可以直接得到目標(biāo)索引,憑借目標(biāo)索引直接在哈希表中取函數(shù)地址進(jìn)行調(diào)用。

image

該索引在 test() 方法放入哈希表的時(shí)候就已經(jīng)確定。
當(dāng)然存在這種情況,假如哈希表數(shù)組為 0,而 @selector(test) & _mask 結(jié)果為 3,則情況為:
image.png

也就是說(shuō),其他位都成了預(yù)留位置且都是 NULL,這樣的做法雖然高效,但卻是以犧牲內(nèi)存空間為代價(jià)的。
而且可以發(fā)現(xiàn),地址 & _mask 的結(jié)果是小于等于 _mask 的。

那么假如兩個(gè)方法地址 & _mask 生成的索引是一樣的該怎么辦?
源碼(objc-cache.mm)中有處理:

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); // key 為 @selector(test),m 為 _mask
    mask_t i = begin;
    do {
        // 找到索引,返回調(diào)用(IMP)
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
        // 若不相等,則使用 cache_next() 方法
    } 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);
}

cache_hash 方法:

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask); // 得到索引的 & 運(yùn)算
}

cache_next() 方法(arm64 架構(gòu)):

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask; // 判斷結(jié)果是否為 0
}

緩存方法的時(shí)候:


image.png

若 new() 函數(shù)的目標(biāo)索引已經(jīng)有值,則在目標(biāo)索引 -1 的位置緩存,若還有值,則繼續(xù)減 1,當(dāng)結(jié)果為 0 的時(shí)候,則取 _mask 值即哈希表長(zhǎng)度 - 1。

當(dāng)緩存進(jìn)來(lái)一個(gè)方法后緩存方法數(shù)大于 _mask 值后會(huì)調(diào)用 expand() 方法對(duì) _buckets 進(jìn)行擴(kuò)容,然后調(diào)用 reallocate() 方法清空緩存。

并不是每次緩存方法 _mask 都會(huì)變,而是一開(kāi)始就開(kāi)辟容量為 n 的哈希表,不夠用的時(shí)候則再開(kāi)辟容量為 2 倍的哈希表,以此類(lèi)推,如 10,20,40,80,160 ...

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    ...
    reallocate(oldCapacity, newCapacity);
}

我們用代碼驗(yàn)證如上過(guò)程:
首先新建 Human 類(lèi),有 run 方法,新建 Singer 類(lèi)繼承 Human 類(lèi), 有 sing 方法,新建 BoA 類(lèi)繼承自 Singer 類(lèi),有 dance 方法。

BoA* boa = [[BoA alloc] init];
v_objc_class* boaCls = (__bridge v_objc_class*)[BoA class];
        
[boa run]; //加斷點(diǎn)
[boa sing]; //加斷點(diǎn)
[boa dance]; //加斷點(diǎn)
NSLog(@"=====end===="); //加斷點(diǎn)

運(yùn)行來(lái)到第一個(gè)斷點(diǎn):


image.png

發(fā)現(xiàn)哈希表容量為 4(_mask + 1),此時(shí) _occupied 為 1,緩存的可能是 init 方法。
來(lái)到第二個(gè)斷點(diǎn):


image.png

_occupied 為 2,已緩存 run 方法。
來(lái)到第三個(gè)斷點(diǎn):


image.png

_occupied 為 3,已緩存 sing 方法。
來(lái)到第四個(gè)斷點(diǎn):


image.png

_occupied 為 1,并且哈希表已經(jīng)擴(kuò)容,容量為 8。舊的緩存內(nèi)容全部清空,這個(gè) 1 是緩存的 dance 方法。

objc_msgSend

首先我們將下面的代碼轉(zhuǎn)成 C++ 代碼:

BoA* boa = [[BoA alloc] init];
[boa dance];

得到:

BoA* boa = ((BoA *(*)(id, SEL))(void *)objc_msgSend)((id)((BoA *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("BoA"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)boa, sel_registerName("dance"));

[boa dance] 簡(jiǎn)化版后得:

objc_msgSend(boa, sel_registerName("dance"));

這就是我們最熟悉的消息機(jī)制:objc_msgSend 方法。

第二個(gè)參數(shù)為:傳遞一個(gè) C 語(yǔ)言字符串,返回一個(gè) SEL。實(shí)際等價(jià)于 @selector(dance)。

Obejective-C 中的方法調(diào)用,最終都轉(zhuǎn)換成 objc_msgSend 函數(shù)的調(diào)用。
objc_msgSend 的執(zhí)行流程可分為 3 個(gè)階段:

  • 消息發(fā)送
  • 動(dòng)態(tài)方法解析
  • 消息轉(zhuǎn)發(fā)

在執(zhí)行 objc_msgSend 方法的時(shí)候,會(huì)對(duì)給接收者(Receiver)發(fā)送消息,例子中的接收者是對(duì)象 boa,在該階段會(huì)嘗試查找方法進(jìn)行調(diào)用,若能找到,就不會(huì)進(jìn)入動(dòng)態(tài)解析階段,否則則進(jìn)入動(dòng)態(tài)解析階段,該階段允許動(dòng)態(tài)創(chuàng)建新方法,若動(dòng)態(tài)解析階段未做任何操作,則進(jìn)入消息轉(zhuǎn)發(fā)階段,轉(zhuǎn)發(fā)給另外一個(gè)對(duì)象來(lái)調(diào)用,若未找到合適的對(duì)象調(diào)用,則會(huì)報(bào)經(jīng)典的方法找不到的錯(cuò)誤:

unrecognized selector sent to instance xxx.

objc_msgSend 源碼解讀

我們可在 objc-msg-arm64.s 中看到 objc_msgSend 方法的匯編源碼。
看到:

ENTRY _objc_msgSend

ENTRY 是一個(gè)宏,它的定義:

.macro ENTRY /* name */
    .text
    .align 5
    .globl    $0
$0:
.endmacro

_objc_msgSend 結(jié)束調(diào)用為:

END_ENTRY _objc_msgSend

中間的部分都是它的實(shí)現(xiàn),這段代碼內(nèi)部做了什么?
首先看到:

cmp p0 #0
b.le    LNilOrTagged

該句表示若 p0 小于等于 0 的話(huà) 跳轉(zhuǎn)到 LNilOrTagged 代碼塊。并且這里的 p0 是 objc_msgSend 的第一個(gè)參數(shù),為上述例子中的 boa。

b 為匯編中的跳轉(zhuǎn)指令。le 是小于等于的意思。p0 為寄存器,里面存放的是消息接收者。

在 LNilOrTagged 中看到:

b.eq    LReturnZero 

LReturnZero 中看到:

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

ret 為 return 關(guān)鍵字。

那么該段的意思很明確:若消息接收者為 nil,則退出 objc_msgSend 函數(shù)。
若消息接收者不為空,則會(huì)來(lái)到:

LGetIsaDone:
    CacheLookup NORMAL

這句就是方法緩存查找,CacheLookup 也是一個(gè)宏:

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0 // call or return imp
    ...
    ...
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
  ...   
.endmacro

注釋很明顯在表明:該處在計(jì)算索引,然后根據(jù)索引去方法緩存中查找方法。其中:

CacheHit $0 

為查找到方法,直接調(diào)用或者返回 IMP。
沒(méi)有查找到則:

CheckMiss $0

CheckMiss 同樣為一個(gè)宏:

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

由于上面?zhèn)鬟f的參數(shù)為 NORMAL,那么我們也只關(guān)注 NORMAL 的部分,即調(diào)用 __objc_msgSend_uncached 方法。該方法內(nèi)部會(huì)調(diào)用 MethodTableLookup,說(shuō)明未在緩存中找到方法則去其他地方查找方法,該方法內(nèi)部:

bl  __class_lookupMethodAndLoadCache3

bl 為跳轉(zhuǎn)調(diào)用的指令。

該方法為 C 語(yǔ)言函數(shù),內(nèi)部調(diào)用 lookUpImpOrForward

lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);

obj 為消息接收者,核心的代碼就是 lookUpImpOrForward 方法,核心邏輯為:

...
retry:    
    runtimeLock.assertLocked();
    imp = cache_getImp(cls, sel); // 在執(zhí)行該句之前可能動(dòng)態(tài)添加一些方法,所以需要再檢查一次緩存
    if (imp) goto done; // 若找到了,返回 IMP
    {
        // 未找到,來(lái)到這里
        Method meth = getMethodNoSuper_nolock(cls, sel); 
        if (meth) {
            // 找到方法后緩存該方法
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 返回 IMP
            imp = meth->imp;
            goto done;
        }
    }
    // 若還沒(méi)有找到,則去父類(lèi)的方法緩存里去查找
    {
        unsigned attempts = unreasonableClassCount();
        // for 循環(huán)為一層一層向父類(lèi)查找
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            ...
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 若查找到方法,則緩存到本類(lèi)當(dāng)中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    ...
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
...

getMethodNoSuper_nolock 為便利 class_rw_t 中的方法列表:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    ...
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

search_method_list() 方法中是分條件查找,一個(gè)是查找排好序的方法列表,一個(gè)是查找未排序的方法列表,findMethodInSortedMethodList() 為在已經(jīng)排序的方法列表中查找,其內(nèi)部是二分查找。另一個(gè)則是普通遍歷查找。
最終,消息發(fā)送的流程為:

image.png

在 lookUpImpOrForward 的內(nèi)部邏輯中,若如何都沒(méi)有找到方法,會(huì)嘗試動(dòng)態(tài)解析

if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);// 動(dòng)態(tài)解析
        runtimeLock.lock();
        // 標(biāo)記是否解析過(guò),置為 YES
        triedResolver = YES;
        goto retry;
}

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

_class_resolveMethod() 方法中:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { // 判斷是否為元類(lèi)
        
        _class_resolveInstanceMethod(cls, sel, inst);// 內(nèi)部是調(diào)用 objc_msgSend 方法
    } 
    else {
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

_class_resolveInstanceMethod 可以動(dòng)態(tài)的添加方法,我們模擬一下動(dòng)態(tài)解析的過(guò)程,我們首先在 BoA.h 中添加函數(shù)聲明:

- (void)playGolf;

不實(shí)現(xiàn),在外部 [boa playGolf] 的時(shí)候會(huì)報(bào):

'NSInvalidArgumentException', reason: '-[BoA playGolf]: unrecognized selector sent to instance 0x2811b8170'

然后重寫(xiě) resolveInstanceMethod 方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(playGolf)) {
        Method method = class_getInstanceMethod(self, @selector(play));
        IMP imp = method_getImplementation(method);
        class_addMethod(self, sel, imp,  "v@:");
    }
    return YES;
}

play 方法:

- (void)play {
    NSLog(@"Play Golf!!!");
}

再次運(yùn)行 [boa playGolf] 則會(huì)打印:

Play Golf!!!

該函數(shù)就是在運(yùn)行時(shí)動(dòng)態(tài)添加的,而非編譯時(shí)期添加的。并且調(diào)用成功后 triedResolver 置為 YES,并且放到 cache 中,下次再調(diào)用則直接走消息轉(zhuǎn)發(fā)的流程。

image.png

若消息發(fā)送和動(dòng)態(tài)方法解析階段都沒(méi)有找到方法的實(shí)現(xiàn),則會(huì)進(jìn)入到最后的階段:消息轉(zhuǎn)發(fā)。

進(jìn)入消息轉(zhuǎn)發(fā)階段,底層會(huì)調(diào)用 ___forwarding___ 函數(shù),這個(gè)函數(shù)會(huì)調(diào)用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,我們可以在該方法內(nèi)讓別的對(duì)象來(lái)調(diào)用 playGolf 函數(shù):

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(playGolf)) {
        return [[Valenti alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

Valenti 類(lèi)中聲明并實(shí)現(xiàn)了 playGolf 的方法:

-(void)playGolf {
    NSLog(@"Valenti plays golf!!!");
}

運(yùn)行結(jié)果:

Valenti plays golf!!!

若未實(shí)現(xiàn) forwardingTargetForSelector 方法,則會(huì)調(diào)用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,該方法要求返回一個(gè)方法簽名,然后執(zhí)行 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(playGolf)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // anInvocation 原方法接收者為 boa 對(duì)象,在這里改成了 Valenti 的對(duì)象
    [anInvocation invokeWithTarget:[[Valenti alloc] init]];
}

NSInvocation 中封裝了函數(shù)的調(diào)用,參數(shù),以及方法調(diào)用者。這些信息是由方法簽名決定的。

消息轉(zhuǎn)發(fā)的流程為:


image

例子中的方法都是誤無(wú)參且無(wú)返回值的,那么有參有返回值的又是什么形式:
假如有 release 方法,該方法是打印「發(fā)布了多少?gòu)垖?zhuān)輯」,需要傳入一個(gè) count 的參數(shù)決定多少?gòu)?,BoA 聲明未實(shí)現(xiàn)該方法, Valenti 中聲明且實(shí)現(xiàn)了該方法:

- (BOOL)release:(int)count {
    NSLog(@"Release %d albums!", count);
    return count == 0 ? NO : YES;
}

則在消息轉(zhuǎn)發(fā)階段:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(release:)) {
        // 只有函數(shù)類(lèi)型的不同
        return [NSMethodSignature signatureWithObjCTypes:"B@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[[Valenti alloc] init]];
}

外部調(diào)用 [boa release: 5] 運(yùn)行打印:

Release 5 albums!

我們可以在 forwardInvocation 方法中得到 anInvocation 的返回值和參數(shù)信息:

int param;
[anInvocation getArgument:&param atIndex:2];
    
BOOL ret;
[anInvocation getReturnValue:&ret];
NSLog(@"%d %d",param, ret);

打印結(jié)果為 5, 1。

[anInvocation getArgument:&param atIndex:2] 為什么 index 為 2?,因?yàn)閰?shù)順序?yàn)椋簉eceiver、selector 其次才是其他參數(shù)。

以上便是消息機(jī)制的所有內(nèi)容。

super 關(guān)鍵字

理解 super 關(guān)鍵字,還需要借助上面 BoA 的繼承鏈:BoA 繼承 Singer 繼承 Human。
然后在 BoA 的 init() 方法中:

- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"[super class] %@", [super class]);
        NSLog(@"[super superclass] %@", [super superclass]);
    }
    return self;
}

結(jié)果為:

[super class] BoA
[super superclass] Singer

是不是和猜想有點(diǎn)出入?明明是 super 指針,打印的卻是本類(lèi)以及本類(lèi)的父類(lèi)。

super 關(guān)鍵字底層執(zhí)行的是 objc_msgSendSuper 方法。該方法傳入兩個(gè)參數(shù),一個(gè)是 objc_super 的結(jié)構(gòu)體,源碼中的結(jié)構(gòu)體形式為:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver; // 消息接收者,BoA 對(duì)象

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

第二個(gè)參數(shù)為 SEL。
轉(zhuǎn)成 C++ 代碼后,我們發(fā)現(xiàn)傳入的 objc_super 類(lèi)型的參數(shù)第一個(gè)成員初始化結(jié)果為 self,第二個(gè)為 class_getSuperclass(objc_getClass("BoA")) 也就是 Singer 類(lèi)。

從 objc_super 的結(jié)構(gòu)可以知道,雖然調(diào)用的是 super,但是實(shí)際的消息接收者仍然是 BoA 對(duì)象。那么傳入的父類(lèi)作用是什么?是告訴從哪里開(kāi)始找方法,也就是說(shuō)是從父類(lèi)中找class/superclass 方法,但接收者仍然是本類(lèi)的對(duì)象。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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