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)計算器中:
最后一位為 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 位郊供,如圖:
表示字段 shiftcls
,存放著類對象地址或者元類對象的值近哟。
接下來的 6 位 01 1010 表示字段 magic
驮审,表示對象已經(jīng)初始化成功,執(zhí)行完 alloc
和 init
后它的值為 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ù)組:
這樣可以動態(tài)增加方法或者修改方法枷餐,并且二維數(shù)組的每個方法列表都有可能是一個分類的方法列表靶瘸。
class_ro_t
class_ro_t 中的 baseMethodList
、baseProtocols
毛肋、ivars
怨咪、baseProperties
是一維數(shù)組的,只讀村生,包含了類的初始內(nèi)容惊暴。
也就是說本類的協(xié)議饼丘、屬性趁桃、方法等信息在這個一維數(shù)組里面。
這份不變的 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() 信息:
打印得:
Printing description of data->methods->first.imp:
(IMP) imp = 0x00000001002ce654 (Test_3`-[Test test] at Test.m:13)
來到第二個斷點 Debug->Debug Workflow->Always Show Disassembly:
發(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)用。
該索引在 test() 方法放入哈希表的時候就已經(jīng)確定惋啃。
當然存在這種情況哼鬓,假如哈希表數(shù)組為 0,而 @selector(test) & _mask 結果為 3边灭,則情況為:
也就是說异希,其他位都成了預留位置且都是 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
}
緩存方法的時候:
若 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===="); //加斷點
運行來到第一個斷點:
發(fā)現(xiàn)哈希表容量為 4(_mask + 1),此時 _occupied 為 1杉允,緩存的可能是 init 方法邑贴。
來到第二個斷點:
_occupied 為 2,已緩存 run 方法夺颤。
來到第三個斷點:
_occupied 為 3痢缎,已緩存 sing 方法胁勺。
來到第四個斷點:
_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ā)送的流程為:
在 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ā)的流程。
若消息發(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ā)的流程為:
例子中的方法都是誤無參且無返回值的,那么有參有返回值的又是什么形式:
假如有 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:¶m atIndex:2];
BOOL ret;
[anInvocation getReturnValue:&ret];
NSLog(@"%d %d",param, ret);
打印結果為 5, 1慎恒。
[anInvocation getArgument:¶m 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 方法,但接收者仍然是本類的對象氯析。