iOS 底層探索系列html
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列緩存
咱們在前面探索了對象和類的底層原理,接下來咱們要探索一下方法的本質,而在探索以前,咱們先簡單過一遍 Runtime
的知識點,若是讀者對這塊內容已經很熟悉了的話能夠直接跳過第一章。bash
PS: 因爲筆者對彙編暫時仍是摸索的階段,關於彙編源碼的部分若有錯誤,歡迎指正。markdown
Runtime
簡介衆所周知,Objective-C
是一門動態語言,而承載整個 OC
動態特性的就是 Runtime
。關於 Runtime
更多內容能夠直接進入官網文檔查看。app
Runtime
是以 C
/C++
和彙編編寫而成的,爲何不用 OC
呢,這是由於對咱們編譯器來講,OC
屬於更高級的語言,相比於 C
和 C++
以及彙編,執行效率更慢,而在運行時系統須要儘量快的執行效率。ide
Runtime
的前世此生Runtime
分爲兩個版本,legacy
和 modern
,分別對標 OC 1.0
和 OC 2.0
。咱們一般只須要專一於 modern
版本便可,在 libObjc
源碼中體如今 new
後綴的文件上。函數
Runtime
三種交互方式咱們與 Runtime
打交道有三種方式:oop
OC
層進行交互:好比 @selector
NSObject
的方法:NSSelectorFromName
Runtime
的函數: sel_registerName
咱們能夠看到,經過 clang
重寫以後,sayNB
在底層實際上是一個消息的發送。post
咱們把右側的發送消息的代碼簡化一下:學習
LGPerson *person = objc_msgSend((id)objc_getClass("LGPerson"), sel_registerName("alloc")); objc_msgSend((id)person, sel_registerName("sayNB")); 複製代碼
因而可知,真正發送消息的地方是 objc_msgSend
,這個方法有兩個參數,一個是消息的接受者爲 id
類型,第二個個是方法編號 sel
。
做爲對比,run
方法就直接執行了,並無經過 objc_msgSend
進行消息發送:
LGStudent *s = [LGStudent new]; [s sayCode]; objc_msgSend(s, sel_registerName("sayCode")); 複製代碼
上述代碼表示的是向對象 s
發送 sayCode
消息。
id cls = [LGStudent class]; void *pointA = &cls; [(__bridge id)pointA sayNB]; objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB")); 複製代碼
上述代碼表示向 LGStudent
這個類發送 sayNB
消息。
// 向父類發消息(對象方法) struct objc_super lgSuper; lgSuper.receiver = s; lgSuper.super_class = [LGPerson class]; objc_msgSendSuper(&lgSuper, @selector(sayHello)); 複製代碼
上述代碼表示向父類發送 sayHello
消息。
//向父類發消息(類方法) struct objc_super myClassSuper; myClassSuper.receiver = [s class]; myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元類 objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB")); 複製代碼
上述代碼表示向父類的類,也就是元類發送 sayNB
消息。
咱們在 OC
中使用 objc_msgSend
的時候,要注意須要將 Enbale Strict of Checking of objc_msgSend Calls
設置爲 NO
。這樣纔不會報警告。
objc_msgSend
objc_msgSend
之因此採用彙編來實現,是由於
C
和 C++
來講不如彙編更駕輕就熟objc_msgSend
彙編源碼ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame cmp p0, #0 // nil check and tagged pointer check 複製代碼
判斷 p0
,也就是咱們 objc_msgSend
的第一個參數 id
消息的接收者是否爲空。
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
複製代碼
讀取 x0
而後賦值到 p13
,這裏 p13
拿到的是 isa
。爲何要拿 isa
呢,由於不管是對象方法仍是類方法,咱們都須要在類或者元類的緩存或方法列表中去查找,因此 isa
是必需的。
經過 GetClassFromIsa_p16
,將獲取到的 class
存在 p16
上面。
GetClassFromIsa_p16
源碼以下:
.macro GetClassFromIsa_p16 /* src */ #if SUPPORT_INDEXED_ISA // Indexed isa mov p16, $0 // optimistically set dst = src tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa // isa in p16 is indexed adrp x10, _objc_indexed_classes@PAGE add x10, x10, _objc_indexed_classes@PAGEOFF ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array 1: #elif __LP64__ // 64-bit packed isa and p16, $0, #ISA_MASK #else // 32-bit raw isa mov p16, $0 #endif .endmacro 複製代碼
這個方法的目的就是經過位移操做獲取 isa
的 shiftcls
而後進行位運算與操做獲得真正的類信息。
LGetIsaDone: CacheLookup NORMAL // calls imp or objc_msgSend_uncached 複製代碼
獲取完 isa
以後,接下來就要進行 CacheLookup
,查找方法緩存,咱們再來到 CacheLookup
的源碼處:
/******************************************************************** * * 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 * ********************************************************************/ .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 複製代碼
經過上述代碼可知 CacheLookup
有三種模式:NORMAL
,GETIMP
, LOOKUP
ldp p10, p11, [x16, #CACHE]
複製代碼
CacheLookup
須要讀取上一步拿到的類的 cache
緩存,而根據咱們前面對類結構的學習,這裏顯然進行 16 字節地址平移操做,而後把拿到的 cache_t
中的 buckets
和 occupied
、mask
賦值給 p10
, p11
。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 複製代碼
w1
和 w11
進行與操做,其實本質就是 _cmd
& mask
。這一步和咱們探索 cache_t
時遇到的
bucket
結構體指針,而後將這個結構體指針中的 imp
,sel
分別存在 p17
,p9
中。1: cmp p9, p1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 複製代碼
sel
和咱們要查找的 sel
(在這裏也就是所謂的 _cmd
)進行比較,若是匹配了,就經過 CacheHit
將 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 複製代碼
sel
沒有匹配上,咱們須要接着進行搜索。咱們來到 CheckMiss
的源碼:
.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 複製代碼
這裏因爲咱們是 NORMAL
模式,因此會來到 __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 複製代碼
__objc_msgSend_uncached
中最核心的邏輯就是 MethodTableLookup
,意爲查找方法列表。
咱們再來到 MethodTableLookup
的定義:
.macro MethodTableLookup // push frame SignLR 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 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 AuthenticateLR .endmacro 複製代碼
咱們觀察 MethodTableLookup
內容以後會定位到 __class_lookupMethodAndLoadCache3
。在 __class_lookupMethodAndLoadCache3
以前會作一些準備工做,真正的方法查找流程核心邏輯是位於 __class_lookupMethodAndLoadCache3
裏面的。 可是咱們全局搜索 __class_lookupMethodAndLoadCache3
會發現找不到,這是由於此時咱們會從彙編跳入到 C/C++
。因此去掉一個下劃線就能找到:
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } 複製代碼
objc_msgSend
以及其派生函數來實現的。objc_msgSend
爲了執行效率以及 C/C++ 不能支持參數未知,類型未知的代碼,因此採用彙編來實現 objc_msgSend
。lookUpImpOrForward
。咱們下一章將會從 lookUpImpOrForward
開始探索,探索底層的方法查找的具體流程究竟是怎麼樣的,敬請期待~