iOS 開發:『Runtime』詳解(三)Category 底層原理

行走少年郎發表於2019-07-25
  • 本文首發於我的個人部落格:『不羈閣』
  • 文章連結:傳送門
  • 本文更新時間:2019年07月24日20:15:36

本文用來介紹 iOS 開發中『Runtime』中的 Category 底層原理。通過本文,您將瞭解到:

  1. Category (分類)簡介
  2. Category 的實質
  3. Category 的載入過程
  4. Category(分類)和 Class(類)的 +load 方法
  5. Category 與關聯物件

文中示例程式碼在: bujige / YSC-Category-Demo


1. Category (分類)簡介

1.1 什麼是 Category(分類)?

Category(分類) 是 Objective-C 2.0 新增的語言特性,主要作用是為已經存在的類新增方法。Category 可以做到在既不子類化,也不侵入一個類的原始碼的情況下,為原有的類新增新的方法,從而實現擴充套件一個類或者分離一個類的目的。在日常開發中我們常常使用 Category 為已有的類擴充套件功能。

雖然繼承也能為已有類增加新的方法,而且還能直接增加屬性,但繼承關係增加了不必要的程式碼複雜度,在執行時,也無法與父類的原始方法進行區分。所以我們可以優先考慮使用自定義 Category(分類)。

通常 Category(分類)有以下幾種使用場景:

  • 把類的不同實現方法分開到不同的檔案裡。
  • 宣告私有方法。
  • 模擬多繼承。
  • 將 framework 私有方法公開化。

1.2 Category(分類)和 Extension(擴充套件)

Category(分類)看起來和 Extension(擴充套件)有點相似。Extension(擴充套件)有時候也被稱為 匿名分類。但兩者實質上是不同的東西。 Extension(擴充套件)是在編譯階段與該類同時編譯的,是類的一部分。而且 Extension(擴充套件)中宣告的方法只能在該類的 @implementation 中實現,這也就意味著,你無法對系統的類(例如 NSString 類)使用 Extension(擴充套件)。

而且和 Category(分類)不同的是,Extension(擴充套件)不但可以宣告方法,還可以宣告成員變數,這是 Category(分類)所做不到的。

為什麼 Category(分類)不能像 Extension(擴充套件)一樣新增成員變數?

因為 Extension(擴充套件)是在編譯階段與該類同時編譯的,就是類的一部分。既然作為類的一部分,且與類同時編譯,那麼就可以在編譯階段為類新增成員變數。

而 Category(分類)則不同, Category(分類)的特性是:可以在執行時階段動態地為已有類新增新行為。 Category(分類)是在執行時期間決定的。而成員變數的記憶體佈局已經在編譯階段確定好了,如果在執行時階段新增成員變數的話,就會破壞原有類的記憶體佈局,從而造成可怕的後果,所以 Category(分類)無法新增成員變數。


2. Category 的實質

2.1 Category 結構體簡介

在第一篇 iOS 開發:『Runtime』詳解(一)基礎知識 中我們知道了:Object(物件)Class(類) 的實質分別是 objc_object 結構體objc_class 結構體,這裡 Category 也不例外,在 objc-runtime-new.h 中,Category(分類)被定義為 category_t 結構體category_t 結構體 的資料結構如下:

typedef struct category_t *Category;

struct category_t {
    const char *name;                                // 類名
    classref_t cls;                                  // 類,在執行時階段通過 clasee_name(類名)對應到類物件
    struct method_list_t *instanceMethods;           // Category 中所有新增的物件方法列表
    struct method_list_t *classMethods;              // Category 中所有新增的類方法列表
    struct protocol_list_t *protocols;               // Category 中實現的所有協議列表
    struct property_list_t *instanceProperties;      // Category 中新增的所有屬性
};
複製程式碼

從 Category(分類)的結構體定義中也可以看出, Category(分類)可以為類新增物件方法、類方法、協議、屬性。同時,也能發現 Category(分類)無法新增成員變數。

