準(zhǔn)備工作
- 新建一個(gè)命令行工程;
- 新建一個(gè)
YYPerson類,定義一個(gè)walk方法; - 新建一個(gè)
YYPerson+Test分類,定義一個(gè)test方法; - 新建一個(gè)
YYPerson_Eat分類,定義一個(gè)eat方法; - 然后cd 到文件路徑下,執(zhí)行
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YYPerson+Test.m,經(jīng)過編譯,會(huì)生成一個(gè)YYPerson+Test.cpp文件,在此文件中我們看到_category_t的結(jié)構(gòu)體定義如下:
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
-
YYPerson+Test分類 在編譯期會(huì)生成一個(gè)_category_t結(jié)構(gòu)體,如下所示:
static struct _category_t _OBJC_$_CATEGORY_YYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"YYPerson",
0, // &OBJC_CLASS_$_YYPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_YYPerson_$_Test,
0,
0,
0,
};
- 主類
YYPerson會(huì)賦值給_category結(jié)構(gòu)體的第一個(gè)成員; -
_CATEGORY_INSTANCE_METHODS_YYPerson_的定義如下:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_YYPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_YYPerson_Test_test}}
};
- 看到了
test方法,最終會(huì)賦值到_category結(jié)構(gòu)體的第四個(gè)成員_method_list_t *instance_methods實(shí)例方法列表中; - 表明
YYPerson+Test文件,在編譯期時(shí)是生成一個(gè)_category結(jié)構(gòu)體,所有數(shù)據(jù)都存放在這個(gè)結(jié)構(gòu)體中,并沒有合并到主類YYPerson中,合并的操作是在運(yùn)行時(shí),不在編譯期
分類的底層結(jié)構(gòu)體
在objc源碼工程中全局搜索category_t,可以看到分類的結(jié)構(gòu)體定義如下所示:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
- 分類category在底層是
category_t結(jié)構(gòu)體; - 存在
name與cls兩個(gè)屬性; - 有兩個(gè)
method_list_t類型的結(jié)構(gòu)體屬性分別表示分類中實(shí)現(xiàn)的實(shí)例方法+類方法; - 一個(gè)protocol_list_t類型的協(xié)議列表,表示分類中實(shí)現(xiàn)的協(xié)議;
- 一個(gè)property_list_t類型的屬性列表,表示分類中定義的屬性,一般在分類中添加的屬性都是通過關(guān)聯(lián)對(duì)象來實(shí)現(xiàn);
- 注意在分類中的屬性是沒有set、get方法。
分類的加載機(jī)制
- 準(zhǔn)備工作一:主類
YYCat,新建兩個(gè)分類YYCat (Add_One)與YYCat (Add_Two)

- 準(zhǔn)備工作二:為了方便調(diào)試源碼工程,我們?cè)?code>readClass,
realizeClassWithoutSwift,methodizeClass,attachToClass,attachCategories中都加入了以下的測(cè)試代碼;
//測(cè)試代碼
const char *mangledName = cls->mangledName();
const char *YYCatName = "YYCat";
if (strcmp(mangledName, YYCatName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
if (!kc_isMeta) {
printf("%s: 定位到 %s \n",__func__,YYCatName);
}
}
- 在 iOS底層系列16 -- 類的加載 文章中探索了類的加載機(jī)制,其中
methodizeClass函數(shù)關(guān)于類的數(shù)據(jù)加載與分類的數(shù)據(jù)加載是分開進(jìn)行的。


- 可以看到分類category是通過調(diào)用
attatchToClass添加到類class的,然后才能在外界進(jìn)行使用,主要分為以下幾個(gè)步驟:- 分類數(shù)據(jù)加載時(shí)機(jī)是
根據(jù)類和分類是否實(shí)現(xiàn)load方法來區(qū)分不同的時(shí)機(jī); - attachCategories準(zhǔn)備分類數(shù)據(jù);
- attachLists將分類數(shù)據(jù)添加到主類中;
- 分類數(shù)據(jù)加載時(shí)機(jī)是
探索分類的加載時(shí)機(jī)
【當(dāng)主類與分類均實(shí)現(xiàn)load類方法時(shí)】
-
attachCategories源碼實(shí)現(xiàn)如下:
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];
//測(cè)試代碼
const char *mangledName = cls->mangledName();
const char *YYCatName = "YYCat";
if (strcmp(mangledName, YYCatName) == 0) {
bool kc_isMeta = cls->isMetaClass();
auto kc_rw = cls->data();
auto kc_ro = kc_rw->ro();
if (!kc_isMeta) {
printf("%s: 定位到 %s \n",__func__,YYCatName);
}
}
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
//創(chuàng)建class_rwe_t結(jié)構(gòu)體
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) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
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;
}
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);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
- 直接在
attachCategories函數(shù)內(nèi)部加入測(cè)試代碼并打下斷點(diǎn),當(dāng)斷點(diǎn)停住時(shí),在LLDB控制臺(tái)輸入bt,打印函數(shù)調(diào)用堆棧如下所示:

