iOS底層系列17 -- 分類的加載機(jī)制

準(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)體;
  • 存在namecls兩個(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)
Snip20210309_18.png
  • 準(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)行的。
Snip20210309_19.png
Snip20210309_20.png
  • 可以看到分類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ī)

【當(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)用堆棧如下所示:
Snip20210309_21.png
  • 由于map_imagesload_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;
image.png
  • 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í):

Snip20210309_29.png
  • 前后兩次打印的結(jié)果如下:
Snip20210309_27.png
  • 可以看到加載的兩個(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中即可;
Snip20210309_28.png

類與分類的搭配使用

  • 主要是看是否實(shí)現(xiàn)了load類方法,兩兩組合總共有四種情況如下所示:
Snip20210309_30.png
【第一種:非懶加載類 與 非懶加載分類】
  • 主類與分類都實(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)試圖下所示:

Snip20210309_36.png
Snip20210309_35.png
  • 可以看出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)如下:

Snip20210309_37.png
  • 當(dāng)斷點(diǎn)停止1436行時(shí),LLDB調(diào)試如下:
Snip20210309_38.png
Snip20210309_39.png
  • 跟上面的打印結(jié)果一致;
  • 過掉當(dāng)前斷點(diǎn)來到prepareMethodLists函數(shù) 打下斷點(diǎn)如下:
Snip20210309_40.png
  • 當(dāng)斷點(diǎn)停在所在行,LLDB調(diào)試如下:
Snip20210309_41.png
  • 過掉當(dāng)前斷點(diǎn)來到fixupMethodList函數(shù),打下斷點(diǎn)如下:
Snip20210310_42.png
  • 當(dāng)斷點(diǎn)停在1218行時(shí),LLDB調(diào)試如下:
Snip20210310_44.png
  • 與排序之前的方法順序進(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í)現(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é)果如下:
Snip20210310_47.png
  • 在編譯期時(shí),類的class_ro_t中就已經(jīng)存在類與分類的data數(shù)據(jù);
  • 接下來在外界向主類發(fā)送消息,YYPerson *person = [[YYPerson alloc]init],LLDB調(diào)試結(jié)果如下:
Snip20210310_48.png
  • 發(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í)現(xiàn)load類方法,分類實(shí)現(xiàn)了load類方法;
  • 當(dāng)程序執(zhí)行到readClass內(nèi)部時(shí)斷點(diǎn)斷住,LLDB調(diào)試如下:
Snip20210310_49.png
  • class_ro_t中只有主類的data數(shù)據(jù),并沒有分類的data數(shù)據(jù),說明在編譯期時(shí),分類的數(shù)據(jù)并沒有合并到主類中;
  • 過掉斷點(diǎn),在attachCategories函數(shù)內(nèi)部斷住,LLDB調(diào)試如下:
Snip20210310_50.png
  • 函數(shù)調(diào)用堆棧為load_images->prepare_load_methodsprepare_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é)如下圖所示:

image.png
class_加載.png

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

相關(guān)閱讀更多精彩內(nèi)容

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