Objective-C作為一門(mén)動(dòng)態(tài)語(yǔ)言肥印,將很多事都在運(yùn)行期間完成识椰,如消息發(fā)送、消息轉(zhuǎn)發(fā)深碱、動(dòng)態(tài)的方法交換腹鹉、對(duì)象關(guān)聯(lián)(為類(lèi)添加實(shí)例變量)、攔截系統(tǒng)自帶的方法調(diào)用(Swizzle黑魔法)敷硅、KVC功咒、KVO
我們先來(lái)探究下我們最常用的消息發(fā)送機(jī)制是如何實(shí)現(xiàn)的愉阎。
1.消息發(fā)送的基礎(chǔ)
(1)對(duì)象、類(lèi)航瞭、元類(lèi)
[receiver message];
這一類(lèi)代碼我們可以通過(guò)clang轉(zhuǎn)換為C++代碼進(jìn)行窺探
//NSString * str = [[NSString alloc] initWithFormat: @"hello world!"];
NSUInteger a = [str length];
//terminal終端輸入: clang -rewrite-objc test.m
NSUInteger a = ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)str, sel_registerName("length"));
我們看到調(diào)用了一個(gè)返回NSUInteger
參數(shù)為(id, SEL)的函數(shù)诫硕,通過(guò)函數(shù)指針objc_msgSend
調(diào)用。通過(guò)對(duì)參數(shù)的觀察發(fā)現(xiàn)刊侯,傳入了str對(duì)象指針章办,方法是通過(guò)對(duì)方法的字符串"length"進(jìn)行解析獲得。即轉(zhuǎn)變成的調(diào)用形式為
objc_msgSend(receiver, selector) //無(wú)參數(shù)
objc_msgSend(receiver, selector, arg1, arg2, ...) //有參數(shù)
通過(guò)匯編查看也是調(diào)用_objc_msgSend
movq L_OBJC_SELECTOR_REFERENCES_.4(%rip), %rsi
movq %rax, %rdi
callq _objc_msgSend
沒(méi)有直接調(diào)用對(duì)應(yīng)的方法滨彻,而是放在了運(yùn)行時(shí)進(jìn)行查找藕届。這也是OC語(yǔ)言的動(dòng)態(tài)性所在。
我們先看參數(shù)id與SEL的意義亭饵,id是OC中的關(guān)鍵字之一休偶,作用類(lèi)似于void *,這里傳入了我們的對(duì)象指針str
,顯然這是一個(gè)對(duì)象指針辜羊,OC中對(duì)id的定義可以在runtime中窺見(jiàn)
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
private:
isa_t isa;
public:
//一堆方法實(shí)現(xiàn)
};
這個(gè)結(jié)構(gòu)體包含了一個(gè)isa和一堆方法實(shí)現(xiàn)踏兜,isa是isa_t的聯(lián)合體類(lèi)型,
OC所有對(duì)象都包含isa,實(shí)例對(duì)象中其指向類(lèi),類(lèi)中指向元類(lèi)拓轻。OC中類(lèi)和元類(lèi)也是對(duì)象郎任,不過(guò)只是唯一而已黍瞧。類(lèi)中存了實(shí)例對(duì)象方法,元類(lèi)中存了類(lèi)方法。具體關(guān)系如圖所示,superclass和isa構(gòu)成了整個(gè)Objective-C的對(duì)象繼承關(guān)系纳本。發(fā)送方法消息時(shí),實(shí)例對(duì)象可以通過(guò)isa找到類(lèi)對(duì)象腋颠。
Rootclass在OC中為NSObject類(lèi)繁成,繼承關(guān)系圖示非常清晰了。值得注意的便是NSObject的元類(lèi)的超類(lèi)是NSObject淑玫。
我們需要了解類(lèi)對(duì)象的定義巾腕。在runtime.h中我們可以找到類(lèi)對(duì)象的定義
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
我們?cè)谄渲锌梢钥吹筋?lèi)對(duì)象中定義如下
super_class:指明其父類(lèi)
isa:指明其元類(lèi)
methodLists :指明方法的具體實(shí)現(xiàn)
cache:緩存最近用到的方法,減少消息發(fā)送開(kāi)銷(xiāo)
protocols :要實(shí)現(xiàn)的原型列表
Ivars: 類(lèi)的實(shí)例變量
下圖展示了相關(guān)結(jié)構(gòu)體的關(guān)系與isa中結(jié)構(gòu)體bit意義
ivars:該類(lèi)的對(duì)象成員變量鏈表,這里和下面的兩個(gè)結(jié)構(gòu)體都用到了變長(zhǎng)結(jié)構(gòu)體的技術(shù)混移,即通過(guò)定義一個(gè)ivar_list[1]占位符祠墅,最后alloc變長(zhǎng)堆空間時(shí)侮穿,以其為基準(zhǔn)地址進(jìn)行偏移量的增加獲得成員變量歌径。
如
objc_ivar_list * test = malloc(sizeof(objc_ivar_list)+sizeof(objc_ivar)*SIZE);
//然后通過(guò)test->ivar_list[i]可以直接訪問(wèn)
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;//成員變量個(gè)數(shù)
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 變量名
char *ivar_type OBJC2_UNAVAILABLE; // 變量類(lèi)型
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字節(jié)
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
methodLists:該類(lèi)的實(shí)例方法鏈表。這是一個(gè)二級(jí)指針亲茅,即指針變量當(dāng)中存的是一個(gè)地址回铛,你可以改變這個(gè)地址的值從而改變最終指向的變量狗准。可以動(dòng)態(tài)的修改*methodLists的值來(lái)添加方法茵肃。而ivars是一級(jí)指針腔长,這也是category能增加方法而無(wú)法增加實(shí)例變量的體現(xiàn)了。objc_method中包含了IMP的指針验残,IMP實(shí)際就是方法地址捞附。定義見(jiàn)下面
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;//方法名
char *method_types OBJC2_UNAVAILABLE;//返回值和參數(shù)
IMP method_imp OBJC2_UNAVAILABLE;//實(shí)現(xiàn)
}
typedef void (*IMP)(void /* id, SEL, ... */ );
cache:緩存最近用到的方法,消息發(fā)送時(shí)先從cache中查找您没,沒(méi)有才會(huì)進(jìn)入方法表中查找鸟召。
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
//一個(gè)整數(shù),指定分配的緩存bucket的總數(shù)
unsigned int occupied OBJC2_UNAVAILABLE;
//指定實(shí)際占用的緩存bucket的總數(shù)氨鹏。
Method buckets[1] OBJC2_UNAVAILABLE;
//buckets作為cache數(shù)組基址欧募,這個(gè)數(shù)組包含不超過(guò)mask+1個(gè)元素,
};
(2)消息發(fā)送
有了大致的基礎(chǔ)我們現(xiàn)在來(lái)看如何進(jìn)行消息發(fā)送
消息發(fā)送和轉(zhuǎn)發(fā)流程如圖仆抵,可略跟继,僅做備份。
<1>.objc_msgSend
參考objc-msg-x86_64.s镣丑,匯編偽指令參考匯編偽指令
首先看調(diào)用的objc_msgSend(id self, SEL _cmd,...);方法實(shí)現(xiàn)
我們看到在objc-msg-x86_64.s定義了多個(gè)msgsend函數(shù)舔糖,在編譯器消息發(fā)送時(shí),編譯器會(huì)根據(jù)不同的返回值類(lèi)型決定生成哪一個(gè)msgsend函數(shù)传轰,具體情況如下
//定義八字節(jié)數(shù)據(jù).quard偽指令剩盒,即函數(shù)地址。
.align 4
.private_extern _objc_entryPoints
_objc_entryPoints:
.quad _cache_getImp
.quad _objc_msgSend
//Sends a message with a simple return value to an instance of a class.
.quad _objc_msgSend_fpret
//a floating-point return value
.quad _objc_msgSend_fp2ret
//complex long double` return only.
.quad _objc_msgSend_stret
//a data-structure return value
.quad _objc_msgSendSuper
//Sends a message with a simple return value to the superclass of an instance of a class.
.quad _objc_msgSendSuper_stret
//a data-structure return value
.quad _objc_msgSendSuper2
.quad _objc_msgSendSuper2_stret
.private_extern _objc_exitPoints
_objc_exitPoints:
.quad LExit_cache_getImp
.quad LExit_objc_msgSend
.quad LExit_objc_msgSend_fpret
.quad LExit_objc_msgSend_fp2ret
.quad LExit_objc_msgSend_stret
.quad LExit_objc_msgSendSuper
.quad LExit_objc_msgSendSuper_stret
.quad LExit_objc_msgSendSuper2
.quad LExit_objc_msgSendSuper2_stret
.quad 0
我們這次只看第一個(gè)慨蛙,其他代碼結(jié)構(gòu)是類(lèi)似的辽聊。
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd,...);
*
********************************************************************/
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
ENTRY _objc_msgSend
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r11 = self->isa
CacheLookup NORMAL // calls IMP on success
/*
CacheLookup實(shí)現(xiàn)中會(huì)判斷調(diào)用IMP
4: ret
.elseif $0 == NORMAL || $0 == FPRET || $0 == FP2RET
// eq already set for forwarding by `jne`
MESSENGER_END_FAST
jmp *8(%r10) // call imp
*/
NilTestSupport NORMAL
GetIsaSupport NORMAL
// cache miss: go search the method lists
LCacheMiss:
// isa still in r11
MethodTableLookup %a1, %a2 // r11 = IMP
cmp %r11, %r11 // set eq (nonstret) for forwarding
jmp *%r11 // goto *imp
END_ENTRY _objc_msgSend
ENTRY _objc_msgSend_fixup //int3斷點(diǎn)
int3
END_ENTRY _objc_msgSend_fixup
STATIC_ENTRY _objc_msgSend_fixedup
// Load _cmd from the message_ref
movq 8(%a2), %a2
jmp _objc_msgSend
END_ENTRY _objc_msgSend_fixedup
根據(jù)注解我們可以看到,objc_msgSend首先調(diào)用GetIsaFast
將isa賦值到R11寄存器中期贫,然后通過(guò)調(diào)用CacheLookup
進(jìn)行cache查找跟匆,如果找到則直接調(diào)用IMP
指向的方法。
如果沒(méi)有找到則通過(guò)MethodTableLookup
方法進(jìn)行查找方法表通砍,動(dòng)態(tài)解析也在這一步中添加玛臂。
/////////////////////////////////////////////////////////////////////
//
// MethodTableLookup classRegister, selectorRegister
//
// Takes: $0 = class to search (a1 or a2 or r10 ONLY)
// $1 = selector to search for (a2 or a3 ONLY)
// r11 = class to search
//
// On exit: imp in %r11
//
/////////////////////////////////////////////////////////////////////
.macro MethodTableLookup
MESSENGER_END_SLOW
SaveRegisters
// _class_lookupMethodAndLoadCache3(receiver, selector, class)
movq $0, %a1
movq $1, %a2
movq %r11, %a3
call __class_lookupMethodAndLoadCache3
// IMP is now in %rax
movq %rax, %r11
RestoreRegisters
.endmacro
/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
其中調(diào)用了_class_lookupMethodAndLoadCache3(receiver, selector, class)參數(shù)分別是對(duì)象,方法封孙,類(lèi)對(duì)象迹冤。
并將 IMP 返回(從 rax
挪到 r11
)。最后在 objc_msgSend
中調(diào)用 IMP方法虎忌。
那么查找路徑顯然在lookUpImpOrForward里面
<2>. lookUpImpOrForward 快速查找 IMP
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search 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.
runtimeLock.read();
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// 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
}
retry:
runtimeLock.assertReading();
// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
{
Method 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.
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// 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.
Method 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);
runtimeLock.read();
// 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();
return imp;
}
我們傳入了
return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
先判斷類(lèi)是否時(shí)第一次用到泡徙,是否要初始化,然后進(jìn)行初始化膜蠢。retray中開(kāi)始進(jìn)行標(biāo)準(zhǔn)的方法表查找
過(guò)程如下:
- 查找類(lèi)中的cache是否緩存該方法堪藐。IMP非nil莉兰,命中緩存則跳轉(zhuǎn)到done;否則繼續(xù)
- 嘗試查找類(lèi)的方法表礁竞,調(diào)用getMethodNoSuper_nolock(cls, sel)查找Method糖荒。如果search_method_list有序則采用二分法查找,無(wú)序則順序查找模捂。如果找到Method捶朵,存cache,賦值imp=Method->imp,跳轉(zhuǎn)到done狂男;否則繼續(xù)
- 繼承層級(jí)中遞歸向超類(lèi)查找泉孩。先查cache。命中緩存并淋,找到imp寓搬,則跳到4;沒(méi)找到則跳5
- 其不是
_objc_msgForward_impcache
(消息轉(zhuǎn)發(fā)函數(shù))則寫(xiě)入類(lèi)的cache县耽,跳轉(zhuǎn)到done句喷;否則遇到消息轉(zhuǎn)發(fā)的標(biāo)記,終止遞歸查找兔毙,跳6- 嘗試查找超類(lèi)的方法表唾琼,調(diào)用getMethodNoSuper_nolock(curClass, sel)查找Method。如果search_method_list有序則采用二分法查找澎剥,無(wú)序則順序查找锡溯。如果找到Method,存cache哑姚,賦值imp=Method->imp,跳轉(zhuǎn)到done祭饭;否則跳3
- 動(dòng)態(tài)方法解析,這步是消息轉(zhuǎn)發(fā)前的最后一次機(jī)會(huì)叙量。此時(shí)釋放讀入鎖(
runtimeLock.unlockRead()
)倡蝙,接著間接地發(fā)送+resolveInstanceMethod
或+resolveClassMethod
消息動(dòng)態(tài)添加imp方法。完成后回到1查找imp绞佩,否則將_objc_msgForward_impcache
當(dāng)做 IMP 并寫(xiě)入緩存寺鸥,進(jìn)入done- done:讀操作解鎖,并將之前找到的 IMP 返回品山。這里的imp有可能是找到的方法地址胆建,也可能是
_objc_msgForward_impcache
方法轉(zhuǎn)發(fā)函數(shù)地址。
通過(guò)這一過(guò)程消息發(fā)送找到imp肘交,或者返回消息轉(zhuǎn)發(fā)函數(shù)的地址笆载。在這一步消息機(jī)制結(jié)束,
jmp *%r11 // goto *imp
要么執(zhí)行方法,要么進(jìn)入消息轉(zhuǎn)發(fā)宰译。
<3>.動(dòng)態(tài)解析的使用
方法表的遞歸查找失敗時(shí)會(huì)進(jìn)行動(dòng)態(tài)解析,動(dòng)態(tài)解析失敗則進(jìn)入方法轉(zhuǎn)發(fā)魄懂。動(dòng)態(tài)解析會(huì)給我們?nèi)螜C(jī)會(huì)沿侈。
- Method resolution
- Fast forwarding
- Normal forwarding
具體參考Objective-C Runtime 之動(dòng)態(tài)方法解析實(shí)踐
具體將會(huì)在后面文章闡述