2.2 Category 的 C++ 原始碼

想要了解 Category 的本質,我們需要藉助於 Category 的 C++ 原始碼。 首先呢,我們需要寫一個繼承自 NSObject 的 Person 類,還需要寫一個 Person+Additon 的分類。在分類中新增物件方法,類方法,屬性,以及代理。

例如下邊程式碼中這樣:

/********************* Person+Addition.h 檔案 *********************/

#import "Person.h"

// PersonProtocol 代理
@protocol PersonProtocol <NSObject>

- (void)PersonProtocolMethod;

+ (void)PersonProtocolClassMethod;

@end

@interface Person (Addition) <PersonProtocol>

/* name 屬性 */
@property (nonatomic, copy) NSString *personName;

// 類方法
+ (void)printClassName;

// 物件方法
- (void)printName;

@end

/********************* Person+Addition.m 檔案 *********************/

#import "Person+Addition.h"

@implementation Person (Addition)

+ (void)printClassName {
    NSLog(@"printClassName");
}

- (void)printName {
    NSLog(@"printName");
}

#pragma mark - <PersonProtocol> 方法

- (void)PersonProtocolMethod {
    NSLog(@"PersonProtocolMethod");
}

+ (void)PersonProtocolClassMethod {
    NSLog(@"PersonProtocolClassMethod");
}
複製程式碼

Category 由 OC 轉 C++ 原始碼方法如下:

  1. 在專案中新增 Person 類檔案 Person.h 和 Person.m,Person 類繼承自 NSObject 。
  2. 在專案中新增 Person 類的 Category 檔案 Person+Addition.h 和 Person+Addition.m,並在 Category 中新增的相關物件方法,類方法,屬性,以及代理。
  3. 開啟『終端』,執行 cd XXX/XXX 命令,其中 XXX/XXX 為 Category 檔案 所在的目錄。
  4. 繼續在終端執行 clang -rewrite-objc Person+Addition.m
  5. 執行完命令之後,Person+Addition.m 所在目錄下就會生成一個 Person+Addition.cpp 檔案,這就是我們需要的 Category(分類) 相關的 C++ 原始碼。

當我們得到 Person+Addition.cpp 檔案之後,就會神奇的發現:這是一個 3.7M 大小,擁有近 10W 行程式碼的龐大檔案。

iOS 開發:『Runtime』詳解(三)Category 底層原理

不用慌。Category 的相關 C++ 原始碼在檔案的最底部。我們刪除其他無關程式碼,只保留 Category 有關的程式碼,大概就會剩下差不多 200 多行程式碼。下邊我們根據 Category 結構體 的不同結構,分模組來講解一下。

2.2.1 『Category 結構體』

// Person 類的 Category 結構體
struct _category_t {
	const char *name;
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;
	const struct _method_list_t *class_methods;
	const struct _protocol_list_t *protocols;
	const struct _prop_list_t *properties;
};

// Person 類的 Category 結構體賦值
static struct _category_t _OBJC_$_CATEGORY_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person",
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Addition,
};

// Category 陣列,如果 Person 有多個分類,則 Category 陣列中對應多個 Category 
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_Person_$_Addition,
};
複製程式碼

從『Category 結構體』原始碼中我們可以看到:

  1. Categor 結構體。
  2. Category 結構體的賦值語句。
  3. Category 結構體陣列。

第一個 Categor 結構體和 2.1 Category 結構體簡介 中的結構體其實質是一一對應的。可以看做是同一個結構體。第三個 Category 結構體陣列中存放了 Person 類的相關分類,如果有多個分類,則陣列中存放對應數目的 Category 結構體。

2.2.2 Category 中『物件方法列表結構體』

// - (void)printName; 物件方法的實現
static void _I_Person_Addition_printName(Person * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_Person_Addition_405207_mi_1);
}

// - (void)personProtocolMethod; 方法的實現
static void _I_Person_Addition_personProtocolMethod(Person * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_Person_Addition_f09f6a_mi_2);
}

// Person 分類中新增的『物件方法列表結構體』
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"printName", "v16@0:8", (void *)_I_Person_Addition_printName},
	{(struct objc_selector *)"personProtocolMethod", "v16@0:8", (void *)_I_Person_Addition_personProtocolMethod}}
};
複製程式碼

從『物件方法列表結構體』原始碼中我們可以看到:

  1. - (void)printName; 物件方法的實現。
  2. - (void)personProtocolMethod; 方法的實現。
  3. 物件方法列表結構體。

只要是 Category 中 實現了 的物件方法(包括代理中的物件方法)。都會新增到 物件方法列表結構體 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition 中來。如果只是在 Person.h 中定義,而沒有實現,則不會新增。

2.2.3 Category 中『類方法列表結構體』

// + (void)printClassName; 類方法的實現
static void _C_Person_Addition_printClassName(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_Person_Addition_c2e684_mi_0);
}

// + (void)personProtocolClassMethod; 方法的實現
static void _C_Person_Addition_personProtocolClassMethod(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_Person_Addition_c2e684_mi_3);
}

// Person 分類中新增的『類方法列表結構體』
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"printClassName", "v16@0:8", (void *)_C_Person_Addition_printClassName},
	{(struct objc_selector *)"personProtocolClassMethod", "v16@0:8", (void *)_C_Person_Addition_personProtocolClassMethod}}
};
複製程式碼

從『類方法列表結構體』原始碼中我們可以看到:

  1. + (void)printClassName; 類方法的實現。
  2. + (void)personProtocolClassMethod; 類方法的實現。
  3. 類方法列表結構體。

只要是 Category 中 實現了 的類方法(包括代理中的類方法)。都會新增到 類方法列表結構體 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition 中來。如果只是在 Person.h 中定義,而沒有實現,則不會新增。

2.2.4 Category 中『協議列表結構體』

// Person 分類中新增的『協議列表結構體』
static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	1,
	&_OBJC_PROTOCOL_PersonProtocol
};

// 協議列表 物件方法列表結構體
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_PersonProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"personProtocolMethod", "v16@0:8", 0}}
};

// 協議列表 類方法列表結構體
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_PROTOCOL_CLASS_METHODS_PersonProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"personProtocolClassMethod", "v16@0:8", 0}}
};

// PersonProtocol 結構體賦值
struct _protocol_t _OBJC_PROTOCOL_PersonProtocol __attribute__ ((used)) = {
	0,
	"PersonProtocol",
	(const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_PersonProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_PersonProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_CLASS_METHODS_PersonProtocol,
	0,
	0,
	0,
	sizeof(_protocol_t),
	0,
	(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_PersonProtocol
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_PersonProtocol = &_OBJC_PROTOCOL_PersonProtocol;
複製程式碼

從『協議列表結構體』原始碼中我們可以看到:

  1. 協議列表結構體。
  2. 協議列表 物件方法列表結構體。
  3. 協議列表 類方法列表結構體。
  4. PersonProtocol 協議結構體賦值語句。

2.2.5 Category 中『屬性列表結構體』

// Person 分類中新增的屬性列表
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Addition __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"personName","T@\"NSString\",C,N"}}
};
複製程式碼

從『屬性列表結構體』原始碼中我們看到:

只有 Person 分類中新增的 屬性列表結構體 _OBJC_$_PROP_LIST_Person_$_Addition,沒有成員變數結構體 _ivar_list_t 結構體。更沒有對應的 set 方法 / get 方法 相關的內容。這也直接說明了 Category 中不能新增成員變數這一事實。


2.3 Category 的實質總結

下面我們來總結一下 Category 的本質

