引言
當(dāng)我們稍稍跨進底層大門的時候, 我們就應(yīng)該發(fā)現(xiàn), 我們平常所調(diào)用的一個個方法, 都會編譯成
objc_msgSend函數(shù)體. 我們來驗證下:
1. 首先我們定義個類文件, 實現(xiàn)兩個方法并調(diào)用:
@interface LLPerson : NSObject
- (void)sayHello;
- (void)sayHappy;
@end
---------------------
@implementation LLPerson
- (void)sayHello{
NSLog(@"sayHello");
}
- (void)sayHappy{
NSLog(@"sayHappy");
}
---------------------
LLPerson *person = [LLPerson alloc];
[person sayHappy];
[person sayHello];
@end
2. 接下來我們來利用xcrun命令來看下我們的調(diào)用方法被翻譯成底層源碼是什么形式:
LLPerson *person = ((LLPerson *(*)(id, SEL))(void *)objc_msgSend)
((id)objc_getClass("LLPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
// objc_msgSend的聲明
// id self: 第一個隱式參數(shù)->對象自身
// SEL: 方法名. 因無法直接打印該格式轉(zhuǎn)換成->sel_registerName("xxx")
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
可以驗證我們調(diào)用的方法確實是被翻譯成Runtime中的objc_msgSend函數(shù)體是去實現(xiàn)的
一: 那什么是Runtime呢?
-
定義:
- 我們常說的
運行時(用匯編寫的, 通過編譯時(為什么?:速度快), 翻譯成機器能識別的語言), 通俗講就是代碼跑起來后, 被裝到了內(nèi)存里
- 我們常說的
-
作用:
- 在
程序運行過程中,動態(tài)的創(chuàng)建類,動態(tài)添加、修改這個類的屬性和方法; - 遍歷并返回一個類中所有的
成員變量、屬性、以及所有方法 消息傳遞、轉(zhuǎn)發(fā)
- 在
-
runtime的使用有以下三種方式,其三種實現(xiàn)方法與編譯層和底層的關(guān)系如圖所示- 通過OC代碼,例如 [person sayHappy]
- 通過NSObject方法,例如isKindOfClass
- 通過Runtime API,例如class_getInstanceSize
-
說到運行時, 那就要補充一個知識點-->
編譯時-
編譯時: 顧名思義就是正在編譯的時候 . 那啥叫編譯呢?就是編譯器幫你把
源代碼翻譯成機器能識別的代碼 - 作用就是簡單的負責(zé)代碼的
翻譯工作, 并分析代碼關(guān)鍵字和語法之類有沒有錯誤 - 編譯時不會分配內(nèi)存
- 與runtime的關(guān)系-c782
-
- 它的工作流程很明顯是通過
對象和SEL去查找SEL對應(yīng)的IMP實現(xiàn), 那我就有問題了, 它是怎么查找的呢?
要思考的問題:
- 如果類沒有實現(xiàn), 我們調(diào)用流程會有什么變化呢? 父類沒有實現(xiàn)呢? 根類也沒有實現(xiàn)呢
- 如果我們類的分類實現(xiàn), 流程又是怎樣的呢?給NSObject的分類擴展方法實現(xiàn)呢?
答: 參考
isa的走位和繼承圖就不難得知
- 調(diào)用方法會根據(jù)
繼承關(guān)系一層層往父類去找它的實現(xiàn)如果找到則返回, 如果沒有就會報錯提示- 分類(包括
NSObject)擴展的方法會被存儲到類的信息里, 在結(jié)合問題1的思路, 就知道最終是能找到并調(diào)用方法的實現(xiàn)的.
二: objc_msgSend查找方法的流程
回顧: 上面我們看到objc_msgSend的聲明時, 留下個疑問: 怎么根據(jù)對象和SEL去查找相對應(yīng)的IMP呢? ---> 看objc的源碼
首先我們在objc源碼中全局搜索 objc_msgSend, 發(fā)現(xiàn)找到很多相關(guān)類別, 該選哪個呢? 前面我們有說runtime是用匯編寫的, 那我們要找它的實現(xiàn)就要去匯編代碼里找
拓展:
.h:聲明文件
.mm:c++實現(xiàn)文件
.s:匯編實現(xiàn)文件(arm64: 真機)
ENTRY:匯編進入指令
cache查找即為快速查找, 因為cache是存儲在內(nèi)存中, 并針對擴容和清理舊緩存做了優(yōu)化
涉及到cache的查找實現(xiàn), 都去匯編源碼中找就好了
1. 我們要看的是真機模式, 所以我們?nèi)rm64中查找相關(guān)實現(xiàn), 注意查找ENTRY關(guān)鍵字, 這是匯編的入口, 詳細注釋如下圖


2. 來看下匯編是怎么通過類獲取isa的

注意:
-
p16是類內(nèi)存指針, 相當(dāng)于對象的isa指針和掩碼``與后的內(nèi)存指針
3. 來看CacheLookup快速查找流程

總結(jié):
先通過哈希算法(_cmd & mask)找到我們起始查詢位置的下標(biāo)index, 通過buckets(即第一個bucket的地址)的指針偏移index乘16(bucket內(nèi)存占據(jù)16字節(jié))個位置, 來得到起始查詢位置的bucket, 以及它的imp和sel. 即: bucket = buckets + (index * 16)

4. 接下來, 我們來看它是怎么遞歸查找緩存里有沒有參數(shù)SEL的



總結(jié):
通過對比起始位和參數(shù)的sel是否相等之后, 蘋果通過偏移指針--bucket查找起始位之前的bucket, 如果沒有找到則繼續(xù)偏移指針到buckets的最后一個bucket繼續(xù)向前遞歸查詢, 直到再查詢到起始位置的(一開始通過哈希算法算出的index上)的bucket, 確保整個buckets都被掃描完一遍.如果期間查找到則CacheHit, 沒找到則jumpMiss, 根據(jù)JumpMiss的匯編實現(xiàn)(我們傳入的normal), 返回__objc_msgSend_uncached.
至此整個cache快速查找的匯編流程就走完了, 接下來如果還沒找到, 蘋果則會進入慢速查找sel的流程, 敬請期待下節(jié)分享


總結(jié): 可以看出找不到查找目標(biāo)時, 最終會進入lookUpImpOrForward(慢速查找流程)
整體流程圖如下:

