1.1 runtime 的概念api
iOS語言Objective-C是一門動態語言,其在runtime(運行時)會作不少事情,好比:消息的查找、消息的轉發、動態屬性的添加等等,可是runtime究竟是什麼呢?runtime就是由C、C++、 彙編爲OC提供運行時功能開發出來的一套api.xcode
1.2 runtime的使用方式 緩存
runtime的使用方式主要有三種 : ObjectIve-C調用 @selector()、NSObject的方法 NSSelectorFromString()、sel_registerName. 函數apisass
二.objc_msgSend 的初步瞭解
bash
首先咱們在main.m 文件裏,建立了個LGPerson的對象並調用了sayNB的方法,又寫了個C語言函數run,而且調用.好的那咱們如今看下底層在編譯的時候到底作了哪些處理. 打開終端 輸入命令:clang -rewrite-objc main.m -o main.cpp(輸出個.cpp編譯後的文件)打開main.cpp文件截取其中一段以下:app
咱們看到 sayNB方法底層編譯經過objc_msgSend 底層查找函數的實現、默認帶了兩個參數 id和SEL id就是函數的接受者在這裏就是person,SEL方法編號. 而run函數底層沒有編譯成objc_msgSend 那是由於C語言中函數名就是函數指針,它經過這個函數名直接就能找到函數實現並不須要objc_msgSend這一中轉這一階段.可是objc_msgSend到底作了什麼呢?咱們先在代碼裏打個斷點、打開工程裏的Debug -> Debug Workflow - > Always Show Disassembly函數
就是打開彙編調試、咱們在方法上打個斷點:oop
發現會進來下面彙編指令:post
sayCode下有個0x100000c63 <+67>: callq *0x397(%rip) ; (void *)0x00007fff63046000: objc_msgSend. 咱們在這行上打上斷點而且按住control+xcode中step into進去發現指令會進入系統的libobjc.A.dylib的庫裏ui
接下來咱們就要在這個系統的庫中繼續研究objc_msgSend的流程了.
二.objc_msgSend 的進階
首先咱們去 www.opensource.apple.com/apsl/ 中下載目前蘋果開源的私有庫libobjc.A.dylib ,而後配置到咱們的項目裏.咱們的objc_msgSend是由彙編寫的(由彙編寫的好處是:1.能夠經過寫一個函數保留未知的函數任意的跳轉到另外個函數指針 2.彙編語言更接近機器語言 因此使用起來速度會更快)
開始在項目工程全局搜索 objc_msgSend 找到實現objc_msgSend的彙編文件
找到是這個 arm.s 的文件,那咱們進文件裏看下究竟吧.
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
// person - isa - 類
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, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// 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
// SUPPORT_TAGGED_POINTERS
#endif
複製代碼
代碼解析:
cmp p0, #0 判斷是不是 nonepointerisa(是否是純淨的ISA)複製代碼
// person - isa - 類
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class複製代碼
還記得剛剛objc_msgsend 時會默認 傳兩個參數嗎 .id 和 SEL,id 就是當前的對象 它的內存地址首地址是ISA,因此 這個對象 [xo]首地址位 就是isa .由於剛剛第一步已經判斷出不是純淨的isa,因此本身的isa內部偏移16位獲得 shiftClass也就是獲取到當前對象所在的類.
CacheLookup NORMAL // calls imp or objc_msgSend_uncached複製代碼
接着在類的緩存裏進行查找.
(對cache_t結構不是很熟悉的小夥伴們,建議看下上篇文章juejin.im/post/5e0ca3…)
.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, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, 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, 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: // double wrap
JumpMiss $0
.endmacro
複製代碼
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
由ISA偏移16位獲得類的cache,cache裏有buckets 和 occupied|mask 分別由p10和p11進行接收。
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask複製代碼
用低32位的 p11接收mask,而後 & _cmd (也就是sel) 獲得 (mask_t)(key & mask);就是將要在緩存池buckets 進行查找的索引.
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複製代碼
緊接着就在緩存池裏進行查找若是 bucket裏的sel和_cmd匹配的話說明在緩存中已經找到,緩存命中.若是沒有找到就跳轉 2f步驟,找到則返回 , 找不到 JumpMiss
.
繼續來到 __objc_msgSend_uncached
-> MethodTableLookup
最後調用 bl __class_lookupMethodAndLoadCache3
, 來到慢速查找流程 .至於慢速查找流程請期待下篇 《論objc_msgSend消息機制正傳之消息查找》.
四.總結
當咱們調用一個對象或者類方法的時候,系統會調用objc_msgSend進行彙編層面的查找,會什麼先用匯編?由於快!咱們詳細的探索了整個過程,彙編層面主要是對已經緩存過的IMP進行搜索。搜索的路徑是存放整個方法的類或者元類的cache_t的bucket!若是找到直接返回整個方法的實現,若是查不到,說明這個方法沒有進行緩存!那這個方法是否存在呢?請看下一篇文章,objc_msgSend在C函數_class_lookupMethodAndLoadCache3的查找和方法的動態解析!