Category 的本質就是 _category_t 結構體 型別,其中包含了以下幾部分:

  1. _method_list_t 型別的『物件方法列表結構體』;
  2. _method_list_t 型別的『類方法列表結構體』;
  3. _protocol_list_t 型別的『協議列表結構體』;
  4. _prop_list_t 型別的『屬性列表結構體』。

_category_t 結構體 中不包含 _ivar_list_t 型別,也就是不包含『成員變數結構體』。


3. Category 的載入過程

3.1 dyld 載入大致流程

之前我們談到過 Category(分類)是在執行時階段動態載入的。而 Runtime(執行時) 載入的過程,離不開一個叫做 dyld 的動態連結器。

在 MacOS 和 iOS 上,動態連結載入器 dyld 用來載入所有的庫和可執行檔案。而載入Runtime(執行時) 的過程,就是在 dyld 載入的時候發生的。

dyld 的相關程式碼可在蘋果開源網站上進行下載。 連結地址:dyld 蘋果開原始碼

dyld 載入的流程大致是這樣:

  1. 配置環境變數;
  2. 載入共享快取;
  3. 初始化主 APP;
  4. 插入動態快取庫;
  5. 連結主程式;
  6. 連結插入的動態庫;
  7. 初始化主程式:OC, C++ 全域性變數初始化;
  8. 返回主程式入口函式。

本文中,我們只需要關心的是第 7 步,因為 Runtime(執行時) 是在這一步初始化的。載入 Category(分類)自然也是在這個過程中。

初始化主程式中,Runtime 初始化的呼叫棧如下:

dyldbootstrap::start ---> dyld::_main ---> initializeMainExecutable ---> runInitializers ---> recursiveInitialization ---> doInitialization ---> doModInitFunctions ---> _objc_init

最後呼叫的 _objc_initlibobjc 庫中的方法, 是 Runtime 的初始化過程,也是 Objective-C 的入口。

執行時相關的程式碼可在蘋果開源網站上進行下載。 連結地址: objc4 蘋果開原始碼

_objc_init 這一步中:Runtimedyld 繫結了回撥,當 image 載入到記憶體後,dyld 會通知 Runtime 進行處理,Runtime 接手後呼叫 map_images 做解析和處理,呼叫 _read_images 方法把 Category(分類) 的物件方法、協議、屬性新增到類上,把 Category(分類) 的類方法、協議新增到類的 metaclass 上;接下來 load_images 中呼叫 call_load_methods 方法,遍歷所有載入進來的 Class,按繼承層級和編譯順序依次呼叫 Classload 方法和其 Categoryload 方法。

載入Category(分類)的呼叫棧如下:

_objc_init ---> map_images ---> map_images_nolock ---> _read_images(載入分類) ---> load_images

既然我們知道了 Category(分類)的載入發生在 _read_images 方法中,那麼我們只需要關注_read_images 方法中關於分類載入的程式碼即可。

3.2 Category(分類) 載入過程

3.2.1 _read_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);
    // 處理這個分類
    // 首先,使用目標類註冊當前分類
    // 然後,如果實現了這個類,重建類的方法列表
    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());
        }
    }
}
複製程式碼

主要用到了兩個方法:

  • addUnattachedCategoryForClass(cat, cls, hi); 為類新增未依附的分類
  • remethodizeClass(cls); 重建類的方法列表

通過這兩個方法達到了兩個目的:

  1. Category(分類) 的物件方法、協議、屬性新增到類上;
  2. Category(分類) 的類方法、協議新增到類的 metaclass 上。

下面來說說上邊提到的這兩個方法。

3.2.2 addUnattachedCategoryForClass(cat, cls, hi); 方法

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

    // 取得儲存所有未依附分類的列表:cats
    NXMapTable *cats = unattachedCategories();
    category_list *list;
    // 從 cats 列表中找到 cls 對應的未依附分類的列表:list
    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));
    }
    // 將新增的分類 cat 新增 list 中
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    // 將新生成的 list 新增重新插入 cats 中,會覆蓋舊的 list
    NXMapInsert(cats, cls, list);
}
複製程式碼

