Category的實現原理

阿多發表於2018-08-04

什麼是category?

是在不改變已存在類的情況下,對其新增方法來達到對類進行功能擴充套件的目的。

category結構

可以在 objc-runtime-new.h 檔案中看到 category_t 結構體的定義

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;   //類屬性列表

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
複製程式碼

category 的實現原理

在原始碼 objc-runtime-new.mm 檔案中的第 2560 行開始,可以看到關於 category 的處理邏輯:

for (EACH_HEADER) {
         // 1.獲取category列表
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count); 
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        // 2.遍歷
        for (i = 0; i < count; i++) {   
            category_t *cat = catlist[i];
            // 3.對應的類cls
            Class cls = remapClass(cat->cls);   
             // 3.1 如果cls為nil
            if (!cls) {     
                 // 3.2 將category置為nil
                catlist[i] = nil;  
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }
            
            bool classExists = NO;
            // 4. 例項方法 或 協議 或 屬性存在
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 4.1 處理category中的資料(類)
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            // 5. 類方法 或 協議 或 屬性存在
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    // 5.1 處理category中的資料(元類)
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
    
複製程式碼

處理category中的資料又是在方法remethodizeClass()中實現,注意傳入的cls引數,如果是例項方法則傳入的是類cls,如果是類方法,傳入的是cls的isa指標,也就是元類,跟蹤該方法:

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    // 1. 是否為元類
    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        // 2. 修改對應的類的方法列表、協議列表、屬性列表
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
複製程式碼

修改對應的類或元類中的方法列表、協議列表、屬性列表又是在attachCategories()方法內實現的,跟蹤該方法:

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

    bool isMeta = cls->isMetaClass();

    // 1.建立陣列
    // fixme rearrange to remove these intermediate allocations
    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));

    // 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;
    // 2. 倒敘插入
    while (i--) {  
        auto& entry = cats->list[i];

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

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

        // 2.3 協議列表
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    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);
}
複製程式碼

建立一個陣列,倒敘插入 新增的方法、屬性和協議、具體的插入實現在方法attachLists()中,跟蹤該方法

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        // 1.主類中有多個資料集合的時候
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            
            // 1.1 使用realloc() 函式將原來的空間擴充
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            // 1.2 將原來的陣列複製到後面
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
                    
            // 1.3 把新陣列複製到前面
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        
        //2.為空的時候,直接將指標指向新的資料集。
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        }
        
        // 3. 主類中只有一個資料集合的時候
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            
            // 3.1 使用malloc()重新申請一塊記憶體
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            
            // 3.2 將原來的集合放到最後
            if (oldList) array()->lists[addedCount] = oldList;
            
            // 3.3 新陣列複製到前邊
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
複製程式碼

如上縮減,runtime 針對主類中原有列表的三種情況,有三種不同的插入處理。

小結

  1. 當category中方法和類中的方法同名的話,會優先呼叫category中的方法
  2. 這裡看起來是category的方法覆蓋了主類的方法,但實際是category的方法在方法列表的前面,所以總先呼叫category中的方法。
  3. 多個category中存在相同的方法時,呼叫的順序跟編譯的順序有關,最後一個編譯的,會被呼叫。

category為什麼可以新增方法,而不能新增屬性呢?

回頭仔細看下 obj_class 的結構:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    
    / ************** /
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    / ************** /
    
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

複製程式碼

注意看 ivars 和 methodLists 有什麼區別?
ivars是指向 objc_ivar_list 的指標,而 methodLists 是指 向objc_method_list 的指標的指標。
objc_class 結構體的大小是固定的,不能新增資料,只能修改。 所以 ivars 指向的 objc_ivar_list 是一個固定區域,只能修改成員變數的值,不能增加成員變數的個數;
雖然沒辦法擴充套件methidLists指向的區域,但是可以修改 *objc_method_list 的值來增加成員變數;

給category增加屬性 : 關聯物件(Associated Object)

雖然可以實現,但是不推薦在category中新增屬性

在 .h 檔案中新增屬性的宣告

@property (copy, nonatomic) NSString * addName;
複製程式碼

在 .m 中

#import "NSObject+addProperty.h"
#import <objc/runtime.h>

@implementation NSObject (addProperty)

static char *addNameKey = "addNameKey";

-(void)setAddName:(NSString *)addName{
    /* 關聯方法:
     * id object 給哪個物件的屬性賦值
     * const void *key 屬性對應的key
     * id value  設定屬性值為value
     * objc_AssociationPolicy policy  使用的儲存策略,copy/retain/assign
     */
    objc_setAssociatedObject(self, addNameKey, addName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)addName{
    return objc_getAssociatedObject(self, addNameKey);
}


@end
複製程式碼

關聯物件(Associated Object)

關聯物件可以被理解為 Runtime 中的字典

objc_setAssociatedObject 相當於 setValue:forKey 進行關聯value物件
關聯物件與被關聯物件本身的儲存並沒有直接的關係,它是儲存在單獨的雜湊表中的\

objc_getAssociatedObject 用來讀取物件
objc_removeAssociatedObjects函式來移除一個關聯物件
或者使用objc_setAssociatedObject函式將key指定的關聯物件設定為nil。

相關文章