- 由于
map_images在load_images之前調(diào)用,那么map_images函數(shù)中關(guān)于分類信息加載的調(diào)用鏈為:map_images --> map_images_nolock --> _read_images --> readClass --> _getObjc2NonlazyClassList(非懶加載類) --> realizeClassWithoutSwift(實(shí)現(xiàn)類即加載類信息) --> methodizeClass(方法類化) --> attachToClass->attachCategories - 在
map_images的調(diào)用鏈中,在attachToClass函數(shù)內(nèi)部,去獲取分類數(shù)據(jù)auto it = map.find(previously),分類數(shù)據(jù)是空的,所以不會(huì)調(diào)用attachCategories函數(shù),加載分類數(shù)據(jù)延遲到load_images函數(shù)中了,在methodizeClass會(huì)將主類的data數(shù)據(jù)賦值給class_rw_ext_t;

在
load_images函數(shù)中分類加載的函數(shù)調(diào)用鏈為:load_images --> loadAllCategories --> load_categories_nolock --> attachCategories在
attachCategories函數(shù)中將分類的data數(shù)據(jù)賦值給class_rw_ext_t;當(dāng)斷點(diǎn)停在
attachCategories函數(shù)中的如下代碼行時(shí):

- 前后兩次打印的結(jié)果如下:

- 可以看到加載的兩個(gè)分類的詳細(xì)信息;
- 當(dāng)?shù)谝淮渭虞d主類的一個(gè)分類(YYCat (Add_One))時(shí),需要開辟rwe內(nèi)存,第二次再加載另一個(gè)分類(YYCat (Add_Two)),就不需要再開辟rwe內(nèi)存了,先前已經(jīng)創(chuàng)建了,直接將第二次加載的分類信息寫入rwe中即可;

類與分類的搭配使用
- 主要是看是否實(shí)現(xiàn)了load類方法,兩兩組合總共有四種情況如下所示:

【第一種:非懶加載類 與 非懶加載分類】
- 即
主類與分類都實(shí)現(xiàn)了load方法,且外界向主類發(fā)送消息,邏輯流程上面已經(jīng)探討過了,現(xiàn)做如下總結(jié): -
map_images中的調(diào)用鏈:map_images --> map_images_nolock --> _read_images --> readClass --> _getObjc2NonlazyClassList(非懶加載類) --> realizeClassWithoutSwift(實(shí)現(xiàn)類即加載類信息) --> methodizeClass(方法類化) --> attachToClass --> attachCategories; - 在
map_images的調(diào)用鏈中,在attachToClass函數(shù)內(nèi)部,去獲取分類數(shù)據(jù)auto it = map.find(previously),分類數(shù)據(jù)是空的,所以不會(huì)調(diào)用attachCategories函數(shù),加載分類數(shù)據(jù)延遲到load_images函數(shù)中了,在methodizeClass會(huì)將主類的data數(shù)據(jù)賦值給class_rw_ext_t; - 在
load_images函數(shù)中分類加載的函數(shù)調(diào)用鏈為:load_images --> loadAllCategories --> load_categories_nolock --> attachCategories - 在
attachCategories函數(shù)中將分類的data數(shù)據(jù)賦值給class_rw_ext_t;
【第二種:非懶加載類 與 懶加載分類】
即
主類實(shí)現(xiàn)load方法,分類沒有實(shí)現(xiàn)load方法,且外界沒有調(diào)用主類發(fā)送消息當(dāng)斷點(diǎn)停在
realizeClassWithoutSwift函數(shù)內(nèi)部的測(cè)試代碼行時(shí),函數(shù)調(diào)用堆棧為map_images->map_images_nolock->read_images->realizeClassWithoutSwift,LLDB調(diào)試圖下所示:


可以看出
class_ro_t中方法的存儲(chǔ)順序?yàn)椋?code>YYCat (Add_Two) --> YYCat (Add_One) --> YYCat;class_ro_t中已經(jīng)包含了分類的方法信息,即在編譯期時(shí)類與分類的data數(shù)據(jù)就已經(jīng)合并了;放開當(dāng)前斷點(diǎn),進(jìn)入
methodizeClass函數(shù),打下斷點(diǎn)如下:

- 當(dāng)斷點(diǎn)停止1436行時(shí),LLDB調(diào)試如下:


- 跟上面的打印結(jié)果一致;
- 過掉當(dāng)前斷點(diǎn)來到
prepareMethodLists函數(shù) 打下斷點(diǎn)如下:

