iOS - Runtime 中 Class酝陈、消息機制床玻、super 關鍵字

image.png

Objective-C 是一門動態(tài)語言,這就意味著消息傳遞和類以及對象的創(chuàng)建都在運行時完成沉帮,這個核心的庫是由 C\C++ 和匯編編寫的锈死,保證其系統(tǒng)運行的高效性。

isa

這個老朋友我們見了無數(shù)次了穆壕,在 arm64 架構之前待牵,isa 僅僅是一個普通的指針,存儲 Class喇勋、Meta-Class 對象的地址缨该。
在 arm64 后,isa 變成了聯(lián)合體(union)類型川背。這個類型可以像 struct 那樣存儲更多的信息贰拿。

我們可在 objc 源碼中看到 isa 的結構并非是 Class 類型而是聯(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)形式是位域。

存儲的某些信息是不需要一個完整的字節(jié)的熄云,僅僅需要 1 個或幾個二進制位膨更,就可以通過位域來存儲。位域的形式為:類型說明符(int缴允、unsigned int 或 signed int)位域名: 位域長度荚守,如:int a: 8;

位域中的字段

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

nonpointer:0 表示普通指針矗漾,存儲類對象及元類對象的地址锈候,1 表示優(yōu)化后的指針,通過位域列表存儲更多信息缩功。

has_assoc:是否設置過關聯(lián)對象晴及,若沒有都办,則 release 時更快嫡锌。

has_cxx_dtor:是否有 C++ 的析構函數(shù),若沒有琳钉,release 時更快势木。

shiftcls:存儲類對象和元類對象的內(nèi)存地址。

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

weakly_referenced:是否被若引用指向啦桌。

deallocating:對象是否正在釋放。

extra_rc:存儲的值為引用計數(shù)器減 1及皂。

has_sidetable_rc:引用計數(shù)器是否過大無法存儲在 isa 中甫男,若為 1,那么引用計數(shù)會存儲在一個叫 SideTable 的類的屬性中验烧。

做個簡單的驗證板驳,假如有 Test 類,無屬性碍拆,在另一個類中使用它:

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

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

(Class) $0 = 0x000001a10000cdc1 Test

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


image

最后一位為 1 說明 nonpointer 位為 1感混,說明該 isa 指針是 arm64 優(yōu)化過后的指針端幼,存儲了更多信息。

倒數(shù)第二位為 0弧满,說明 has_assoc 位為 0婆跑,說明該類未設置關聯(lián)對象,例子中我沒有給 Test 類設置關聯(lián)對象庭呜。

倒數(shù)第三位為 0洽蛀,說明 has_cxx_dtor 位為 0,說明該類沒有析構函數(shù)疟赊。(析構函數(shù)類似 dealloc 函數(shù))

接下來的 33 位郊供,如圖:


image.png

表示字段 shiftcls,存放著類對象地址或者元類對象的值近哟。

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

#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

接下來的一位為 0疯淫,為 weakly_referenced 位地来,表示該對象未被弱引用指向過。

接下來一位為 0熙掺,為 deallocating 位未斑,表示該對象沒有正在被釋放。

接下來一位為 0币绩,為 has_sidetable_rc 位蜡秽,表示引用計數(shù)存儲在后 19 位,若引用計數(shù)并沒有存在后 19 位的時候該位為 1.

最后十九位為 0缆镣,為 extra_rc 位芽突,用來存放引用計數(shù) - 1。所以都是 0董瞻。

Objective-C 對象的分類以及 isa寞蚌、superclass 指針 中提到,在 arm64 架構下钠糊,isa 需要和 ISA_MASK 位運算一次才能得到真正的類對象或者元類對象地址挟秤,正是因為 isa 優(yōu)化后存儲了更多的信息,只有中間的 33 位是類對象或者元類對象地址抄伍,所以需要對 ISA_MASK 進行一次位運算艘刚。

Class

Objective-C 中類對象和元類對象都能用 Class 表示,或者通俗點說逝慧,元類對象是特殊的類對象昔脯。在底層為 objc_class。

在 objc 源碼中可看到 objc_class 的結構:

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;

所以可簡化為:

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

其中 bits 和 FAST_DATA_MASK 進行 & 運算可得到 class_rw_t笛臣,class_rw_t 的結構為:

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 是一個只讀的結構體:

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

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

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

