Category探索

VanchChen發表於2018-12-29

什麼是Category?

Category是Objective-C 2.0之後新增的語言特性,Category的主要作用是為已經存在的類新增方法,一般稱為分類,檔名格式是"NSObject+A.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;
    struct property_list_t *_classProperties;
}
複製程式碼

從結構能看出分類可以擴充套件例項方法列表、類方法列表、協議列表,也支援擴充套件屬性,但不支援擴充套件成員變數(之後會說)。

一般使用的場景有擴充套件現有類方法、程式碼分割槽、新增私有方法(不對外暴露category.h)、模擬多繼承(使用關聯物件的方式新增屬性實現)


什麼是Extension?

Extension一般被稱為類擴充套件、匿名分類,用於定義私有屬性和方法,不可被繼承。只能依附自定義類寫於.m中,定義一般為:

@interface ViewController ()

@property (nonatomic, strong) NSObject *obj;

@end
複製程式碼

類擴充套件支援寫在多個.h檔案,但都必須在.m檔案中引用,且不能有自己的實現。

類擴充套件很多時候會與分類搞混,我在文後問答環節詳細整理了他們的區別。


Category如何載入的?

struct objc_class : objc_object {
    Class superclass;
    class_data_bits_t bits; 
    class_rw_t *data() {
        return bits.data();
    }
    ...
}

struct class_rw_t {
    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}

struct class_ro_t {
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; //只有ro才有例項變數表
    property_list_t *baseProperties;
    ...
};
複製程式碼

先簡單瞭解一下Class物件的結構,每個objc_class都包含有class_data_bits_t資料位,其中儲存了class_rw_t的指標地址和一些其他標記。class_rw_t中包含有屬性方法協議列表,以及class_ro_t指標地址。而在class_ro_t結構中,儲存的是編譯器決定的屬性方法協議。

那麼是怎麼執行的呢?

在編譯期類的結構中的class_data_bits_t指向的是一個 class_ro_t指標。

在執行時呼叫realizeClass方法,初始化一個class_rw_t結構體,設定ro值為原資料中的class_ro_t後設為資料位中的指向,最後呼叫methodizeClass方法載入。

static void methodizeClass(Class cls)
{
    auto rw = cls->data();
    auto ro = rw->ro;

    //從ro中載入方法表
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    //載入屬性
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    //載入協議
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    //基類新增初始化方法
    if (cls->isRootMetaclass()) {
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }
    //載入分類
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
    
    if (cats) free(cats);
}
複製程式碼

可以看到,在methodizeClass中載入了原先類在編譯期決定的方法屬性和協議,然後獲取了未連線的分類表,將列表中的擴充套件方法新增到執行期類中。


Category方法覆蓋

如果不同的分類實現了相同名字的方法,那麼呼叫時會使用最後加入的實現,這是為什麼呢?

載入Category

dyld連結並初始化二進位制檔案後,交由ImageLoader讀取,接著通知runtime處理,runtime呼叫map_images解析,然後執行_read_images分析檔案中包含的類和分類。

//載入分類
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);

    if (!cls) {
        //分類指定的類還沒載入,可能是連結庫順序的問題
        catlist[i] = nil;
        continue;
    }
    //新增分類到類的分類表中,伺機過載入
    bool classExists = NO;
    if (cat->instanceMethods ||  cat->protocols  
        ||  cat->instanceProperties) 
    {
        addUnattachedCategoryForClass(cat, cls, hi);
        if (cls->isRealized()) {
            remethodizeClass(cls);
            classExists = YES;
        }
    }
    //新增分類到元類中
    if (cat->classMethods  ||  cat->protocols  
        ||  (hasClassProperties && cat->_classProperties)) 
    {
        addUnattachedCategoryForClass(cat, cls->ISA(), hi);
        if (cls->ISA()->isRealized()) {
            remethodizeClass(cls->ISA());
        }
    }
}
複製程式碼

新增方法屬性和協議

如果有新增的分類,就分別新增到原類和meta類,並通過remethodizeClass更新,具體就是呼叫attachCategories方法把分類中所有的方法都新增到指定類中。

static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;

    bool isMeta = cls->isMetaClass();

    //新建陣列指標
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    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();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();
    //載入列表到rw中
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

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

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
複製程式碼
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]));
        }
    }
複製程式碼

可以看到最後呼叫了rw->methods.attachLists(mlists, mcount); 把新增分類中的方法列表新增到實際執行時查詢的方法列表頭部。

在進行方法呼叫時會從頭部查詢,一旦查到後就返回結果,因此後編譯的檔案中的方法會被優先呼叫。

同時之前新增的方法實現也儲存了,可以通過獲取同名方法的方式查詢原類的實現。


Category實現屬性

分類不能新增成員變數

屬性(Property)包含了成員變數(Ivar)和Setter&Getter。

可以在分類中定義屬性,但由於分類是在執行時新增分類屬性到類的屬性列表中,所以並沒有建立對應的成員變數和方法實現。

關聯物件

如果我們想讓分類實現新增新的屬性,一般都通過關聯物件的方式。

// 宣告檔案
@interface TestObject (Category)
@property (nonatomic, strong) NSObject *object;
@end

// 實現檔案
static void *const kAssociatedObjectKey = (void *)&kAssociatedObjectKey;

@implementation TestObject (Category)

- (NSObject *)object {
    return objc_getAssociatedObject(self, kAssociatedObjectKey);
}

- (void)setObject:(NSObject *)object {
    objc_setAssociatedObject(self, kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
複製程式碼

這種方式可以實現存取物件,但是不能獲取_object變數。


問答

分類和擴充套件有什麼區別?

1.分類多用於擴充套件方法實現,類擴充套件多用於申明私有變數和方法。

2.類擴充套件作用在編譯期,直接和原類在一起,而分類作用在執行時,載入類的時候動態新增到原類中。

3.類擴充套件可以定義屬性,分類中定義的屬性只會申明setter/getter,並沒有相關實現和變數。

分類有哪些侷限性?

1.分類只能給現有的類加方法或協議,不能新增例項變數(ivar)。

2.分類新增的方法如果與現有的重名,會覆蓋原有方法的實現。如果多個分類方法都重名,則根據編譯順序執行最後一個。

分類的結構體裡面有哪些成員?

分類結構體包含了分類名,繫結的類,例項與類方法列表,例項與類方法屬性以及協議表。


參考

深入理解Objective-C:Category

神經病院 Objective-C Runtime 入院第一天—— isa 和 Class

探祕Runtime - 深入剖析Category

相關文章