前面已經(jīng)探究了類的加載
流程洪唐,類分為懶加載類
和非懶加載類
,他們有不同加載流程客扎,下面來探究下分類的加載
饺律,以及分類和類搭配使用的情況
分類的本質(zhì)
準備工作
在 main.m
中定義 HTPerson
的分類HT
, 代碼如下
探索分類本質(zhì)的三種方法
探索分類的本質(zhì),有以下三種方式
- 【方式一】通過
clang
- 【方式二】通過
Xcode
文檔搜索Category
- 【方式三】通過
objc
源碼搜索category_t
【方式一】:通過clang
通過clang -rewrite-objc main.m -o main.cpp
命令矢棚,查看編譯后的 c++文件
- 其中
分類
的類型是_category_t
郑什,存儲了相應(yīng)的實例方法
、類方法
蒲肋、屬性
蘑拯、協(xié)議
等信息
搜索struct _category_t
,如下所示
- 其中有兩個
_method_list_t
兜粘,分別對應(yīng)對象方法
和類方法
- 全局搜索
_CATEGORY_INSTANCE_METHODS_HTPerson_
申窘,找到其底層實現(xiàn)
- 查看
協(xié)議
和屬性
的結(jié)構(gòu)
這里我們發(fā)現(xiàn)一個【問題】:分類中定義的屬性沒有相應(yīng)的set、get方法
孔轴,我們可以通過關(guān)聯(lián)對象
來設(shè)置(關(guān)于如何設(shè)置關(guān)聯(lián)對象
剃法,我們將在下一篇中進行分析)
【方式二】:通過Xcode文檔搜索 Category
通過快捷鍵command+shift+0
,搜索Category
【方式三】:通過objc源碼搜索 category_t
通過objc818
源碼搜索category_t
類型
分類的加載的源碼分析
分類的底層結(jié)構(gòu)是結(jié)構(gòu)體category_t
路鹰,下面我們就來探究 分類是何時加載進來的贷洲,以及加載的過程
分類加載的引入
WWDC2020中關(guān)于數(shù)據(jù)結(jié)構(gòu)的變化(Class data structures changes)視頻地址收厨,蘋果為分類和動態(tài)添加專門分配的了一塊內(nèi)存rwe
,因為rwe
屬于dirty memory
优构,所以肯定是需要動態(tài)開辟內(nèi)存诵叁。下面從class_rw_t
中去查找相關(guān)rwe
的源碼
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
// ...省略代碼
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
// 判斷rwe是否存在
if (fastpath(v.is<class_rw_ext_t *>())) {
// 如果已經(jīng)有rwe數(shù)據(jù),直接返回地址指針
return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
} else {
// 為rwe開辟內(nèi)存并且返回地址指針
return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
}
}
class_rw_ext_t *deepCopy(const class_ro_t *ro) {
return extAlloc(ro, true);
}
// ...省略代碼
}
從代碼可以看出俩块,extAllocIfNeeded
方法用來開辟rwe
內(nèi)存黎休,全局搜索extAllocIfNeeded
,在下列幾個地方有相關(guān)調(diào)用:
-
attachCategories
方法:添加分類信息 demangledName
-
class_setVersion
:設(shè)置類的版本 -
addMethods_finish
:動態(tài)添加方法 -
class_addProtocol
:動態(tài)添加協(xié)議 -
_class_addProperty
:動態(tài)添加屬性 objc_duplicateClass
本文主要來探究分類的加載
玉凯,??我們來分析attachCategories
方法做了什么
attachCategories 反推法
attachCategories
方法势腮,源碼如下:
// 將分類的 方法列表、屬性漫仆、協(xié)議等數(shù)據(jù)加載到 類中
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
// 獲取rwe
auto rwe = cls->data()->extAllocIfNeeded();
// 遍歷所有的分類
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[I];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
// 當(dāng)mlists的個數(shù)為 64時捎拯,對方法進行排序,然后將 mlists加載到rwe中
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 如果 mcount = 0盲厌,mlist存放的位置在63個位置署照,總共是0 ~ 63, mlists最多存放64個方法列表(mlist)
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 處理屬性數(shù)據(jù)
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
// 處理協(xié)議相關(guān)信息
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategories
準備分類的數(shù)據(jù)吗浩,然后調(diào)用attachLists
將數(shù)據(jù)添加到rwe
中建芙,那么到底哪些地方調(diào)用attachCategories
方法,
- 全局搜索
attachCategories
懂扼,發(fā)現(xiàn)有兩處進行了調(diào)用禁荸,分別是attachToClass
方法和load_categories_nolock
方法
attachToClass流程流程分析
全局搜索attachToClass
,發(fā)現(xiàn)只有methodizeClass
方法中進行了調(diào)用
-
methodizeClass
方法阀湿,我們應(yīng)該不陌生赶熟,在上一篇類的加載
中有分析,從源碼我們發(fā)現(xiàn)previously
的值為nil
-
previously
作為備用參數(shù)陷嘴,這種設(shè)計可能是蘋果內(nèi)部調(diào)試用的 -
attachToClass
調(diào)用流程:_read_images
-->realizeClassWithoutSwift
-->methodizeClass
-->attachToClass
-->attachCategories
-->attachLists
load_categories_nolock流程分析
- 全局搜索
load_categories_nolock
映砖,在loadAllCategories
方法中調(diào)用
- 接著全局搜索
loadAllCategories
,在load_images
方法中調(diào)用
-
didInitialAttachCategories
默認值是false
灾挨,當(dāng)執(zhí)行完loadAllCategories()
后將didInitialAttachCategories
的值設(shè)為true
邑退,其實就是只調(diào)用一次loadAllCategories()方法
-
load_categories_nolock
的調(diào)用流程:load_images
-->loadAllCategories
-->load_categories_nolock
-->attachCategories
attachLists方法分析
attachLists
方法得源碼如下:
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; I--)
newArray->lists[i + addedCount] = array()->lists[I];
for (unsigned i = 0; i < addedCount; I++)
newArray->lists[i] = addedLists[I];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; I++)
array()->lists[i] = addedLists[I];
validate();
}
}
從源碼可以看出,attachLists
方法總共有三個流程分支:
【流程1】:0 lists -> 1 list
- 將
addedLists[0]
的指針賦值給list
【流程2】:1 list -> many lists
- 計算
舊的list
的個數(shù) - 計算
新的list
個數(shù) 涨醋,新的list個數(shù) = 原有的list個數(shù) + 新增的list個數(shù)
- 根據(jù)
newCount開辟相應(yīng)的內(nèi)存
瓜饥,類型是array_t
類型,并設(shè)置數(shù)組標識位
-setArray
- 將原有的
list
放在數(shù)組的末尾浴骂,因為最多只有一個不需要遍歷存儲
- 遍歷
addedLists
將遍歷的數(shù)據(jù)從數(shù)組的開始位置存儲
【流程3】:many lists -> many lists
判斷
hasArray()
是否存在計算
原有的
數(shù)組中的list
個數(shù),array()->lists
計算
新的list
個數(shù) 宪潮,新的list個數(shù) = 原有的list個數(shù) + 新增的list個數(shù)
根據(jù)
newCount
開辟相應(yīng)的內(nèi)存溯警,類型是array_t
類型設(shè)置新數(shù)組的個數(shù)等于
newCount
設(shè)置原有數(shù)組的個數(shù)等于
newCount
遍歷原有數(shù)組中l(wèi)ist將其存放在
newArray->lists
中 且是放在數(shù)組的末尾
遍歷
addedLists
將遍歷的數(shù)據(jù)從數(shù)組的開始位置存儲釋放原有的
array()
設(shè)置新的
newArray
list_array_tt
結(jié)構(gòu)和方法分析
-
rwe
結(jié)構(gòu)中的方法
趣苏、屬性
、協(xié)議
的類型都是繼承自list_array_tt
梯轻,在底層是二維數(shù)組的形式存儲
實例驗證分類的加載
通過上面的兩個例子食磕,我們可以大致將類
和 分類
是否實現(xiàn)+load
方法的情況分為4種
類和分類 | 分類實現(xiàn)+load | 分類未實現(xiàn)+load |
---|---|---|
類實現(xiàn)+load | 非懶加載類+非懶加載分類<span class="Apple-tab-span" style="white-space:pre"></span> | 非懶加載類+懶加載分類<span class="Apple-tab-span" style="white-space:pre"></span> |
類未實現(xiàn)+load | 懶加載類+非懶加載分類<span class="Apple-tab-span" style="white-space:pre"></span> | 懶加載類+懶加載分類 |
準備工作
- 創(chuàng)建
HTPerson
類以及分類HTPerson (HTA)
非懶加載類和非懶加載分類的加載
即主類實現(xiàn)了+load方法,分類同樣實現(xiàn)了+load方法
喳挑,在前文分類的加載時機時彬伦,我們已經(jīng)分析過這種情況,所以可以直接得出結(jié)論伊诵,這種情況下
- 程序啟動单绑,會直接加載
非懶加載類
,加載主類的方法 -
分類的數(shù)據(jù)加載
是通過load_images
加載到類中的
運行代碼曹宴,發(fā)現(xiàn)會調(diào)用attachCategories
方法搂橙,來加載分類信息,通過bt
查看函數(shù)調(diào)用棧
在相應(yīng)函數(shù)出設(shè)置斷點笛坦,打印結(jié)果如下
- 通過
MachOView
查看可執(zhí)行文件
非懶加載類與懶加載分類
即主類實現(xiàn)了+load方法区转,分類未實現(xiàn)+load方法
- 運行程序,發(fā)現(xiàn)并沒有調(diào)用
attachCategories
方法版扩,那么分類是如何加載的呢废离?
- 在
realizeClassWithoutSwift
方法處設(shè)置斷點,我們來看一下ro
是否有分類方法
- 獲取ro的方法列表:
p ro->baseMethods()
- 打印第i個方法信息:
p $2.get(i).big()
從上面的打印輸出可以看出礁芦,分類的方法和類的方法已經(jīng)合并到一起了蜻韭,方法的順序是 HTA分類-HTPerson類
,此時分類已經(jīng) 加載進來了宴偿,但是還沒有排序湘捎,說明這種情況下分類數(shù)據(jù)在編譯時
就與類數(shù)據(jù)合并到一起了,不需要運行時添加進去
- 通過
MachOView
查看可執(zhí)行文件
懶加載類與懶加載分類
即主類和分類均未實現(xiàn)+load方法
- 程序啟動時窄刘,類數(shù)據(jù)不會加載窥妇,只有在
首次接收消息時
才加載
其中realizeClassMaybeSwiftMaybeRelock
是消息流程中慢速查找中的函數(shù),即在第一次調(diào)用消息時
才會去加載懶加載類
- 在
realizeClassWithoutSwift
方法處設(shè)置斷點娩践,我們來看一下ro
是否有分類方法
- 通過
MachOView
查看可執(zhí)行文件
【結(jié)論】:
- 懶加載類與懶加載分類的數(shù)據(jù)加載是在
消息第一次調(diào)用
時加載 - 分類數(shù)據(jù)與類數(shù)據(jù)活翩,在
編譯時
已合并到一起,MachO
文件中的分類列表__objc_catlist
中無分類
懶加載類與非懶加載分類
即主類未實現(xiàn)+load方法翻伺,分類實現(xiàn)了+load方法
- 運行程序材泄,會調(diào)用
realizeClassWithoutSwift
方法,即程序一啟動吨岭,就會記載類數(shù)據(jù)拉宗,產(chǎn)看函數(shù)調(diào)用棧如下圖:
- 在
realizeClassWithoutSwift
方法處設(shè)置斷點,我們來看一下ro
是否有分類方法
從上面的打印輸出可以看出,分類的方法和類的方法已經(jīng)合并到一起了旦事,方法的順序是
HTA分類-HTPerson類
魁巩,此時分類已經(jīng) 加載進來了,但是還沒有排序姐浮,說明這種情況下分類數(shù)據(jù)在編譯時
就與類數(shù)據(jù)合并到一起了谷遂,不需要運行時添加進去通過
MachOView
查看可執(zhí)行文件
結(jié)論:
- 懶加載類變成非懶加載類,分類的數(shù)據(jù)在
編譯期間
合并到類數(shù)據(jù)中
多分類的情況
新增兩個分類卖鲤,HTPerson (HTB)
和HTPerson (HTC)
通過不同組合來肾扰,驗證類和分類的加載,總結(jié)如下
實現(xiàn)+load方法的分類個數(shù) | 非懶加載類 | 懶加載類 |
---|---|---|
0 | 編譯時類數(shù)據(jù)與分類數(shù)據(jù)已合并 </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 | 首次接收消息時蛋逾,才加載類數(shù)據(jù)集晚,分類數(shù)據(jù)與類數(shù)據(jù),在編譯時 已合并到一起 </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 |
1 | 程序啟動加載類數(shù)據(jù)换怖,load_images時加載分類數(shù)據(jù) </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:1 | 程序啟動加載類數(shù)據(jù)(編譯器將類標記為非懶加載類 )甩恼,分類數(shù)據(jù)與類數(shù)據(jù),在編譯時 已合并到一起 </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 |
2 | 程序啟動加載類數(shù)據(jù)沉颂,load_images時加載分類數(shù)據(jù) </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:2 | 編譯后条摸,類仍是懶加載類 ,程序啟動(load_images 方法中)會加載類和分類數(shù)據(jù) </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3 </br> __objc_nlcatlist:2 |
3 | 程序啟動加載類數(shù)據(jù)铸屉,load_images時加載分類數(shù)據(jù) </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:3 | 編譯后钉蒲,類仍是懶加載類 ,程序啟動(load_images 方法中)會加載類和分類數(shù)據(jù) </br> MachO文件中類和分類數(shù)據(jù)如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3 </br> __objc_nlcatlist:3 |
- 1彻坛、非懶加載類 + 3個懶加載分類
- 2顷啼、非懶加載類 + 2個懶加載分類 + 1個非懶加載分類
程序啟動加載類數(shù)據(jù),load_images
時按照MachO中 __objc_catlist中的順序
挨個加載分類數(shù)據(jù)
- 3昌屉、非懶加載類 + 1個懶加載分類 + 2個非懶加載分類
程序啟動加載類數(shù)據(jù)钙蒙,load_images
時按照MachO中 __objc_catlist中的順序
挨個加載分類數(shù)據(jù)
- 4、非懶加載類 + 3個非懶加載分類
程序啟動加載類數(shù)據(jù)间驮,load_images
時按照MachO中 __objc_catlist中的順序
挨個加載分類數(shù)據(jù)
- 5躬厌、懶加載類 + 3個懶加載分類
首次接收消息時,才加載類數(shù)據(jù)竞帽,分類數(shù)據(jù)與類數(shù)據(jù)扛施,在編譯時
已合并到一起
- 6、懶加載類 + 2個懶加載分類 + 1個非懶加載分類
程序啟動加載類數(shù)據(jù)(編譯器將類標記為非懶加載類
)屹篓,分類數(shù)據(jù)與類數(shù)據(jù)疙渣,在編譯時
已合并到一起
- 7、懶加載類 + 1個懶加載分類 + 2個非懶加載分類
編譯后堆巧,類仍是懶加載類
妄荔,程序啟動(load_images
方法中)會加載類和分類數(shù)據(jù)泼菌,類和分類的加載流程:load_images
--> prepare_load_methods
--> realizeClassWithoutSwift
--> methodizeClass
--> attachToClass
--> attachCategories
- 8、懶加載類 + 3個非懶加載類
編譯后懦冰,類仍是懶加載類
灶轰,程序啟動(load_images
方法中)會加載類和分類數(shù)據(jù)谣沸,類和分類的加載流程:load_images
--> prepare_load_methods
--> realizeClassWithoutSwift
--> methodizeClass
--> attachToClass
--> attachCategories