上一節(jié)伐憾,我們了解了map_images的整體結(jié)構(gòu) & 非懶加載類辅斟,了解了APP啟動
時,所有類
都已記錄
在哈希表
中(僅類名字
和地址
)生真。
- 實現(xiàn)
類+load方法
的非懶加載類
,會在啟動時
奕筐,實現(xiàn)類的加載,從macho
中讀取原始數(shù)據(jù)
存放到rw
? - 而
懶加載類
則是在被第一次調(diào)用
時衔蹲,通過消息機制
觸發(fā)類的實現(xiàn)
坚踩。
兩種類的加載方式,最終都是調(diào)用realizeClassWithoutSwift
完成實現(xiàn)毡惜。
上節(jié)回顧:
我們上一節(jié)留下了2個問題:rwe
何時加載拓轻?分類
如何加載?
- 現(xiàn)在不急著回答,本節(jié)結(jié)束后经伙,我相信你就完全懂了扶叉。
本節(jié)盡可能講得詳細一些:
- sel注冊
- 分類的本質(zhì)
- 分類的數(shù)據(jù)加載
- attachCategories詳解
- attachCategories的調(diào)用
準備工作:
- 可編譯的
objc4-781
源碼: http://www.reibang.com/p/45dc31d91000dyld-750.6
: https://opensource.apple.com/tarballs/dyld/
1. sel注冊
我們在前面學習msgSend消息機制
時,慢速查找階段
中,在類的函數(shù)列表
查找方法時枣氧,是使用二分查找
(??流程圖)溢十。
Q: 二分查找
必須是有序的
,那排序依據(jù)
是什么达吞,如何排序
茶宵?
- 上一節(jié)我們分析
map_images
流程時,在第2步 修復預編譯階段的SEL的混亂問題
時宗挥,就需要將SEL
插入到nameSelectors
哈希表中乌庶。
- 其中
_getObjc2SelectorRefs
是macho
的__objc_selrefs
,存儲的內(nèi)容是SEL
:
image.png
- 遍歷從
macho
的__objc_selrefs
讀取SEL
契耿,其中的sels
包含的是帶地址
的sel
(后面證明)瞒大。 - 循環(huán)注冊sel,
檢查sel地址
搪桂,如果不同透敌,就重新賦值sel地址
進入sel_registerNameNoLock:
- 進入
__sel_registerName
:
- 一般是可以通過
name
搜索到result
,直接返回result
踢械。 - 但如果特殊情況
name
搜索不到酗电,就重新創(chuàng)建,再返回sel
内列。
我們進入search_builtins
來了解查詢路徑:
- 發(fā)現(xiàn)
_dyld_get_objc_selector
是extern
申明在dyld
中:
// Called only by objc to see if dyld has uniqued this selector.
// Returns the value if dyld has uniqued it, or nullptr if it has not.
// Note, this function must be called after _dyld_objc_notify_register.
//
// Exists in Mac OS X 10.15 and later
// Exists in iOS 13.0 and later
extern const char* _dyld_get_objc_selector(const char* selName);
- 打開
dyld源碼
撵术,搜索_dyld_get_objc_selector(const
:
- 進入
getObjCSelector
:
- 發(fā)現(xiàn)是調(diào)用
getString
方法在讀取內(nèi)容,所以我們反向搜索getString(const
话瞧,檢查函數(shù)的實現(xiàn):
- 通過這里嫩与,我們就明確知道了:
sel
雖然是函數(shù)名(字符串)
,但同時它是有地址值
的交排。
拓展:
函數(shù)地址
完全隨機
划滋,是由它所在的段基礎地址
和偏移值
確定的。程序每次運行埃篓,函數(shù)地址
都可能變化
处坪。- 判斷兩個
函數(shù)是否相等
,是通過地址值
進行判斷
兩個不同類
有相同名稱
的函數(shù)
架专,但函數(shù)地址不同同窘,是兩個獨立的函數(shù)
。- 函數(shù)列表排序胶征,是依據(jù)
SEL地址
進行排序
塞椎。所以排序后,可使用二分查找睛低。
2.分類的本質(zhì)
-
main.m
文件加入測試代碼
:
// 本類
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)func1;
- (void)func3;
- (void)func2;
+ (void)classFunc;
@end
@implementation HTPerson
+ (void)load { NSLog(@"%s",__func__); };
- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };
+ (void)classFunc { NSLog(@"%s",__func__); };
@end
// 分類 CatA
@interface HTPerson (CatA)
@property (nonatomic, copy) NSString *catA_name;
@property (nonatomic, assign) int catA_age;
- (void)func1;
- (void)func3;
- (void)func2;
+ (void)classFunc;
@end
@implementation HTPerson (CatA)
+ (void)load { NSLog(@"%s",__func__); };
- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };
+ (void)classFunc { NSLog(@"%s",__func__); };
@end
int main(int argc, const char * argv[]) {
return 0;
}
檢查格式的方式:1. clang 2. 官方幫助文檔
2.1 clang
cd
到main.m
所在文件夾,輸入clang -rewrite-objc main.m -o main.cpp
,打開main.cpp
文件钱雷,搜索分類_CatA
:-
分類的
實例方法
和類方法
:
image.png -
分類的
屬性
:
image.png -
分類的
結(jié)構(gòu)
:
image.png
- 我們搜索
struct _category_t
骂铁,可看到分類的完整格式
:
image.png發(fā)現(xiàn)
編譯期
的HTPerson(CatA)
:name
是HTPerson
,cls
也是HTPerosn類
- 分類的
實現(xiàn)
:
image.png
本類屬性
和分類屬性
的區(qū)別:
本類屬性:在
clang
編譯環(huán)節(jié)罩抗,會自動生成并實現(xiàn)
對應的set和get方法分類屬性:會存在set拉庵、get方法,但是
沒有實現(xiàn)
(需要runtime設置關(guān)聯(lián)屬性
)套蒂。易混淆點: 分類屬性存在
set
钞支、get
方法,但沒有實現(xiàn)
操刀。
檢驗方式: 使用person對象
可以快捷訪問到catA_age
烁挟,并可以賦值
。但是程序運行時
會crash
骨坑。 這是因為方法存在撼嗓,但找不到
對應的imp實現(xiàn)
。
image.png
Q: 1.分類屬性
為何存在set
欢唾、get方法
且警? 2.如何
讓它不crash
(關(guān)聯(lián)屬性的動態(tài)實現(xiàn))第1個問題在本節(jié)后續(xù)探索中,會得到很清晰的答案礁遣。 第2個問題斑芜,我們下一節(jié)專門講解
關(guān)聯(lián)屬性
。
- 2.2 官方幫助文檔
打開官方文檔 (快捷鍵:shift + command + 0
)祟霍,搜索Categor
:
-
切換語言為
Objective-C
:
image.png 發(fā)現(xiàn)類型是
objc_category
押搪,在objc4源碼
中搜索:
- ??
格式不一樣?name呢浅碾?cls呢大州?
- ?? 注意看后面的聲明:
OBJC2_UNAVAILABLE
, objc2不可用垂谢。文檔
是已過期
的厦画。這個時候,我們要以真實運行的代碼為準
滥朱。
了解了分類
的數(shù)據(jù)格式
根暑,那分類的數(shù)據(jù)
是如何加到HTPerson
的呢?
3. 分類的加載
如何研究呢徙邻?
- 從
已知
的信息出發(fā)
排嫌,先找
到一條
抵達目的地的路徑
,找到核心方法
缰犁,再反向搜索
核心方法被調(diào)用的地方
淳地,進行全面推理
怖糊。
我們上一節(jié)分析_read_images
結(jié)構(gòu)時,第9步 實現(xiàn)非懶加載類
->methodizeClass
內(nèi)部有對分類的處理
颇象。
- 在
methodizeClass
中加入測試代碼:
// >>>> 測試代碼
const char *mangledName = cls->mangledName();
const char * HTPersonName = "HTPerson";
if (strcmp(HTPersonName, mangledName) == 0 ) {
if (!isMeta) {
printf("%s - 精準定位: %s\n", __func__, mangledName);
}
}
// <<<< 測試代碼
- 在
printf
打印處加入斷點伍伤,運行程序
- 發(fā)現(xiàn)進入了
HTPerosn
類,查看ro
信息遣钳,發(fā)現(xiàn)其中baseMethods
只有8個
扰魂,分別打印查看,都是HTPerosn本類
的實例函數(shù)
蕴茴。 從信息欄可以看rwe
此時為Null
ro的讀热捌馈:
image.png
- 單步往下運行,發(fā)現(xiàn)最終會到達
attachToClass
處:
methodizeClass
的內(nèi)容是:
- 讀取
函數(shù)(已排序)
存到list
-> 讀取屬性
存到proplist
-> 讀取協(xié)議
存到protolist
->分類添加到類
中attachToClass
有個細節(jié)倦淀,我們發(fā)現(xiàn)
initialize
在這里被添加到根元類
的函數(shù)列表
了蒋畜。根元類
擁有initialize
方法,所有繼承
自NSObject
的類晃听,都將擁有initialize
方法百侧。我們知道
+load
方法會將懶加載類
轉(zhuǎn)變?yōu)?code>非懶加載類,在app啟動前
就完成
了所有非懶加載類
的加載
能扒。但是app啟動環(huán)節(jié)加載過多內(nèi)容佣渴,會影響
app的啟動時長
。
- Q:有些準備必須在
類初始化之前
就完成初斑,如果不寫
在+load
方法內(nèi)辛润,怎么
做到提前準備
呢?- A:寫在
initialize
內(nèi)见秤,因為每個類都繼承自NSObject
砂竖,所以都自帶了initialize
函數(shù),而initialize
函數(shù)是在類第一次發(fā)送消息
時鹃答,就觸發(fā)
乎澄。 所以可以做到提前準備
。
- 進入
attachToClass
测摔,加入測試代碼:
看到了關(guān)鍵的attachCategories
函數(shù):綁定分類置济。
- 如果是
元類
,需要分別綁定對象
和類方法
锋八。否則浙于,只需要綁定對象
方法。
注意挟纱,此時測試代碼中
HTPerson
和HTPerson(CatA)
都必須實現(xiàn)+load
方法羞酗,才會進入attachCategories
代碼區(qū)域) 具體原因,后面第5部分 本類與分類的+load區(qū)別
會詳細講解紊服。
下面檀轨,我們詳細分析一下attachCategories
:
4. attachCategories詳解
進入attachCategories
胸竞,加入定位測試代碼
:
開辟了64個
空間大小的mlists
、proplists
裤园、protolists
容器撤师,分別用于存儲函數(shù)
剂府、屬性
拧揽、協(xié)議
。
attachCategories
流程:
- 首先腺占,
開辟空間
淤袜,對rwe
進行初始化
。 - 然后衰伯,
遍歷
所有的分類
:
entry
記錄當前分類
铡羡,entry.cat是category_t
結(jié)構(gòu),存儲了分類所有數(shù)據(jù)意鲸。
從分類中讀取函數(shù)
烦周、屬性
、協(xié)議
信息怎顾,存放
到指定容器
內(nèi)读慎。 - 最后,將
容器內(nèi)數(shù)據(jù)
槐雾,分別添加
到rwe
指定屬性中夭委。
此處分為3小部分講解:
- rwe的初始化
- 數(shù)據(jù)讀取
- prepareMethodLists函數(shù)排序
- attachLists 綁定數(shù)據(jù)
4.1 rwe的初始化
哈哈哈 ?? 走過千山萬水,終于找到你募强,我的rwe
- 進入
extAllocIfNeeded
:
- 進入
extAlloc
:
此時株灸,rwe
才完成了初始化工作
。各項屬性完備
擎值。(關(guān)于attachLists
賦值操作慌烧,在4.3小部分
進行講解)
關(guān)于rwe何時加載的問題:
我們現(xiàn)在知道分類加載
會進行rwe初始化
和加載數(shù)據(jù)
。那還有其他地方
會觸發(fā)rwe
的加載嗎鸠儿?
-
rwe
的加載屹蚊,是執(zhí)行了extAlloc
方法,所以我們反向搜索
捆交,查看誰調(diào)用
了extAlloc
方法:
只有extAllocIfNeeded
和deepCopy
調(diào)用了淑翼。
deepCopy深拷貝
: 搜索deepCopy(
,發(fā)現(xiàn)只被objc_duplicateClass
調(diào)用品追,而是objc_duplicateClass
開放使用的API
接口玄括,并沒自動調(diào)用
的地方。 所以此處不做考慮肉瓦。-
extAllocIfNeeded
: 搜索extAllocIfNeeded(
遭京,發(fā)現(xiàn)有以下7處
調(diào)用了它:
image.png 發(fā)現(xiàn)都是
動態(tài)添加(函數(shù)胃惜、屬性、協(xié)議哪雕、分類等)
時船殉,才會創(chuàng)建rwe
。
還記得上面ro的讀取
嗎斯嚎?
- 當
rwe存在
時:表示這個類有數(shù)據(jù)被修改
了利虫,所以需要從rwe返回數(shù)據(jù)
。 - 而如果
rwe不存在
堡僻,表明這個類的數(shù)據(jù)沒有
被動態(tài)修改
過糠惫,所以可以直接從macho
中拷貝
一份ro
返回即可。
附上
WWDC2020視頻
Advancements in the Objective-C runtime钉疫,回顧官方對于rwe
的解釋硼讽,會理解得更深刻。
4.2 數(shù)據(jù)讀取和prepareMethodLists
函數(shù)排序
初始化rwe
后牲阁,我們讀取分類數(shù)據(jù)
:
- 查看
entry.cat
結(jié)構(gòu):
- 查看
category_t
結(jié)構(gòu)固阁,發(fā)現(xiàn)存儲了分類所有數(shù)據(jù)。
image.png
所以分類的數(shù)據(jù)都是從entry.cat
進行讀取城菊。
- 我們在上面
定位測試代碼
的打印處
加上斷點备燃,運行代碼
,到達斷點后役电,往下進入循環(huán)
內(nèi):
image.png- 發(fā)現(xiàn)此時name已從編譯時的
HTPerson
變成了CatA
赚爵,而我們的cls
仍舊是HTPerson
:
(類地址在內(nèi)存中是唯一的,地址相同表示是一個類)
image.png
- 下面以
函數(shù)
的讀取
為例法瑟,(屬性冀膝、協(xié)議的讀取和賦值方式一樣):
image.png
將分類的methods
函數(shù)列表讀取到mlist
,如果存在:
- 如果
數(shù)組
是否已滿(64)
霎挟,將mlist
內(nèi)部排序
后窝剖,調(diào)用attachLists
存到rwe
的methods
中,并將mcount歸零酥夭。 - 將
mlist
倒序插入到mlists
中
屬性
和協(xié)議
也是相同的操作方式赐纱,只是讀取的內(nèi)容
和存入的容器
不同而已。
- 至此熬北,已遍歷分類疙描,將分類的
函數(shù)、屬性讶隐、協(xié)議
都分別存儲到mlists
起胰、proplists
、protolists
中了巫延。
接下來效五,是將他們賦值給rwe
對應屬性
:
4.3 prepareMethodLists
函數(shù)排序
函數(shù)
在插入前地消,都會預先
進行一輪排序
,進入prepareMethodLists
:
- 進入
fixupMethodList
:
- 執(zhí)行完
prepareMethodLists
函數(shù)后畏妖,我們p mlists
打印容器脉执,p $7[63]
取出剛才存放在最后的mlist
,p $8->get(index)
打印數(shù)據(jù):
發(fā)現(xiàn)排序后的順序為: [ func1, func3 , func2 ]
戒劫,確實不是根據(jù)sel字符串
進行的排序半夷。
- 我們使用
p/x $8->get(0)
,打印SEL地址:
-
0x0000000100003e12
<0x0000000100003e18
<0x0000000100003e1e
谱仪,發(fā)現(xiàn)我們SEL地址
確實是從小到大排列的玻熙。
所以驗證了:
函數(shù)的排序:不是
根據(jù)SEL字符串
排序否彩,也不是
通過imp
進行排序疯攒,而是
通過SEL地址
進行排序
- 排序后,我們通過
attachLists
完成數(shù)據(jù)的綁定
4.4 attachLists 綁定數(shù)據(jù)
- 進入
attachLists
:
拓展函數(shù):
memcpy(開始位置列荔,放置內(nèi)容敬尺,占用大小)
:內(nèi)存拷貝
memmove(開始位置,移動內(nèi)容贴浙,占用大小)
:內(nèi)存平移
LRU算法:
Least Recently Used
的縮寫砂吞,最近最少使用
算法,越容易被調(diào)用(訪問)的放前面
崎溃。回想一下蜻直,不管我們是
動態(tài)插入
函數(shù),還是添加分類
袁串,一定是有需求時才這么操作概而。而新加入的數(shù)據(jù),明顯訪問頻率
會高于
默認模板內(nèi)容
囱修。所以我們addedLists
使用LRU
算法赎瑰,將舊數(shù)據(jù)
放在最后面
,新數(shù)據(jù)
永遠插入最前面
破镰。 這樣可以提高查詢效率
餐曼,減少運行時資源的占用
。
這里有3種情況:
- 0->1: 首次加入鲜漩,直接將addedLists[0]
賦值給list
源譬,是一維數(shù)組
。
(首次加載是本類
數(shù)據(jù)在extAllocIfNeeded
時孕似,從macho
中讀取ro
中的對應數(shù)據(jù)
加入)
- 1->多: 此時擴容為二維數(shù)組
踩娘,舊數(shù)據(jù)
插入后面
,新數(shù)據(jù)
插入前面
:
將數(shù)組擴容
到newCount
大小
-> array()
的count
記錄個數(shù)
-> 如果有舊數(shù)據(jù)
鳞青,插入
到lists
容器尾部
-> 調(diào)用memcpy內(nèi)存拷貝
霸饲,從array()首地址
開始为朋,將addedLists
插入,占用addedCount
個元素大小厚脉。
- 多 -> 更多: 類似于1->多
的操作习寸,也是舊數(shù)據(jù)
移到后面
,新數(shù)據(jù)
插入前面
將數(shù)組擴容
到newCount
大小
-> array()
的count
記錄個數(shù)
-> 調(diào)用memmove內(nèi)存評議
傻工,從array()首地址偏移addedCount個元素位置
開始霞溪,移動array()舊數(shù)據(jù)
,占用oldCount
個元素大小
-> 調(diào)用memcpy內(nèi)存拷貝
中捆,從array()首地址
開始鸯匹,將新數(shù)據(jù)addedLists
插入,占用addedCount
個元素大小泄伪。
所以這里rwe
的函數(shù)殴蓬、屬性、協(xié)議
都是attachLists
進行處理后完成的賦值蟋滴。
5. attachCategories的調(diào)用
此時染厅,我們通過一條線,完整熟悉了attachCategories
將分類數(shù)據(jù)
添加到rwe
中的整個流程和細節(jié)津函。
- 我們可以反過來搜索
attachCategories
被哪些地方調(diào)用:
我們發(fā)現(xiàn)肖粮,除了我們已分析的attachToClass
函數(shù),就只有load_categories_nolock
函數(shù)調(diào)用了attachCategories
尔苦。
- 進入
load_categories_nolock
涩馆,加入測試代碼:
const char *mangledName = cls->mangledName();
const char * HTPersonName = "HTPerson";
if (strcmp(HTPersonName, mangledName) == 0 ) {
auto ht_ro = (const class_ro_t *)cls->data();
auto ht_isMeta = ht_ro->flags & RO_META;
if (!ht_isMeta) {
printf("%s - 精準定位: %s\n", __func__, mangledName);
}
}
- 再檢查
load_categories_nolock
在哪里被調(diào)用:
第一處被調(diào)用:loadAllCategories
繼續(xù)搜索loadAllCategories
,發(fā)現(xiàn)在load_images
被調(diào)用:
第二處被調(diào)用:_read_images
的第8步 分類的加載
允坚。
- 而
_read_images
的加載魂那,是從map_images
過來的。
總結(jié):
分類的加載
屋讶,總得來說有2個
大的調(diào)用路徑
:
map_images
->map_images_nolock
->_read_images
有2個可能路徑:
路徑一:第8步 分類的處理
->load_categories_nolock
->attachCategories
路徑二:第9步 實現(xiàn)非懶加載類
->realizeClassWithoutSwift
->methodizeClass
->attachToClass
->attachCategories
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
至此冰寻,文初的2個問題,rwe
何時加載皿渗?分類
如何加載? 相信大家都十分清楚了
本節(jié)斩芭,我們已經(jīng)熟悉了分類
的加載方式
。
- 但是我們一切研究都是在
本類
和分類
都實現(xiàn)+Load
方法的前提乐疆,那其他組合的情況是怎樣呢划乖? -
attachCategories
這些調(diào)用路徑在什么情況下進入哪條路徑呢?
下一節(jié)OC底層原理十九:類的加載(下) 本類與分類load區(qū)別 & 關(guān)聯(lián)屬性挤土,我們將所有情況都一一分析琴庵。