一.什么是 Runtime乏矾?
- 我們都知道,將源代碼轉(zhuǎn)換為可執(zhí)行的程序,通常要經(jīng)過三個(gè)步驟
:編譯钻心、鏈接凄硼、運(yùn)行
。不同的編譯語言捷沸,在這三個(gè)步驟中所進(jìn)行的操作又有些不同摊沉。- C 語言 作為一門
靜態(tài)類語言
,在編譯階段
就已經(jīng)確定了所有變量的數(shù)據(jù)類型
痒给,同時(shí)也確定好了要調(diào)用的函數(shù)
说墨,以及函數(shù)的實(shí)現(xiàn)
。- 而
Objective-C
語言 是一門動(dòng)態(tài)語言
苍柏。在編譯階段
并不知道變量的具體數(shù)據(jù)類型
尼斧,也不知道所真正調(diào)用的哪個(gè)函數(shù)
。只有在運(yùn)行時(shí)
間才檢查變量的數(shù)據(jù)類型
试吁,同時(shí)在運(yùn)行時(shí)才會(huì)根據(jù)函數(shù)名查找要調(diào)用的具體函數(shù)
棺棵。這樣在程序沒運(yùn)行的時(shí)候,我們并不知道調(diào)用一個(gè)方法具體會(huì)發(fā)生什么熄捍。Objective-C
語言 把一些決定性的工作從編譯階段烛恤、鏈接階段
推遲到運(yùn)行時(shí)階段
的機(jī)制,使得Objective-C
變得更加靈活余耽。我們甚至可以在程序運(yùn)行的時(shí)候缚柏,動(dòng)態(tài)的去修改
一個(gè)方法的實(shí)現(xiàn),這也為大為流行的『熱更新』
提供了可能性碟贾。- 而實(shí)現(xiàn)
Objective-C
語言 運(yùn)行時(shí)機(jī)制 的一切基礎(chǔ)就是Runtime
币喧。Runtime
實(shí)際上是一個(gè)庫,這個(gè)庫使我們可以在程序運(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建對(duì)象缕陕、檢查對(duì)象粱锐,修改類和對(duì)象的方法
Runtime
交互的三種方式
Objective-C Code
直接調(diào)用,比如直接調(diào)用方法[self say]扛邑、#selector()
等怜浅。Framework&Serivce
,比如NSSelectorFromString
蔬崩、isKindeofClass
恶座、isMemberOfClass
等方法。RuntimeAPI
沥阳,比如sel_registerName
跨琳、class_getInstanceSize
等底層方法。
-
runtime 特性:
動(dòng)態(tài)類型,動(dòng)態(tài)綁定,動(dòng)態(tài)加載
二.objc_msgSend探索
1.什么是objc_msgSend?官方文檔給出這么一個(gè)解釋
When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.
大概意思是:
遇到方法調(diào)用時(shí)桐罕,編譯器會(huì)生成對(duì)objc_msgSend
脉让,objc_msgSend_stret
桂敛,objc_msgSendSuper
或objc_msgSendSuper_stret
函數(shù)之一的調(diào)用。 發(fā)送到對(duì)象超類的消息(使用super關(guān)鍵字)是使用objc_msgSendSuper
發(fā)送的溅潜; 其他消息使用objc_msgSend
發(fā)送术唬。 使用objc_msgSendSuper_stret和
objc_msgSend_stret`發(fā)送具有數(shù)據(jù)結(jié)構(gòu)作為返回值的方法。
2.準(zhǔn)備可運(yùn)行objc源碼工程
int main(int argc, const char * argv[]) {
@autoreleasepool {
PHPerson * person = [[PHPerson alloc]init];
Class cls = object_getClass(person);
[person doFirst];
[person doSecond];
[person doThird];
NSLog(@"");
}
return 0;
}
- 新建一個(gè)PHPerson類滚澜,對(duì)象方法doFirst/doSecond/doThird
- cd到當(dāng)前工程main.m文件所在到文件夾
- 執(zhí)行
clang -rewrite-objc main.m
轉(zhuǎn)成.cpp文件 - 找到.cpp文件
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
PHPerson * person = ((PHPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((PHPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("PHPerson"), sel_registerName("alloc")), sel_registerName("init"));
Class cls = object_getClass(person);
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doFirst"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doSecond"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("doThird"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_4j_v597272j6kb0q7g5x72k3xhw0000gn_T_main_d18b1e_mi_0);
}
return 0;
}
每次調(diào)用方法的時(shí)候都會(huì)存在((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("方法名"));
3.斷點(diǎn)調(diào)試
-
下斷點(diǎn)
-
開啟匯編調(diào)試
結(jié)果:我們通過給objc_msgSend
下符號(hào)斷點(diǎn)得知objc_msgSend
函數(shù)在我們的libobjc.A.dylib
中粗仓。
4.匯編分析(筆者匯編不大懂,查資料理解设捐,不正確的地方希望多多指正)
-
關(guān)掉匯編分析借浊,下符號(hào)斷點(diǎn),并重新運(yùn)行工程萝招。
image.png
跳轉(zhuǎn)到_objc_msg-x86_64.s下 根據(jù)r10 = self->isa 可知此過程是獲取對(duì)象的isa指針
接下來我們?cè)?code>libobjc.A.dylib中來查看我們的
objc_msgSend
源碼蚂斤。由于app真機(jī)環(huán)境下基于arm64架構(gòu),我們以
objc-msg-arm64.s
為研究對(duì)象即寒。
我們發(fā)現(xiàn)objc_msgSend
使用匯編
來實(shí)現(xiàn)的橡淆,為什么要用匯編來實(shí)現(xiàn)呢?有以下幾點(diǎn)原
- 匯編更加
容易被機(jī)器識(shí)別母赵,效率更高。
- C語言中不可以通過一個(gè)函數(shù)來
保留未知的參數(shù)并且跳轉(zhuǎn)到任意的函數(shù)指針
具滴。C語言沒有滿足這些事情的必要特性凹嘲。
- 截取部分代碼
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
// calls IMP on success
CacheLookup NORMAL, CALL, _objc_msgSend
NilTestReturnZero NORMAL
GetIsaSupport NORMAL
我們可以看到在獲取isa
之后在方法的緩存列表繼續(xù)查找
-
緩存查找
全局搜索CacheLookup
截取代碼
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
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
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
{imp, sel} = *bucket
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
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
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
可以看到CacheHit
和CheckMiss
兩個(gè)函數(shù),CacheHit
是命中緩存(call or return imp
构韵,方法返回的imp
指針)周蹭,CheckMiss
是沒有在緩存方法列表找到方法的函數(shù),此時(shí)主要分析CheckMiss
這個(gè)函數(shù)內(nèi)部
.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
由于現(xiàn)在的$0
是NORMAL
疲恢,繼續(xù)查找__objc_msgSend_uncached
凶朗,發(fā)現(xiàn)內(nèi)部都調(diào)用了MethodTableLookup
函數(shù)返回
-
arm64
架構(gòu)下MethodTableLookup
函數(shù)部分源碼
.macro MethodTableLookup
....
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
bl _lookUpImpOrForward
....
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
內(nèi)部調(diào)用了_lookUpImpOrForward
函數(shù),看看這個(gè)函數(shù)內(nèi)部干了啥
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
}
- 注釋中有一段比較重要段話翻譯如下
runtimeLock
在isrealization
和isInitialized
檢查期間被保持显拳,以防止與并發(fā)實(shí)現(xiàn)的競(jìng)爭(zhēng)棚愤。runtimeLock
在方法搜索期間進(jìn)行,使方法查找+緩存填充
相對(duì)于方法添加具有原子性
杂数。
否則宛畦,可能會(huì)添加一個(gè)類別,但會(huì)無限期地忽略它揍移,因?yàn)樵?code>緩存刷新后會(huì)用舊值重新填充緩存
次和。
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
如果
sel == initialize
,則class_initialize
將發(fā)送+initialize
那伐,然后信使將在此過程結(jié)束后再次發(fā)送+initialize
踏施。當(dāng)然石蔗,如果這不是被信使調(diào)用,那么它不會(huì)發(fā)生畅形。
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
遞歸超找父類方法緩存列表
可以看到在查找方法實(shí)現(xiàn)的時(shí)候有個(gè)
cache_getImp
函數(shù)养距,繼續(xù)跟進(jìn)
END_ENTRY _cache_getImp
/********************************************************************
*
* id _objc_msgForward(id self, SEL _cmd,...);
*
* _objc_msgForward is the externally-callable
* function returned by things like method_getImplementation().
* _objc_msgForward_impcache is the function pointer actually stored in
* method caches.
*
********************************************************************/
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
cache_fill
ENTRY _objc_msgSend_noarg
b _objc_msgSend
END_ENTRY _objc_msgSend_noarg
realizeClassMaybeSwiftAndLeaveLocked
ENTRY _objc_msgSend_debug
b _objc_msgSend
END_ENTRY _objc_msgSend_debug
ENTRY _objc_msgSendSuper2_debug
b _objc_msgSendSuper2
END_ENTRY _objc_msgSendSuper2_debug
ENTRY _method_invoke
// x1 is method triplet instead of SEL
add p16, p1, #METHOD_IMP
ldr p17, [x16]
ldr p1, [x1, #METHOD_NAME]
TailCallMethodListImp x17, x16
END_ENTRY _method_invoke
注釋的一段話大概意思是:
_objc_msgForward
是外部可調(diào)用的函數(shù),由method_getImplementation()
等函數(shù)返回束亏。_objc_msgForward_impcache
是實(shí)際存儲(chǔ)在方法緩存中的函數(shù)指針铃在。
method_getImplementation(Method m)
{
return m ? m->imp : nil;
}