前言
在《cache底層分析》一文中詳細得剖析了cache
的底層原理以及其相關的流程基公。那么我們有沒有留意到cahche
調用insert
方法之前做了哪些操作呢?哪些操作又是以什么形式傳遞的呢妒茬?
那么查看objc-cache.mm文件的頭部注釋中寫著insert()的插入時機是通過最上層的objc_msgSend觸發(fā)的仰美,如下圖:
準備資料
runtime
runtime定義
編譯時
-
編譯時
顧名思義就是正在編譯的時候. 編譯器把源代碼翻譯成機器能識別的代碼(當然只是?般意義上這么說,實際上可能只是翻譯成某個中間狀態(tài)的語?)迷殿。編譯時通過語法分析、詞法分析等編譯時類型檢查(靜態(tài)類型檢查)
來發(fā)現(xiàn)代碼中的errors
或warning
等編譯時的錯誤信息咖杂。 -
靜態(tài)檢查
不會把代碼放內存中運?起來,?只是當作?本
來掃描檢查庆寺,?些?說編譯時還分配內存啥的肯定是錯誤的說法。
運行時
-
運?時
就是代碼通過dyld
被裝載到內存中執(zhí)行
的過程诉字。運?時類型檢查
就與前?講的編譯時類型檢查(或者靜態(tài)類型檢查
)不?樣懦尝。不是簡單的掃描代碼,?是在內存中做操作和判斷
壤圃。
runtime的版本
runtime
有兩個版本陵霉,一個Legacy
版本(早期版本),一個Modern
版本(現(xiàn)行版本)伍绳。
- 早期版本對應的編程接口:
Objective-C 1.0
- 現(xiàn)行版本對應的編程接口:
Objective-C 2.0
踊挠,源碼中經常看到的OBJC2
- 早期版本用于
Objective-C 1.0
冲杀,32
位的Mac OS X
的平臺 - 現(xiàn)行版本用于
Objective-C 2.0
止毕,iPhone
程序和Mac OS X v10.5
及以后的系統(tǒng)中的64
位程序
注意:runtime就是c/c++/匯編寫的一套API
。
runtime三種實現(xiàn)方式
-
Objective-C
方式漠趁,[penson sayHappy]
-Framework & Serivce
方式扁凛,isKindOfClass
-
Runtime API
方式,class_getInstanceSize
方法的本質
探究底層又兩個方式闯传,第一種就是看匯編代碼谨朝,其次就是C/C++編譯之后的代碼。如果分析匯編代碼的話會設計到寄存器數(shù)據(jù)的一系列讀取操作甥绿,過程比較繁瑣字币,那么我們就采用第二種方式來看看方法底層的實現(xiàn)是怎么樣子的。首先編譯生成main.cpp文件共缕,然后自定義XXPerson類洗出,在XXPerson類中添加實例方法,在main函數(shù)中調用如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXPerson *person = [[XXPerson alloc]init];
[person saySomething];
[person sayHappy:@"happy!"];
}
return 0;
}
xrun導出main.cpp文件图谷,查看到main函數(shù)的底層實現(xiàn)如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy:"), (NSString *)&__NSConstantStringImpl__var_folders_mq_n7r4vx491nz9b2b3wpmz1mg00000gn_T_main_12c37c_mi_1);
}
return 0;
}
分析:
- 通過底層的代碼翩活,方法的實現(xiàn)是通過
objc_msgSend
來發(fā)送的阱洪。 - 方法的本質其實就是
消息的發(fā)送
。
通過底層objc_msgSend來實現(xiàn)法法菠镇,情況如下:
-
objc_msgSend
能夠調用類的方法冗荸,跟對象調用的結果一樣。
注意:
- 運行項目之前必須導入
<objc/message.h>
頭文件利耍。 - 關閉
objc_msgSend
檢查機制:target --> Build Setting -->搜索objc_msgSend -- Enable strict checking of obc_msgSend calls設置為NO
蚌本。
調用類方法
創(chuàng)建XXPerson
類方法sayNB
,通過調用隘梨,已經底層的main.cpp
可以得出一下代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXPerson *person = [[XXPerson alloc]init];
[person saySomething];
[XXPerson sayNB];
}
return 0;
}
//底層代碼
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
XXPerson *person = ((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((XXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("XXPerson"), sel_registerName("sayNB"));
}
return 0;
}
- 類方法的調用也是通過
objc_msgSend
來進行消息發(fā)送程癌。
注意:
- 通過之前類結構的學習,底層是
不分
類方法跟實例方法的轴猎,只是查找方法的地方不一樣(實例方法保存在本類鐘嵌莉,類方法保存在元類中)
。 - 類方法其實就是
元類的實例方法
税稼。
調用父類方法
穿件XXTeacher類繼承XXPerson類,并用XXTeacher實例調用父類的saySomething方法如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXTeacher *teacher = [[XXTeacher alloc]init];
[teacher saySomething];
}
return 0;
}
用xrun
導出main.cpp
文件垮斯,查看底層代碼實現(xiàn)
在用XXTeacher.m文件重寫saySomething方法郎仆,然后用
xrun
把XXTeacher.m
生成XXTeacher.cpp
文件,查詢XXTeacher
函數(shù)的實現(xiàn):- 子類調用父類的方法可以通過
objc_msgSendSuper
來進行消息的發(fā)送
兜蠕,其本質也就是消息的發(fā)送
扰肌。 -
objc_msgSendSuper
是通過向父類發(fā)送消息,與objc_msgSend
流程有點不一樣熊杨。
objc_msgSendSuper的數(shù)據(jù)結構
通過查找objc4
的源碼發(fā)現(xiàn):
//objc_msgSendSuper的定義
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
查看參數(shù)objc_super
的結構如下(提取__OBJC2__
的部分):
struct objc_super {
//消息的接收者
__unsafe_unretained _Nonnull id receiver;
//方法最先查找的class是super_class ,如果super_class查找不到會查找super_class的父類
__unsafe_unretained _Nonnull Class super_class;
};
objc_msgSendSuper代碼案例
通過objc_msgSendSuper
的方式曙旭,調用XXPerson
的saySomething
方法:
-
objc_msgSendSuper
能夠向父類發(fā)送消息,調用父類的方法晶府。 - 方法調用桂躏,首先在本類中找,如果沒有就到父類中找川陆。
(receiver只是指定調用的是誰剂习,但是方法是在super_class找)
objc_msgSend匯編探究
首相我們在saySomething方法調用時候下個匯編斷點,如下圖:
然后我們進入objc_msgSend的匯編實現(xiàn)(打objc_msgSend的符號斷點)较沪,如下圖:
- 匯編顯示
objc_msgSend
在libobjc.A.dylib
系統(tǒng)庫鳞绕。 -
objc_msgSend
也可以在objc4
的源碼中找到。
objc_msgSend在objc4源碼中的實現(xiàn)
到這里源碼的實現(xiàn)尸曼,有些同學就會想到objc_msgSend
可能是c
或者是c++
來實現(xiàn)的们何。可是實踐告訴我們控轿,objc_msgSend
的底層實現(xiàn)在源碼中是匯編語言冤竹。
源碼查找流程:在objc
源碼中全局搜索objc_msgSend
拂封,找到真機的匯編objc-msg-arm64.s
源碼中寄存器的對應發(fā)生了一丟丟改變
(如p0 = x0)
,為了方便理解方法體代碼,如下圖:objc_msgSend
入口匯編代碼判斷
receiver
是否等于nil
贴见, 再判斷是否支持Taggedpointer
小對象類型烘苹。
- 支持
Taggedpointer
小對象類型,小對象為空 片部,返回nil
镣衡,不為nil
處理isa
獲取class
跳轉CacheLookup
流程 。 - 不支持
Taggedpointer
小對象類型且receiver = nil
档悠,跳轉LReturnZero
流程返回nil
廊鸥。 - 不支持
Taggedpointer
小對象類型且receiver != nil
,通過GetClassFromIsa_p16
把獲取到class
存放在p16
的寄存器中辖所,然后走CacheLookup
流程惰说。
GetClassFromIsa_p16
獲取Class
匯編流程
GetClassFromIsa_p16
核心功能獲取class
存放在p16
寄存器。(那么就是著重看ExtractISA方法的實現(xiàn))
ExtractISA
方法的匯編實現(xiàn)
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else
...
.macro ExtractISA //ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器
and $0, $1, #ISA_MASK // and 表示 & 操作缘回, $0 = $1(isa) & ISA_MASK = class
.endmacro
// not JOP
#endif
ExtractISA
主要功能isa & ISA_MASK = class
存放到p16
寄存器吆视。
重點:CacheLookup
匯編實現(xiàn)流程
在《cache底層分析》一文中已經根據(jù)objc4底層源碼分析過整個insert
的流程了,那么通過CacheLookup
匯編的形式來看看這個流程跟之前的是否能夠銜接上酥宴,拭目以待@舶伞!
buckets
和下標index
源碼分析:
- 獲取
_bucketsAndMaybeMask
地址也就是cache
的地址:p16 = isa(class)
拙寡,p16 + 0x10 = _bucketsAndMaybeMask = p11
授滓。 - 獲取buckets容器的首地址:
buckets = _bucketsAndMaybeMask & 0xffffffffffff(maskShift不同架構也會不同)
。 - 獲取
hash
下標:p12 =(cmd ^ ( _cmd >> 7))& msak
這一步的作用就是獲取hash
下標index
肆糕。
流程:isa
--> _bucketsAndMaybeMask
-->buckets
-->hash
-->index
般堆。
遍歷緩存
源碼分析:
- 根據(jù)下標
index
找到index
對應的bucket
。p13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))
诚啃。 - 先獲取對應的
bucket
然后取出imp
和sel
存放到p17
和p9
淮摔,然后*bucket--
向前移動。 -
1
流程:p9= sel
和 傳入的參數(shù)_cmd
進行比較始赎。如果相等走2
流程噩咪,如果不相等走3流程。 -
2
流程:緩存命中直接跳轉CacheHit
流程极阅。 -
3
流程:判斷sel = 0
條件是否成立胃碾。如果成立說明buckets
里面沒有傳入的參數(shù)_cmd
的緩存,沒必要往下走直接跳轉__objc_msgSend_uncached
流程筋搏。如果sel 仆百!= 0
說明這個bucket
被別的方法占用了。你去找下一個位置看看是不是你需要的奔脐。然后在判斷下個位置的bucket
和第一個bucket
地址大小俄周,如果大于第一個bucket
的地址跳轉1
流程循環(huán)查找吁讨,如果小于等于則接繼續(xù)后面的流程。 - 如果循環(huán)到第
1
個bucket
里都沒有找到符合的_cmd
峦朗。那么會接著往下走建丧,因為下標index
后面的可能還有bucket
還沒有查詢。
CacheHit
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
...
#else //這是我們需要研究的
.macro TailCallCachedImp
// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class(也就是isa)
eor $0, $0, $3 // $0 = imp ^ class 這一步是對imp就行解碼波势,獲取運行時的imp地址
br $0 //調用 imp翎朱,意思是找到方法了并調用了
.endmacro
...
#endif
緩存查詢到以后直接對bucket
的imp
進行解碼
操作。即imp = imp ^ class
尺铣,然后調用解碼后的imp
拴曲。
遍歷緩存流程圖
帶著疑問:為什么sel = 0 的時候就直接跳出了緩存的查找呢?
分析得出:
- 如果既沒有
hash沖突
又沒有目標方法的緩存
凛忿,那么hash
下標對應的bucket
就是空的直接跳出緩存查找澈灼。 - 不會出現(xiàn)中間是有空的
bucket
,兩邊有目標bucket
這種情況店溢。
mask
向前遍歷緩存
分析:
- 找到最后一個
bucket
的位置:p13 = buckets + (mask << 1+3)
找到最后一個bucket
的位置叁熔。 - 先獲取對應的
bucket
然后取出imp
和sel
存放到p17
和p9
,然后*bucket--
向前移動床牧。 -
p9= sel
和 傳入的參數(shù)_cmd
進行比較荣回。如果相等走2
流程。 - 如果不相等在判斷(
sel 叠赦!= 0 && bucket > 第一次確定的hash下標bucket
)接著循環(huán)緩存查找驹马,如果整個流程循環(huán)完仍然沒有查詢到或者遇到空的bucket
革砸。說明該緩存中沒有緩存)sel = _cmd
的方法除秀,緩存查詢結束跳轉__objc_msgSend_uncached
流程。 -
mask
向前遍歷和前面的循環(huán)遍歷邏輯基本一樣算利。