addUnattachedCategoryForClass(cat, cls, hi); 的執行過程可以參考程式碼註釋。執行完這個方法之後,系統會將當前分類 cat 放到該類 cls 對應的未依附分類的列表 list 中。這句話有點拗口,簡而言之,就是:把類和分類做了一個關聯對映。

實際上真正起到新增載入作用的是下邊的 remethodizeClass(cls); 方法。

3.2.3 remethodizeClass(cls); 方法

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

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // 取得 cls 類的未依附分類的列表:cats
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        // 將未依附分類的列表 cats 附加到 cls 類上
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
複製程式碼

remethodizeClass(cls); 方法主要就做了一件事:呼叫 attachCategories(cls, cats, true); 方法將未依附分類的列表 cats 附加到 cls 類上。所以,我們就再來看看 attachCategories(cls, cats, true); 方法。

3.2.4 attachCategories(cls, cats, true); 方法

我發誓這是本文中載入 Category(分類)過程的最後一段程式碼。不過也是最為核心的一段程式碼。

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

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

    // 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;     // 記錄是否是從 bundle 中取的
    while (i--) { // 從後往前依次遍歷
        auto& entry = cats->list[i];  // 取出當前分類
    
        // 取出分類中的方法列表。如果是元類,取得的是類方法列表;否則取得的是物件方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;            // 將方法列表放入 mlists 方法列表陣列中
            fromBundle |= entry.hi->isBundle();  // 分類的頭部資訊中儲存了是否是 bundle,將其記住
        }

        // 取出分類中的屬性列表,如果是元類,取得的是 nil
        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;
        }
    }

    // 取出當前類 cls 的 class_rw_t 資料
    auto rw = cls->data();

    // 儲存方法、屬性、協議陣列到 rw 中
    // 準備方法列表 mlists 中的方法
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 將新方法列表新增到 rw 中的方法列表中
    rw->methods.attachLists(mlists, mcount);
    // 釋放方法列表 mlists
    free(mlists);
    // 清除 cls 的快取列表
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    // 將新屬性列表新增到 rw 中的屬性列表中
    rw->properties.attachLists(proplists, propcount);
    // 釋放屬性列表
    free(proplists);

    // 將新協議列表新增到 rw 中的協議列表中
    rw->protocols.attachLists(protolists, protocount);
    // 釋放協議列表
    free(protolists);
}
複製程式碼

attachCategories(cls, cats, true); 方法的註釋中可以看出這個方法就是儲存分類的方法、屬性、協議的核心程式碼。

但是需要注意一些細節問題:

  • Category(分類)的方法、屬性、協議只是新增到原有類上,並沒有將原有類的方法、屬性、協議進行完全替換。 舉個例子說明就是:假設原有類擁有 MethodA方法,分類也擁有 MethodA 方法,那麼載入完分類之後,類的方法列表中會擁有兩個 MethodA方法。
  • Category(分類)的方法、屬性、協議會被新增到原有類的方法列表、屬性列表、協議列表的最前面,而原有類的方法、屬性、協議則被移動到了列表後面。 因為在執行時查詢方法的時候是順著方法列表的順序依次查詢的,所以 Category(分類)的方法會先被搜尋到,然後直接執行,而原有類的方法則不被執行。這也是 Category(分類)中的方法會覆蓋掉原有類的方法的最直接原因。

4. Category(分類)和 Class(類)的 +load 方法

Category(分類)中的的方法、屬性、協議附加到類上的操作,是在 + load 方法執行之前進行的。也就是說,在 + load 方法執行之前,類中就已經載入了 Category(分類)中的的方法、屬性、協議。

