探祕Runtime - 深刻剖析Category

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/0dc2513e117bgit


博客配圖


Category

有了以前Runtime的基礎,一些內部實現就很好理解了。在OC中能夠經過Category添加屬性、方法、協議,在RuntimeClassCategory都是經過結構體實現的。github

Category語法很類似的還有Extension,兩者的區別在於,Extension在編譯期就直接和原類編譯在一塊兒,而Category是在運行時動態添加到原類中的。數組

基於以前的源碼分析,咱們來分析一下Category的實現原理。ide

_read_images函數中會執行一個循環嵌套,外部循環遍歷全部類,並取出當前類對應Category數組。內部循環會遍歷取出的Category數組,將每一個category_t對象取出,最終執行addUnattachedCategoryForClass函數添加到Category哈希表中。函數

// 將category_t添加到list中,並經過NXMapInsert函數,更新所屬類的Category列表
static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    // 獲取到未添加的Category哈希表
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    // 獲取到buckets中的value,並向value對應的數組中添加category_t
    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    // 替換以前的list字段
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
複製代碼

Category維護了一個名爲category_map的哈希表,哈希表存儲全部category_t對象。源碼分析

// 獲取未添加到Class中的category哈希表
static NXMapTable *unattachedCategories(void)
{
    // 未添加到Class中的category哈希表
    static NXMapTable *category_map = nil;

    if (category_map) return category_map;

    // fixme initial map size
    category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);

    return category_map;
}
複製代碼

上面只是完成了向Category哈希表中添加的操做,這時候哈希表中存儲了全部category_t對象。而後須要調用remethodizeClass函數,向對應的Class中添加Category的信息。佈局

remethodizeClass函數中會查找傳入的Class參數對應的Category數組,而後將數組傳給attachCategories函數,執行具體的添加操做。ui

// 將Category的信息添加到Class,包含method、property、protocol
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;
    isMeta = cls->isMetaClass();

    // 從Category哈希表中查找category_t對象,並將已找到的對象從哈希表中刪除
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
複製代碼

attachCategories函數中,查找到Category的方法列表、屬性列表、協議列表,而後經過對應的attachLists函數,添加到Class對應的class_rw_t結構體中。this

// 獲取到Category的Protocol list、Property list、Method list,而後經過attachLists函數添加到所屬的類中
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // 按照Category個數,分配對應的內存空間
    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;
    
    // 循環查找出Protocol list、Property list、Method list
    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();

    // 執行添加操做
    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);
}
複製代碼

這個過程就是將Category中的信息,添加到對應的Class中,一個類的Category可能不僅有一個,在這個過程當中會將全部Category的信息都合併到Class中。atom

方法覆蓋

在有多個Category和原類的方法重複定義的時候,原類和全部Category的方法都會存在,並不會被後面的覆蓋。假設有一個方法叫作methodCategory和原類的方法都會被添加到方法列表中,只是存在的順序不一樣。

排列順序

在進行方法調用的時候,會優先遍歷Category的方法,而且後面被添加到項目裏的Category,會被優先調用。上面的例子調用順序就是Category3 -> Category2 -> Category1 -> TestObject。若是從方法列表中找到方法後,就不會繼續向後查找,這就是類方法被Category」覆蓋」的緣由。

問題

在有多個Category和原類方法重名的狀況下,怎樣在一個Category的方法被調用後,調用全部Category和原類的方法?

能夠在一個Category方法被調用後,遍歷方法列表並調用其餘同名方法。可是須要注意一點是,遍歷過程當中不能再調用本身的方法,不然會致使遞歸調用。爲了不這個問題,能夠在調用前判斷被調動的方法IMP是否當前方法的IMP

那怎樣在任何一個Category的方法被調用後,只調用原類方法呢?

根據上面對方法調用的分析,Runtime在調用方法時會優先全部Category調用,因此能夠倒敘遍歷方法列表,只遍歷第一個方法便可,這個方法就是原類的方法。

Category Associate

在項目中常常會用到Category,有時候會遇到給Category添加屬性的需求,這時候就須要用到associatedRuntime API了。例以下面的例子中,須要在屬性的setget方法中動態添加實現。

// 聲明文件
@interface TestObject (Category)
@property (nonatomic, strong) NSObject *object;
@end

// 實現文件
#import <objc/runtime.h>
#import <objc/message.h>
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
複製代碼

Category中添加屬性後,默認是沒有實現方法的,若是調用屬性則會崩潰,並且還會提示下面兩個警告信息。

Property 'object' requires method 'object' to be defined - use @dynamic or provide a method implementation in this category

Property 'object' requires method 'setObject:' to be defined - use @dynamic or provide a method implementation in this category
複製代碼

下面讓咱們看一下associated的源碼,看Runtime是怎麼經過Runtime動態添加setget的。下面是objc_getAssociatedObject函數的實現代碼,objc_setAssociatedObject實現也是相似,這裏節省地方就不貼出來了。

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
複製代碼

從源碼能夠看出,全部經過associated添加的屬性,都被存在一個單獨的哈希表AssociationsHashMap中。objc_setAssociatedObjectobjc_getAssociatedObject函數本質上都是在操做這個哈希表,經過對哈希表進行映射來存取對象。

associatedAPI中會設置一些內存管理的關鍵字,例如OBJC_ASSOCIATION_ASSIGN,這是用來指定對象的內存管理的,這些關鍵字在Runtime源碼中也有對應的處理。


簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github上,下載Runtime PDF合集。把全部Runtime文章總計九篇,都寫在這個PDF中,並且左側有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!😁

相關文章
相關標籤/搜索