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

  • 本文首發於個人我的博客:『不羈閣』
  • 文章連接:傳送門
  • 本文更新時間:2019年07月24日20:15:36

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

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

文中示例代碼在: bujige / YSC-Category-Demogit


1. Category (分類)簡介

1.1 什麼是 Category(分類)?

Category(分類) 是 Objective-C 2.0 添加的語言特性,主要做用是爲已經存在的類添加方法。Category 能夠作到在既不子類化,也不侵入一個類的源碼的狀況下,爲原有的類添加新的方法,從而實現擴展一個類或者分離一個類的目的。在平常開發中咱們經常使用 Category 爲已有的類擴展功能。github

雖然繼承也能爲已有類增長新的方法,並且還能直接增長屬性,但繼承關係增長了沒必要要的代碼複雜度,在運行時,也沒法與父類的原始方法進行區分。因此咱們能夠優先考慮使用自定義 Category(分類)。bootstrap

一般 Category(分類)有如下幾種使用場景:數組

  • 把類的不一樣實現方法分開到不一樣的文件裏。
  • 聲明私有方法。
  • 模擬多繼承。
  • 將 framework 私有方法公開化。

1.2 Category(分類)和 Extension(擴展)

Category(分類)看起來和 Extension(擴展)有點類似。Extension(擴展)有時候也被稱爲 匿名分類。但二者實質上是不一樣的東西。 Extension(擴展)是在編譯階段與該類同時編譯的,是類的一部分。並且 Extension(擴展)中聲明的方法只能在該類的 @implementation 中實現,這也就意味着,你沒法對系統的類(例如 NSString 類)使用 Extension(擴展)。緩存

並且和 Category(分類)不一樣的是,Extension(擴展)不但能夠聲明方法,還能夠聲明成員變量,這是 Category(分類)所作不到的。bash

爲何 Category(分類)不能像 Extension(擴展)同樣添加成員變量?網絡

由於 Extension(擴展)是在編譯階段與該類同時編譯的,就是類的一部分。既然做爲類的一部分,且與類同時編譯,那麼就能夠在編譯階段爲類添加成員變量。數據結構

而 Category(分類)則不一樣, Category(分類)的特性是:能夠在運行時階段動態地爲已有類添加新行爲。 Category(分類)是在運行時期間決定的。而成員變量的內存佈局已經在編譯階段肯定好了,若是在運行時階段添加成員變量的話,就會破壞原有類的內存佈局,從而形成可怕的後果,因此 Category(分類)沒法添加成員變量。app


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 行代碼的龐大文件。

不用慌。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 底層實現
相關文章
相關標籤/搜索