而 Category(分類)和 Class(類)的 + load 方法的呼叫順序規則如下所示:

  1. 先呼叫主類,按照編譯順序,順序地根據繼承關係由父類向子類呼叫;
  2. 呼叫完主類,再呼叫分類,按照編譯順序,依次呼叫;ıÏÏ
  3. + load 方法除非主動呼叫,否則只會呼叫一次。

通過這樣的呼叫規則,我們可以知道:主類的 + load 方法呼叫一定在分類 + load 方法呼叫之前。但是分類 + load 方法呼叫順序並不不是按照繼承關係呼叫的,而是依照編譯順序確定的,這也導致了 + load 方法的呼叫順序並不一定確定。一個順序可能是:父類 -> 子類 -> 父類類別 -> 子類類別,也可能是 父類 -> 子類 -> 子類類別 -> 父類類別


5. Category 與關聯物件

之前我們提到過,在 Category 中雖然可以新增屬性,但是不會生成對應的成員變數,也不能生成 gettersetter 方法。因此,在呼叫 Category 中宣告的屬性時會報錯。

那麼就沒有辦法使用 Category 中的屬性了嗎?

答案當然是否定的。

我們可以自己來實現 gettersetter 方法,並藉助關聯物件(Objective-C Associated Objects)來實現 gettersetter 方法。關聯物件能夠幫助我們在執行時階段將任意的屬性關聯到一個物件上。具體需要用到以下幾個方法:

// 1. 通過 key : value 的形式給物件 object 設定關聯屬性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 2. 通過 key 獲取關聯的屬性 object
id objc_getAssociatedObject(id object, const void *key);

// 3. 移除物件所關聯的屬性
void objc_removeAssociatedObjects(id object);
複製程式碼

下面講解一個示例。

5.1 UIImage 分類中增加網路地址屬性

/********************* UIImage+Property.h 檔案 *********************/

#import <UIKit/UIKit.h>

@interface UIImage (Property)

/* 圖片網路地址 */
@property (nonatomic, copy) NSString *urlString;

// 用於清除關聯物件
- (void)clearAssociatedObjcet;

@end

/********************* UIImage+Property.m 檔案 *********************/

#import "UIImage+Property.h"
#import <objc/runtime.h>

@implementation UIImage (Property)

// set 方法
- (void)setUrlString:(NSString *)urlString {
    objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

// get 方法
- (NSString *)urlString {
    return objc_getAssociatedObject(self, @selector(urlString));
}

// 清除關聯物件
- (void)clearAssociatedObjcet {
    objc_removeAssociatedObjects(self);
}

@end
複製程式碼

測試程式碼:

UIImage *image = [[UIImage alloc] init];
image.urlString = @"http://www.image.png";

NSLog(@"image urlString = %@",image.urlString);

[image clearAssociatedObjcet];
NSLog(@"image urlString = %@",image.urlString);
複製程式碼

列印結果: 2019-07-24 18:36:31.051789+0800 YSC-Category[74564:17944298] image urlString = www.image.png 2019-07-24 18:36:31.051926+0800 YSC-Category[74564:17944298] image urlString = (null)

可以看到:藉助關聯物件,我們成功的在 UIImage 分類中為 UImage 類增加了 urlString 關聯屬性,並實現了 gettersetter 方法。

注意:使用 objc_removeAssociatedObjects 可以斷開所有的關聯。通常情況下不建議使用,因為它會斷開所有的關聯。如果想要斷開關聯可以使用 objc_setAssociatedObject,將關聯物件傳入 nil 即可。


參考資料


最後

最後說一句,其實一開始只想隨便寫寫關於 Category 與關聯物件。結果不小心觸碰到了 Category 的底層知識。。。然後就不小心寫多了。心累。。。

文中如若有誤,煩請指正,感謝。


iOS 開發:『Runtime』詳解 系列文章:

尚未完成:

  • iOS 開發:『Runtime』詳解(五)Crash 防護系統
  • iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
  • iOS 開發:『Runtime』詳解(七)KVO 底層實現

相關文章