OC是一門動態語言所有的方法都是由執行時進行。 Objective-C 語言將決定儘可能的從編譯和連結時推遲到執行時。只要有可能,Objective-C 總是使用動態 的方式來解決問題。這意味著 Objective-C 語言不僅需要一個編譯器,同時也需要一個執行時系統來執行 編譯好的程式碼。這兒的執行時系統扮演的角色類似於 Objective-C 語言的作業系統,Objective-C 基於該系統來工作。 Runtime的作用是 能動態產生/修改一個類,一個成員變數,一個方法
Runtime呼叫有三種方式
- NSObject(peformselector)
- Selector(底層會轉換為objc_msgSend())
- Runtime的Api
objc_msg_send()
我們知道OC的函式呼叫是訊息傳送機制,那麼訊息傳送機制是如何實現的呢。
Animals * animal = [[Animals alloc]init];
[animal eat];
複製程式碼
將該檔案編譯成c++檔案通過
clang-rewrite-objc 檔名 -o test.c++
命令
一共9w多行程式碼只需看最後
// -(void) eat;
/* @end */
#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_b2_hs7ds2bd5zz7d752kk495bhw0000gn_T_main_f668c6_mi_0);
Animals * animal = ((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)((Animals *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animals"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼
objc_msgSend(void /* id self, SEL op, ... */ ) 當類初始化時候 顯示獲取 id self,即 (id)objc_getClass("Animals"),就是根據類名取獲取這個類,然後alloc,init就是 #selector(alloc) 其底層實現是 sel_registerName("alloc/init"),其目的就是為了查詢該類裡面有沒該方法 第二句同理target是已經生產的animal selector是 eat方法 sel_registerName("eat")去類的記憶體佈局中查詢eat方法
objc_msgsend 底層實現有兩種方法一中是快速轉發一種是慢速轉發 快速是通過彙編從響應的快取裡面找到,慢速是通過c,c++以及彙編一起完成的。
之所以使用匯編的原因是 :
- c裡面不會寫一個函式保留未知的引數跳轉到任意的指> 針,c無法實現,彙編可以通過暫存器直接實現
- 快,下層編譯
快速轉發
快速轉發直接通過 彙編 + 快取 來進行查詢的 快取是來自於類、
///ps: 類繼承於物件從這裡也可以看出來類其實也是一個物件
struct objc_class: objc_objcet {
// class ISA;
Class superclass;
cache_t cache; ///
classs_data_bits_t bitgs; /// 類裡面所有的資料
class_rw_t *data() {
return bits.data()
}
}
複製程式碼
類結構裡的 cacle_t 快取 儲存方法的Selector(在iOS中SEL就是可以根據一個SEL選擇對應的方法IMP。SEL只是描述了一個方法的格式)和IMP(一個函式指標,這個被指向的函式包含一個接收訊息的物件id(self 指標), 呼叫方法的選標 SEL (方法名),以及不定個數的方法引數,並返回一個id。也就是說 IMP 是訊息最終呼叫的執行程式碼,是方法真正的實現程式碼 。我們可以像在C語言裡面一樣使用這個函式指標。)。IMP和Selector會組成一張雜湊表,通過雜湊直接查詢非常快,當查詢第一個方法的時候第一步找到cache,如果裡面有他會直接返回。如果沒有會經歷一個複雜的過程(慢速查詢)。找到了會在裡面存一份方便下次進行查詢,這次主要介紹快速找找的過程通過OC原始碼
剛剛的方法通過Xcode除錯除錯彙編頁面
在原始碼裡搜尋_objc_msgsend
先把完整的彙編原始碼貼上,可以往下看,然後在回來看
********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
ENTRY _objc_msgSend ///************************************** 1.進入objcmsgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
/// x0 recevier
// 訊息接收者 訊息名稱
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative) //// ****************************************************2.isa 優化
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone: ///**************************************************** 3.isa優化完成
CacheLookup NORMAL // calls imp or objc_msgSend_uncached ///*******************************************4.執行 CacheLookup NORMAL
LNilOrTagged:
b.eq LReturnZero // nil check
/// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
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]
b LGetIsaDone
LExtTag:
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
ENTRY _objc_msgLookup
UNWIND _objc_msgLookup, NoFrame
cmp x0, #0 // nil check and tagged pointer check
b.le LLookup_NilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LLookup_GetIsaDone:
CacheLookup LOOKUP // returns imp
LLookup_NilOrTagged:
b.eq LLookup_Nil // nil check
/// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LLookup_ExtTag
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]
b LLookup_GetIsaDone
LLookup_ExtTag:
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LLookup_GetIsaDone
LLookup_Nil:
adrp x17, __objc_msgNil@PAGE
add x17, x17, __objc_msgNil@PAGEOFF
ret
END_ENTRY _objc_msgLookup
STATIC_ENTRY __objc_msgNil
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY __objc_msgNil
ENTRY _objc_msgSendSuper
UNWIND _objc_msgSendSuper, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
END_ENTRY _objc_msgSendSuper
// no _objc_msgLookupSuper
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2
ENTRY _objc_msgLookupSuper2
UNWIND _objc_msgLookupSuper2, NoFrame
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
CacheLookup LOOKUP
END_ENTRY _objc_msgLookupSuper2
.macro MethodTableLookup
// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3/// *********************************************6.方法為_class_lookupMethodAndLoadCache3呼叫的組合語言
// imp in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
.endmacro
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup /// ********************************************** 5.查詢IMP
br x17
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
STATIC_ENTRY _cache_getImp
and x16, x0, #ISA_MASK
CacheLookup GETIMP
LGetImpMiss:
mov x0, #0
ret
END_ENTRY _cache_getImp
複製程式碼
- LLookup_NilOrTagged///針對記憶體裡暫存器進行賦值處理isa優化。
- LGetIsaDone isa /// isa處理完畢
- CacheLookup Normal 呼叫當前imp或者傳送objcmsgsend_uncache
1.CacheLookup Normal
先貼原始碼
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheHit /// cachehit
.if $0 == NORMAL /// normal ///call imp
MESSENGER_END_FAST
br x17 // call imp
.elseif $0 == GETIMP
mov x0, x17 // return imp
ret
.elseif $0 == LOOKUP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b /// loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b /// loop
3: // double wrap
JumpMiss $0
.endmacro
複製程式碼
CacheLookup 有三種 NORMAL GETIMP LOOKUP
- CacheHit call imp /// 查詢快取
- CheckMiss - /// 找不到的處理 傳送_objc_msgSend_uncached
- add /// 如果快取裡沒有找到但是其他地方找到了這時候就可以add到快取裡面去
CacheHit 也是一個巨集,如果$0 == Normal 則進行call imp 操作這是找到了操作。如果找不到的話,則執行check miss,check miss也是一個巨集 $0 == Normal 會傳送 objcmsgsend_uncache,這個時候整個流程就出來了。 CacheHit的意義就是要麼查詢IMP要麼傳送objcmsgsenduncache方法
2. _objc_msgSend_uncache
如果走到這裡說明CacheHit並沒有找到對應的方法而執行了_objc_msgSend_uncache /// 沒有快取去慢速查詢imp
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup /// 重點 方法列表
br x17 // call imp
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached
複製程式碼
MethodTableLookup 方法列表這個方法是關鍵, 因為 br x 17 是設定imp,而 MethodTableLookup 在之前呼叫說明他是在慢速查詢。
3. MethodTableLookup
.macro MethodTableLookup
// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3 /// 重點查詢IMP
// imp in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
.endmacro
複製程式碼
__class_lookupMethodAndLoadCache3 這個方法顧名思義是 查詢方法列表並快取,到了這裡了我們發現原始碼裡面並沒有看到這個方法的定義。因為
__class_lookupMethodAndLoadCache3 方法為_class_lookupMethodAndLoadCache3呼叫的組合語言 通過_class_lookupMethodAndLoadCache3 來到c++檔案
快速轉發總結
oc的方法呼叫本質是進行objc _ msgSend呼叫,而objcmsgSend進行實現的時候有兩種方式一種是快速查詢一種是慢速查詢。快速查詢是oc先去類結構裡的cache_ t的類面去查詢,裡面是由 c c++ 和彙編一起完成的,採用會變得原因是他可以做到c語言無法完成的原因是c裡面不會寫一個函式保留未知的引數跳轉到任意的指標,c無法實現,彙編可以通過暫存器直接保留,而且速度快,進入objc_ msg_ send的時候
- 首先會執行LLookup_NilOrTagged對isa進行優化,優化完畢後會執行LLookup_GetIsaDone.
- 之後執行CacheLookup進行快取查詢,快取查詢會分三步CacheHit如果是Normal,則直接返回imp說明快取中有並返回(br x17),如果是GETIMP,LOOKUP,則會繼續往下走。如果沒有的話則執行checkmiss,Normal LOOKUP操作 傳送objc_msgSend_uncahced訊息,objc_msgSend_uncahced進入該方法後說明沒有快取進入慢速查詢IMP,裡面有一個MethodTableLookup,之所以這句是關鍵程式碼適應br x 17是為imp進行設定,所以該程式碼是關鍵,這方法裡面執行**__class_lookupMethodAndLoadCache3**,因為該方法是**_class_lookupMethodAndLoadCache3**呼叫的組合語言所以就查詢到這個方法,該方法是c++檔案裡的程式碼
到了這裡就已經進入到c++ 檔案裡面。下篇文章具體分析慢速轉發流程。
ps:以上為個人理解,如果有誤歡迎指正。一起進步