探秘Runtime - 深入剖析Category

該文章屬于劉小壯原創,轉載請注明:劉小壯


Category

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

Category語法很相似的還有Extension,二者的區別在于,Extension在編譯期就直接和原類編譯在一起,而Category是在運行時動態添加到原類中的。

基于之前的源碼分析,我們來分析一下Category的實現原理。

_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函數,執行具體的添加操作。

// 將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結構體中。

// 獲取到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中。

方法覆蓋

在有多個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
麻煩各位大佬點個贊,謝謝!??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379