為研究 Class 里面的結構云稚,我們可自己實現(xiàn) Class 的底層機制,包括 class_ro_t沈堡、class_rw_t静陈、緩存列表、協(xié)議列表等等等诞丽,篇幅過長不貼出代碼鲸拥。接下來的例子中將使用這份代碼進行轉(zhuǎn)換。

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

class_rw_t

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

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


image.png

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

class_ro_t

class_ro_t 中的 baseMethodListbaseProtocols毛肋、ivars怨咪、baseProperties 是一維數(shù)組的,只讀村生,包含了類的初始內(nèi)容惊暴。
也就是說本類的協(xié)議饼丘、屬性趁桃、方法等信息在這個一維數(shù)組里面。

image.png

這份不變的 baseMethodList 和 class_rw_t 中最后一個元素是一樣的肄鸽,在 runtime 初始化的過程中卫病,會根據(jù)類的初始信息來創(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 是對方法/函數(shù)的封裝,也是個結構體:

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

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

using MethodListIMP = IMP;

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

SEL 代表方法/函數(shù)名蟀苛,一般叫做選擇器,底層和 char* 類似逮诲,可以通過 @selector()sel_registerName() 獲得帜平。可以通過 sel_getName()NSStringFromSelector() 轉(zhuǎn)成字符串梅鹦。

那么可得知裆甩,不同類中相同名字的方法,所對應的方法選擇器是相同的齐唆。

我們在 Test 類中添加實例方法 test():

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

然后運行:

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

進入調(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)

來到第二個斷點 Debug->Debug Workflow->Always Show Disassembly

image.png

發(fā)現(xiàn)畫圈部分就是這個函數(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(第一個數(shù)字) 表示所有參數(shù)所占字節(jié)數(shù)
@ 第一個參數(shù)箍邮,id 類型
0 表示第一個參數(shù)(id)從 0 開始
: 代表 SEL
8 表示 SEL 從 8 開始

以上就是 objc 通過字符串來描述一個函數(shù)的返回值及參數(shù)信息茉帅。

Type Encoding

iOS 中提供了一個叫 @encode 的指令,可以將具體的類型表示成字符串編碼锭弊,如打涌芭臁:

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

結果:

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 的結構體中,cache_t 類型的 cache 成員是用來緩存方法的味滞,它通過哈希表來緩存曾經(jīng)調(diào)用過的方法樱蛤,可以提高查找速度马昙。

Objective-C 對象的分類以及 isa、superclass 指針一文中刹悴,得知實例方法或者類方法都是通過 isa 指針找到類對象或者元類對象的方法列表行楞,遍歷,有則調(diào)用土匀,沒有則通過 superclass 指針在父類中找方法列表子房,遍歷,有則調(diào)用就轧,沒有則繼續(xù)向上找... 若一個函數(shù)調(diào)用很多次证杭,造成的開銷是很大的,所以在函數(shù)第一次調(diào)用的時候妒御,會緩存到 cache 中解愤,這樣就不用每次都層層尋找而是從哈希表中取出直接調(diào)用。

cache_t 的結構為:

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

bucket_t 是一個結構體乎莉,結構為:

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

緩存方法查找原理

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

image

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

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

那么假如兩個方法地址 & _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); // 得到索引的 & 運算
}

cache_next() 方法(arm64 架構):

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

緩存方法的時候:


image.png

若 new() 函數(shù)的目標索引已經(jīng)有值,則在目標索引 -1 的位置緩存垂涯,若還有值烁焙,則繼續(xù)減 1,當結果為 0 的時候耕赘,則取 _mask 值即哈希表長度 - 1骄蝇。

當緩存進來一個方法后緩存方法數(shù)大于 _mask 值后會調(diào)用 expand() 方法對 _buckets 進行擴容,然后調(diào)用 reallocate() 方法清空緩存操骡。

并不是每次緩存方法 _mask 都會變九火,而是一開始就開辟容量為 n 的哈希表赚窃,不夠用的時候則再開辟容量為 2 倍的哈希表,以此類推岔激,如 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);
}

我們用代碼驗證如上過程:
首先新建 Human 類,有 run 方法炫彩,新建 Singer 類繼承 Human 類匾七, 有 sing 方法,新建 BoA 類繼承自 Singer 類江兢,有 dance 方法昨忆。

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

運行來到第一個斷點:


