溫故而知新 - ObjC Category 實現(xiàn)原理

摘要

Category 主要作用是為已有的類,添加方法、屬性、協(xié)議。

其實現(xiàn)原理,一方面,在編譯時期,會生成 category_t 及相關(guān)結(jié)構(gòu)體。

另一方面,在運行時期,會將這些方法、屬性、協(xié)議添加到類之中。

category_t 結(jié)構(gòu)

// objc4-750.1/runtime/objc-runtime-new.h
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);
};

比較特殊地,它并非繼承 objc_object。

而且,當(dāng)中并沒有 ivars,這也解釋了,為什么 Category 不能添加變量。

插個題外話,其中的 _classProperties 比較少見,查了下,才發(fā)現(xiàn)是為配合 Swift,Xcode8 推出的新特性。

語法如下:

@property (class) NSString *someString;

編譯時期

  1. 編譯時期,不同 Category,會被編譯成不同的 category_t 結(jié)構(gòu)體。
  2. 而 Category 中的方法、屬性、協(xié)議等列表,也會被編譯成不同的結(jié)構(gòu)體,存于 category_t 之中。
  3. Category 的相關(guān)信息最終會在 Mach-O 中有所記錄。

具體的,先來個簡單代碼,看看會編譯成什么。

@implementation Cat (JD)
- (void)run {
    NSLog(@"run");
}
@end

轉(zhuǎn)化成 c++ 代碼

clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0  Cat+JD.m

可看到 run 方法被轉(zhuǎn)換成以下靜態(tài)函數(shù)

// @implementation Cat (JD)

static void _I_Cat_JD_run(Cat * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_mv_t42fx9pj0mn9k96m8y1rwwd40000gn_T_Cat_JD_0bdbd6_mi_0);
}
// @end

創(chuàng)建了名為 _OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD_method_list_t

并與上面的靜態(tài)函數(shù)關(guān)聯(lián)起來。

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_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Cat_JD_run}}
};

創(chuàng)建名為 _OBJC_$_CATEGORY_Cat_$_JD_category_t (即 category_t) 結(jié)構(gòu)體。

static struct _category_t _OBJC_$_CATEGORY_Cat_$_JD __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
    "Cat",
    0, // &OBJC_CLASS_$_Cat,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Cat_$_JD,
    0,
    0,
    0,
};

與原來的類關(guān)聯(lián)

static void OBJC_CATEGORY_SETUP_$_Cat_$_JD(void) {
    _OBJC_$_CATEGORY_Cat_$_JD.cls = &OBJC_CLASS_$_Cat;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
    (void *)&OBJC_CATEGORY_SETUP_$_Cat_$_JD,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_Cat_$_JD,
};

源碼中有不少 section ("__DATA,__objc_const") 這樣的代碼,其實就是 Mach-O 的相關(guān)存儲位置,會在加載時用到。

運行時期

Objc 初始化時,從 Mach-O 中拿到編譯時期產(chǎn)生的 category_t 數(shù)組。

再做了以下操作:

  1. 將 category_t 中的實例屬性、實例方法、協(xié)議添加到 class 之中。
  2. 將類屬性、類方法、協(xié)議添加到 metaClass 中。

添加之后, Category 方法、屬性、協(xié)議都會在原列表的前面。

來看看具體實現(xiàn)。

_read_images()

從 ObjC 初始化方法 _objc_init() 開始,會調(diào)用 _read_images()

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
    ...
    // _objc_init->map_images->map_images_nolock->_read_images->realizeClass
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_read_images() 之中,既給 class 添加了實例方法、協(xié)議、實例屬性,也給 metaClass 添加了類方法、協(xié)議、類屬性。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    ...
    // Discover categories.
    for (EACH_HEADER) {
        // 獲取
        category_t **catlist =
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            ...

            // Process this category.
            // First, register the category with its target class.
            // Then, rebuild the class's method lists (etc) if
            // the class is realized.
            bool classExists = NO;

            // 實例方法、協(xié)議、實例屬性 -> class
            if (cat->instanceMethods ||  cat->protocols
                ||  cat->instanceProperties)
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                ...
            }

            // 類方法、協(xié)議、類屬性 -> class
            if (cat->classMethods  ||  cat->protocols
                ||  (hasClassProperties && cat->_classProperties))
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                ...
            }
        }
    }

    ...
}

attachCategories()

而添加的具體邏輯,并不在 remethodizeClass() 之中,而在 attachCategories()。

其調(diào)用堆棧

20210330-2322.png

attachCategories() 核心代碼如下。

可以看到,后加載到的 category 信息,會先添加。

// 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, category_list *cats, bool flush_caches)
{
    ...

    // 逆序添加
    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 省略了屬性、協(xié)議 ...
    }

    auto rw = cls->data();

    ...
    //  remethodizeClass() 調(diào)用的,需要更新緩存; methodizeClass() 則不需要
    if (flush_caches  &&  mcount> 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    // 省略了協(xié)議 ...
}

list_array_tt::attachLists()

添加列表的邏輯在 list_array_tt::attachLists() 之中

從中可看到,會保留類原來的列表,只是將其順序往后移動了,所以調(diào)用時,會優(yōu)先調(diào)用分類方法。

// objc4-750.1/runtime/objc-runtime-new.h

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;
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    }
    else {
        // 1 list -> many lists
        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;
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
}

many lists -> many lists 當(dāng)中,關(guān)鍵的 2 行代碼

memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));

其實也就是將 Category 里的列表,移動到原列表之前,翻譯過來,可用一張圖表示。

20210331-1800.png

小結(jié)

======== 編譯時期 ========

不同 Category,會被編譯成不同的 category_t 結(jié)構(gòu)體。

包括其中的方法、屬性、協(xié)議等列表,也被編譯成結(jié)構(gòu)體,存于 category_t 之中。

所有這些信息最終會在 Mach-O 中有所記錄。

======== 運行時期 ========

Objc 初始化時,從 Mach-O 中拿到編譯時期產(chǎn)生的 category_t 數(shù)組。

再做了以下操作:

  1. 將 category_t 中的實例屬性、實例方法、協(xié)議添加到 class 之中。
  2. 將類屬性、類方法、協(xié)議添加到 metaClass 中。

添加之后, Category 方法、屬性、協(xié)議列表都會在前面。

疑問

上面有提到 Category 無法添加變量的原因是 category_t 中無 ivars。

那為什么不在 category_t 中也增加 ivars 呢?

個人見解如下:

結(jié)合 objc_class 的結(jié)構(gòu)來看,變量列表存在于 class_ro_t 中,是不可變的。

而變量其實是存在于對象內(nèi)存布局之中,Category 是給類添加?xùn)|西,不應(yīng)影響對象。

struct class_ro_t {
     // 變量列表
     const ivar_list_t * ivars;
 }

但不知正確與否,麻煩知道的同學(xué)告知一聲,感激不盡!

感謝

深入理解 Objective-C:Category

Apple Developer Category

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

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

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