07-msg_send()在背后付出了什么之快速查找流程分析

引言

當(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), 那我就有問題了, 它是怎么查找的呢?

要思考的問題:

  1. 如果類沒有實現(xiàn), 我們調(diào)用流程會有什么變化呢? 父類沒有實現(xiàn)呢? 根類也沒有實現(xiàn)呢
  2. 如果我們類的分類實現(xiàn), 流程又是怎樣的呢?給NSObject的分類擴展方法實現(xiàn)呢?

: 參考isa的走位和繼承圖就不難得知

  1. 調(diào)用方法會根據(jù)繼承關(guān)系一層層往父類去找它的實現(xiàn)如果找到則返回, 如果沒有就會報錯提示
  2. 分類(包括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)鍵字, 這是匯編的入口, 詳細注釋如下圖

文件入口-c275

objc_msgSend匯編實現(xiàn)-c718

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

GetClassFromIsa_p16獲取類流程-c923

注意:

  • p16是類內(nèi)存指針, 相當(dāng)于對象的isa指針和掩碼``與后的內(nèi)存指針

3. 來看CacheLookup快速查找流程

CacheLookup01-c1052

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

buckets指針偏移圖示-c1198

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

遞歸流程01-c881

遞歸流程02-c866
JumpMiss-c451

總結(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é)分享

__objc_msgSend_uncached-c686

MethodTableLookup-c935

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

整體流程圖如下:

objc_msgSend查找cache方法流程圖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容