image.png

發(fā)現(xiàn)哈希表容量為 4(_mask + 1),此時 _occupied 為 1杉允,緩存的可能是 init 方法邑贴。
來到第二個斷點:


image.png

_occupied 為 2,已緩存 run 方法夺颤。
來到第三個斷點:


image.png

_occupied 為 3痢缎,已緩存 sing 方法胁勺。
來到第四個斷點:


image.png

_occupied 為 1世澜,并且哈希表已經(jīng)擴容,容量為 8署穗。舊的緩存內(nèi)容全部清空寥裂,這個 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] 簡化版后得:

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

這就是我們最熟悉的消息機制:objc_msgSend 方法案疲。

第二個參數(shù)為:傳遞一個 C 語言字符串封恰,返回一個 SEL。實際等價于 @selector(dance)褐啡。

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

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

在執(zhí)行 objc_msgSend 方法的時候备畦,會對給接收者(Receiver)發(fā)送消息低飒,例子中的接收者是對象 boa,在該階段會嘗試查找方法進行調(diào)用懂盐,若能找到褥赊,就不會進入動態(tài)解析階段,否則則進入動態(tài)解析階段莉恼,該階段允許動態(tài)創(chuàng)建新方法拌喉,若動態(tài)解析階段未做任何操作速那,則進入消息轉(zhuǎn)發(fā)階段,轉(zhuǎn)發(fā)給另外一個對象來調(diào)用尿背,若未找到合適的對象調(diào)用端仰,則會報經(jīng)典的方法找不到的錯誤:

unrecognized selector sent to instance xxx.

objc_msgSend 源碼解讀

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

ENTRY _objc_msgSend

ENTRY 是一個宏田藐,它的定義:

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

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

END_ENTRY _objc_msgSend

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

cmp p0 #0
b.le    LNilOrTagged

該句表示若 p0 小于等于 0 的話 跳轉(zhuǎn)到 LNilOrTagged 代碼塊坞淮。并且這里的 p0 是 objc_msgSend 的第一個參數(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 關鍵字烁涌。

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

LGetIsaDone:
    CacheLookup NORMAL

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

.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ù)索引去方法緩存中查找方法抒钱。其中:

CacheHit $0 

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

CheckMiss $0

CheckMiss 同樣為一個宏:

.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谋币,那么我們也只關注 NORMAL 的部分,即調(diào)用 __objc_msgSend_uncached 方法症概。該方法內(nèi)部會調(diào)用 MethodTableLookup蕾额,說明未在緩存中找到方法則去其他地方查找方法,該方法內(nèi)部:

bl  __class_lookupMethodAndLoadCache3

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

該方法為 C 語言函數(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í)行該句之前可能動態(tài)添加一些方法调炬,所以需要再檢查一次緩存
    if (imp) goto done; // 若找到了,返回 IMP
    {
        // 未找到司抱,來到這里
        Method meth = getMethodNoSuper_nolock(cls, sel); 
        if (meth) {
            // 找到方法后緩存該方法
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            // 返回 IMP
            imp = meth->imp;
            goto done;
        }
    }
    // 若還沒有找到筐眷,則去父類的方法緩存里去查找
    {
        unsigned attempts = unreasonableClassCount();
        // for 循環(huán)為一層一層向父類查找
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            ...
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 若查找到方法,則緩存到本類當中
                    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() 方法中是分條件查找习柠,一個是查找排好序的方法列表匀谣,一個是查找未排序的方法列表照棋,findMethodInSortedMethodList() 為在已經(jīng)排序的方法列表中查找,其內(nèi)部是二分查找武翎。另一個則是普通遍歷查找烈炭。
最終,消息發(fā)送的流程為:

image.png

在 lookUpImpOrForward 的內(nèi)部邏輯中宝恶,若如何都沒有找到方法符隙,會嘗試動態(tài)解析

if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);// 動態(tài)解析
        runtimeLock.lock();
        // 標記是否解析過,置為 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()) { // 判斷是否為元類
        
        _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 可以動態(tài)的添加方法垫毙,我們模擬一下動態(tài)解析的過程霹疫,我們首先在 BoA.h 中添加函數(shù)聲明:

- (void)playGolf;

不實現(xiàn),在外部 [boa playGolf] 的時候會報:

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

然后重寫 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!!!");
}

再次運行 [boa playGolf] 則會打幼劢妗:

