手把手帶你探索Runtime底層原理(一)方法查詢
Runtime簡單介紹
Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。理解 Objective-C 的 Runtime 機制可以幫我們更好的瞭解這個語言,適當的時候還能對語言進行擴充套件,從系統層面解決專案中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 訊息傳遞 (Messaging)。
Runtime
基本是用 C 和彙編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這裡 密碼:tuw8 下到蘋果維護的開原始碼。蘋果和 GNU 各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。
Runtime訊息傳送
一個物件的方法像這樣[obj foo],通過 clang -rewrite-objc
命令檢視編譯後的程式碼(由於之前的文章操作過,這裡不詳細解釋操作流程了),編譯器轉成訊息傳送objc_msgSend(obj,foo)
objc_msgsend
底層有兩種查詢方式:
- 快速查詢: 通過彙編直接在快取中找到這個方法並呼叫
- 慢速查詢: 通過c,c++以及彙編一起完成的
為什麼要使用匯編?
- 彙編通過一個函式保留未知引數,然後跳轉到任意的指標,可以直接使用暫存器儲存,而C無法實現
- 彙編程式碼執行的效率高,執行的時間週期準確
快速查詢
先在剛剛提供的objc原始碼裡查詢objc_msgSend
arm64
彙編檔案,可以看到ENTRY _objc_msgSend
,其實就是這個函式的入口
1._objc_msgSend入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend,NoFrame
cmp p0,#0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13,[x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10,_objc_debug_taggedpointer_classes@PAGE
add x10,x10,_objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11,x0,#60,#4
ldr x16,[x10,x11,LSL #3]
adrp x10,_OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10,_OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10,x16
b.ne LGetIsaDone
// ext tagged
adrp x10,_objc_debug_taggedpointer_ext_classes@PAGE
add x10,_objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11,#52,#8
ldr x16,LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1,#0
movi d0,#0
movi d1,#0
movi d2,#0
movi d3,#0
ret
END_ENTRY _objc_msgSend
複製程式碼
可能看不懂彙編,根據註釋大概推測其意思,下面的程式碼主要做了非空檢查和無標記指標檢查(如果指標小於等於 LNilOrTagged 直接return返回)
cmp p0,#0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
複製程式碼
接下來看如下程式碼,根據isa獲取這個類,LGetIsaDone
是表示isa處理完畢,CacheLookup NORMAL
表示直接呼叫當前的imp
或者傳送objc_msgSend_uncached
無快取訊息
ldr p13,[x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
複製程式碼
2. CacheLookup巨集定義
在當前檔案裡搜尋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
cmp p12,p10 // wrap if bucket == buckets
b.eq 3f
ldp p17,[x12,#-BUCKET_SIZE]! // {imp,sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket,w11 = mask
add p12,UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17,sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
複製程式碼
在CacheLookup巨集定義這裡
-
CacheHit
: 快取命中,方法的實現IMP在暫存器中,然後傳遞出去 -
CheckMiss
: 快取沒命中,傳送_objc_msgSend_uncached -
add
: 如果快取裡沒找到,去其他地方查詢到該方法實現後新增到快取
3. CacheHit 和 CheckMiss
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17,x12 // authenticate and call imp
.elseif $0 == GETIMP
mov p0,p17
AuthAndResignAsIMP x0,x12 // authenticate imp and re-sign as IMP
ret // return IMP
.elseif $0 == LOOKUP
AuthAndResignAsIMP x17,x12 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
複製程式碼
看到CacheHit
的巨集定義: 在上面呼叫的時候,傳遞過來的是NORMAL
,執行了TailCallCachedImp
,即如果快取命中的話,則返回快取裡的IMP
.
.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
複製程式碼
再看到CheckMiss
的巨集定義:在上面呼叫的是NORMAL
,所以這裡會傳送__objc_msgSend_uncached
的訊息
4.__objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached,FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
複製程式碼
發現這裡呼叫了MethodTableLookup
,所以繼續跟進檢視
.macro MethodTableLookup
// 省略
// save parameter registers: x0..x8,q0..q7
sub sp,sp,#(10*8 + 8*16)
stp q0,q1,[sp,#(0*16)]
stp q2,q3,#(2*16)]
stp q4,q5,#(4*16)]
stp q6,q7,#(6*16)]
stp x0,x1,#(8*16+0*8)]
stp x2,x3,#(8*16+2*8)]
stp x4,x5,#(8*16+4*8)]
stp x6,x7,#(8*16+6*8)]
str x8,#(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2,x16
bl __class_lookupMethodAndLoadCache3
//省略
.endmacro
複製程式碼
-
前面後面的彙編程式碼只能看到有做位元組對齊的操作,不過由於不懂彙編,具體做什麼不是很清楚,不過看到了跳轉進了一個非常重要的函式
__class_lookupMethodAndLoadCache3
-
繼續搜尋
__class_lookupMethodAndLoadCache3
發現並不能在當前彙編檔案裡找到宣告,這時猜想會不會是跳轉到了程式碼裡 -
於是全域性搜尋
class_lookupMethodAndLoadCache3
,果然在objc-runtime-new.mm
檔案裡找到了它的函式實現
IMP _class_lookupMethodAndLoadCache3(id obj,SEL sel,Class cls)
{
return lookUpImpOrForward(cls,sel,obj,YES/*initialize*/,NO/*cache*/,YES/*resolver*/);
}
複製程式碼
所以這裡通過呼叫lookUpImpOrForward
開啟了慢速查詢
的過程
快速查詢總結
慢速查詢
來到lookUpImpOrForward
函式的方法實現原始碼
IMP lookUpImpOrForward(Class cls,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.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls,inst));
runtimeLock.lock();
}
//先省略
}
複製程式碼
- 首先分析一下從傳遞進來的三個引數,根據註釋
initialize
為YES,cache
為NO,resolver
為YES - 首先解釋一下為什麼彙編傳遞的三個引數為這幾個值?在上面分析的彙編裡,
LGetIsaDone
這個判斷是在isa處理完畢後才走快取查詢的彙編程式碼的,所以這個類是載入解析好的,即initialize
和resolver
都為YES,cache
為NO是因為在彙編裡快速查詢沒有找到方法快取才會執行到這裡,所以這裡肯定為NO - 接下來看到這裡判斷是否有快取,如果有直接調用匯編裡的
cache_getImp
去獲取imp,由於傳遞進來的cache為NO,所以這裡不會執行
// Optimistic cache lookup
// 查詢快取!!!
if (cache) {
//彙編程式碼的方式實現的!
imp = cache_getImp(cls,sel);
if (imp) return imp;
}
//彙編程式碼(在之前上面的彙編檔案裡)
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP
複製程式碼
- 繼續看下去
checkIsKnownClass(cls);
檢查這個類是否已知,如果未知則丟擲異常 - 接下來判斷類有沒有實現和有沒有初始化,沒有則呼叫實現方法
realizeClass(cls)
和初始化方法_class_initialize (_class_getNonMetaClass(cls,inst));
慢速查詢重點:retry
在lookUpImpOrForward
裡還有retry相關的程式碼,繼續分析
retry:
runtimeLock.assertLocked();
// Try this class's cache.
imp = cache_getImp(cls,sel);
if (imp) goto done;
// Try this class's method lists.
{
//Method(SEL IMP)
Method meth = getMethodNoSuper_nolock(cls,sel);
if (meth) {
log_and_fill_cache(cls,meth->imp,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,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,curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls,inst);
runtimeLock.lock();
// 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.
//_objc_msgForward
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls,inst);
done:
runtimeLock.unlock();
return imp;
複製程式碼
-
第一步看到這裡再一次呼叫了
cache_getImp(cls,sel);
去從快取中獲取imp,傳遞進來明明已經知道是NO了,為什麼再去查詢一次呢? -
在
objc_init
的時候有一個函式remap(cls)
,在彙編最開始查詢該方法的時候如果沒有方法快取,但可能會在這個類初始化方法objc_init
的過程中,對這個類進行了重對映remap
,即把該方法新增到方法快取裡了,所以這裡要再去查詢一次cache
有快取就可能會節省很多時間 -
接下來先從當前類的方法列表
method_list
去找,找到了就log_and_fill_cache
列印日誌並把方法新增到快取中 -
如果沒找到則繼續找父類的快取
cache_getImp(curClass,sel)
,再找父類的方法列表,和之前在本類的查詢順序一樣,找到了也是新增到方法快取log_and_fill_cache
- 慢速查詢到上面已經結束了,由於上面整個過程比較慢,所以一般稱為慢速。如果沒有找到imp,則進行
動態方法解析和訊息轉發
,篇幅原因,接下來的這個過程在Runtime底層原理(二)動態方法解析和訊息轉發
慢速查詢總結
以上均為個人探索原始碼的理解和所得,如有錯誤請指正,歡迎討論。