- 當(dāng)斷點(diǎn)停在所在行,LLDB調(diào)試如下:

- 過掉當(dāng)前斷點(diǎn)來到
fixupMethodList函數(shù),打下斷點(diǎn)如下:

- 當(dāng)斷點(diǎn)停在1218行時(shí),LLDB調(diào)試如下:

與排序之前的方法順序進(jìn)行比較,發(fā)現(xiàn)排序之后的方法順序確實(shí)發(fā)生了變化;
-
總結(jié):
- 在編譯期時(shí),類的
class_ro_t中就已經(jīng)存在類與分類的data數(shù)據(jù); - 在運(yùn)行時(shí),在
methodizeClass函數(shù)中,將類與分類的data數(shù)據(jù)賦值給class_rw_ext_t;
- 在編譯期時(shí),類的
【第三種:懶加載類 與 懶加載分類】
- 即主類與分類都沒有實(shí)現(xiàn)load類方法;
- 當(dāng)外界沒有向主類發(fā)送消息時(shí),在
map_images流程中只執(zhí)行了readClass只加載了class的地址和名稱,并沒有實(shí)現(xiàn)類即沒有調(diào)用realizeClassWithoutSwift函數(shù),當(dāng)程序執(zhí)行到readClass內(nèi)部時(shí),斷點(diǎn)斷住,LLDB調(diào)試結(jié)果如下:

- 在編譯期時(shí),類的
class_ro_t中就已經(jīng)存在類與分類的data數(shù)據(jù); - 接下來在外界向主類發(fā)送消息,
YYPerson *person = [[YYPerson alloc]init],LLDB調(diào)試結(jié)果如下:

發(fā)送alloc消息,接著進(jìn)入了
消息的慢速查找流程,然后進(jìn)入實(shí)現(xiàn)類的流程,最后進(jìn)入methodizeClass函數(shù),實(shí)現(xiàn)將類與分類的data數(shù)據(jù)賦值給class_rw_ext_t;-
總結(jié):
- 在編譯期時(shí),類的
class_ro_t中就已經(jīng)存在類與分類的data數(shù)據(jù); - 第一次給主類發(fā)送消息時(shí),首先會(huì)進(jìn)入
消息的慢速查找流程,然后進(jìn)入實(shí)現(xiàn)類的流程,最后進(jìn)去methodizeClass函數(shù),實(shí)現(xiàn)將類與分類的data數(shù)據(jù)賦值給class_rw_ext_t;
- 在編譯期時(shí),類的
【第四種:懶加載類 與 非懶加載分類】
- 即主類沒有實(shí)現(xiàn)load類方法,分類實(shí)現(xiàn)了load類方法;
- 當(dāng)程序執(zhí)行到
readClass內(nèi)部時(shí)斷點(diǎn)斷住,LLDB調(diào)試如下:

- 在
class_ro_t中只有主類的data數(shù)據(jù),并沒有分類的data數(shù)據(jù),說明在編譯期時(shí),分類的數(shù)據(jù)并沒有合并到主類中; - 過掉斷點(diǎn),在
attachCategories函數(shù)內(nèi)部斷住,LLDB調(diào)試如下:

函數(shù)調(diào)用堆棧為
load_images->prepare_load_methods在prepare_load_methods中會(huì)調(diào)用_getObjc2NonlazyCategoryList函數(shù)去獲取所有非懶加載分類,通過分類category獲取對(duì)應(yīng)的主類Class cls = remapClass(cat->cls),然后進(jìn)入實(shí)現(xiàn)主類的流程realizeClassWithoutSwift,最后進(jìn)去methodizeClass函數(shù),實(shí)現(xiàn)將類與分類的data數(shù)據(jù)賦值給class_rw_ext_t;最終總結(jié)如下圖所示:


load類方法的調(diào)用順序
- 首先調(diào)用所有類的load方法;
- 按照編譯順序調(diào)用,先編譯的類,先調(diào)用;
- 在調(diào)用當(dāng)前類的load方法之前,會(huì)先調(diào)用父類的load方法;
- 然后調(diào)用所有分類的load方法;
- 按照編譯順序調(diào)用,先編譯的分類,先調(diào)用;
- 分類不存在繼承的情況;
initlization方法
- 在類第一次接收到消息時(shí)調(diào)用;
- 先調(diào)用父類的initlization方法,再調(diào)用子類的initlization方法;
- 若子類沒有實(shí)現(xiàn)initlization方法,會(huì)調(diào)用父類的initlization方法,所以父類的initlization方法可能會(huì)被調(diào)用多次;
- 如果分類實(shí)現(xiàn)了initlization方法,會(huì)優(yōu)先調(diào)用分類的initlization方法;