Play Golf!!!

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

image.png

若消息發(fā)送和動態(tài)方法解析階段都沒有找到方法的實現(xiàn)额各,則會進入到最后的階段:消息轉(zhuǎn)發(fā)国觉。

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

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

Valenti 類中聲明并實現(xiàn)了 playGolf 的方法:

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

運行結果:

Valenti plays golf!!!

若未實現(xiàn) forwardingTargetForSelector 方法,則會調(diào)用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法缸逃,該方法要求返回一個方法簽名针饥,然后執(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 對象,在這里改成了 Valenti 的對象
    [anInvocation invokeWithTarget:[[Valenti alloc] init]];
}

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

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


image

例子中的方法都是誤無參且無返回值的,那么有參有返回值的又是什么形式:
假如有 release 方法藐守,該方法是打印「發(fā)布了多少張專輯」挪丢,需要傳入一個 count 的參數(shù)決定多少張,BoA 聲明未實現(xiàn)該方法卢厂, Valenti 中聲明且實現(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ù)類型的不同
        return [NSMethodSignature signatureWithObjCTypes:"B@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

外部調(diào)用 [boa release: 5] 運行打忧睢:

Release 5 albums!

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

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

打印結果為 5, 1慎恒。

[anInvocation getArgument:&param atIndex:2] 為什么 index 為 2任内?撵渡,因為參數(shù)順序為:receiver、selector 其次才是其他參數(shù)死嗦。

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

super 關鍵字

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

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

結果為:

[super class] BoA
[super superclass] Singer

是不是和猜想有點出入节腐?明明是 super 指針,打印的卻是本類以及本類的父類摘盆。

super 關鍵字底層執(zhí)行的是 objc_msgSendSuper 方法翼雀。該方法傳入兩個參數(shù),一個是 objc_super 的結構體孩擂,源碼中的結構體形式為:

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

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

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

從 objc_super 的結構可以知道护锤,雖然調(diào)用的是 super官地,但是實際的消息接收者仍然是 BoA 對象。那么傳入的父類作用是什么烙懦?是告訴從哪里開始找方法驱入,也就是說是從父類中找class/superclass 方法,但接收者仍然是本類的對象氯析。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末亏较,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掩缓,更是在濱河造成了極大的恐慌雪情,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件你辣,死亡現(xiàn)場離奇詭異巡通,居然都是意外死亡,警方通過查閱死者的電腦和手機舍哄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門宴凉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人表悬,你說我怎么就攤上這事弥锄。” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵籽暇,是天一觀的道長温治。 經(jīng)常有香客問我,道長图仓,這世上最難降的妖魔是什么罐盔? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮救崔,結果婚禮上惶看,老公的妹妹穿的比我還像新娘。我一直安慰自己六孵,他們只是感情好纬黎,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著劫窒,像睡著了一般本今。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上主巍,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天冠息,我揣著相機與錄音,去河邊找鬼孕索。 笑死逛艰,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的搞旭。 我是一名探鬼主播散怖,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肄渗!你這毒婦竟也來了镇眷?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤翎嫡,失蹤者是張志新(化名)和其女友劉穎欠动,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钝的,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡翁垂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了硝桩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡枚荣,死狀恐怖碗脊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤衙伶,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布祈坠,位于F島的核電站,受9級特大地震影響矢劲,放射性物質(zhì)發(fā)生泄漏赦拘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一躺同、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丸逸,春花似錦蹋艺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至憔维,卻和暖如春涛救,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背业扒。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工检吆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凶赁。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓咧栗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親虱肄。 傳聞我的和親對象是個殘疾皇子致板,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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

  • Objective-C語言是一門動態(tài)語言,他將很多靜態(tài)語言在編譯和鏈接時期做的事情放到了運行時來處理咏窿。這種動態(tài)語言...
    tigger丨閱讀 1,381評論 0 8
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • 本文轉(zhuǎn)載自:http://southpeak.github.io/2014/10/25/objective-c-r...
    idiot_lin閱讀 924評論 0 4
  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 748評論 0 1
  • 如果你和我一樣斟或,覺得自己時間不夠用,整天忙忙碌碌的集嵌,但實際上又沒有具體的結果萝挤,也沒有成就感,內(nèi)心也充滿了焦慮...
    PM熊叔閱讀 1,001評論 0 13