本文用來介紹 iOS 開發中『Runtime』中的 Category 底層原理。經過本文,您將瞭解到:html
- Category (分類)簡介
- Category 的實質
- Category 的加載過程
- Category(分類)和 Class(類)的 +load 方法
- Category 與關聯對象
文中示例代碼在: bujige / YSC-Category-Demogit
Category(分類) 是 Objective-C 2.0 添加的語言特性,主要做用是爲已經存在的類添加方法。Category 能夠作到在既不子類化,也不侵入一個類的源碼的狀況下,爲原有的類添加新的方法,從而實現擴展一個類或者分離一個類的目的。在平常開發中咱們經常使用 Category 爲已有的類擴展功能。github
雖然繼承也能爲已有類增長新的方法,並且還能直接增長屬性,但繼承關係增長了沒必要要的代碼複雜度,在運行時,也沒法與父類的原始方法進行區分。因此咱們能夠優先考慮使用自定義 Category(分類)。bootstrap
一般 Category(分類)有如下幾種使用場景:數組
Category(分類)看起來和 Extension(擴展)有點類似。Extension(擴展)有時候也被稱爲 匿名分類。但二者實質上是不一樣的東西。 Extension(擴展)是在編譯階段與該類同時編譯的,是類的一部分。並且 Extension(擴展)中聲明的方法只能在該類的 @implementation
中實現,這也就意味着,你沒法對系統的類(例如 NSString 類)使用 Extension(擴展)。緩存
並且和 Category(分類)不一樣的是,Extension(擴展)不但能夠聲明方法,還能夠聲明成員變量,這是 Category(分類)所作不到的。bash
爲何 Category(分類)不能像 Extension(擴展)同樣添加成員變量?網絡
由於 Extension(擴展)是在編譯階段與該類同時編譯的,就是類的一部分。既然做爲類的一部分,且與類同時編譯,那麼就能夠在編譯階段爲類添加成員變量。數據結構
而 Category(分類)則不一樣, Category(分類)的特性是:能夠在運行時階段動態地爲已有類添加新行爲。 Category(分類)是在運行時期間決定的。而成員變量的內存佈局已經在編譯階段肯定好了,若是在運行時階段添加成員變量的話,就會破壞原有類的內存佈局,從而形成可怕的後果,因此 Category(分類)沒法添加成員變量。app
在第一篇 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(分類)沒法添加成員變量。
想要了解 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++ 源碼方法以下:
- 在項目中添加 Person 類文件 Person.h 和 Person.m,Person 類繼承自 NSObject 。
- 在項目中添加 Person 類的 Category 文件 Person+Addition.h 和 Person+Addition.m,並在 Category 中添加的相關對象方法,類方法,屬性,以及代理。
- 打開『終端』,執行
cd XXX/XXX
命令,其中XXX/XXX
爲 Category 文件 所在的目錄。- 繼續在終端執行
clang -rewrite-objc Person+Addition.m
- 執行完命令以後,Person+Addition.m 所在目錄下就會生成一個 Person+Addition.cpp 文件,這就是咱們須要的 Category(分類) 相關的 C++ 源碼。
當咱們獲得 Person+Addition.cpp 文件以後,就會神奇的發現:這是一個 3.7M 大小,擁有近 10W 行代碼的龐大文件。
不用慌。Category 的相關 C++ 源碼在文件的最底部。咱們刪除其餘無關代碼,只保留 Category 有關的代碼,大概就會剩下差很少 200 多行代碼。下邊咱們根據 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 結構體』源碼中咱們能夠看到:
- Categor 結構體。
- Category 結構體的賦值語句。
- Category 結構體數組。
第一個 Categor 結構體和 2.1 Category 結構體簡介 中的結構體其實質是一一對應的。能夠看作是同一個結構體。第三個 Category 結構體數組中存放了 Person 類的相關分類,若是有多個分類,則數組中存放對應數目的 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}}
};
複製代碼
從『對象方法列表結構體』源碼中咱們能夠看到:
- (void)printName;
對象方法的實現。- (void)personProtocolMethod;
方法的實現。- 對象方法列表結構體。
只要是 Category 中 實現了 的對象方法(包括代理中的對象方法)。都會添加到 對象方法列表結構體
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition
中來。若是隻是在 Person.h 中定義,而沒有實現,則不會添加。
// + (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}}
};
複製代碼
從『類方法列表結構體』源碼中咱們能夠看到:
+ (void)printClassName;
類方法的實現。+ (void)personProtocolClassMethod;
類方法的實現。- 類方法列表結構體。
只要是 Category 中 實現了 的類方法(包括代理中的類方法)。都會添加到 類方法列表結構體
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition
中來。若是隻是在 Person.h 中定義,而沒有實現,則不會添加。
// 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;
複製代碼
從『協議列表結構體』源碼中咱們能夠看到:
- 協議列表結構體。
- 協議列表 對象方法列表結構體。
- 協議列表 類方法列表結構體。
- PersonProtocol 協議結構體賦值語句。
// 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 中不能添加成員變量這一事實。
下面咱們來總結一下 Category 的本質:
Category 的本質就是
_category_t 結構體
類型,其中包含了如下幾部分:
_method_list_t
類型的『對象方法列表結構體』;_method_list_t
類型的『類方法列表結構體』;_protocol_list_t
類型的『協議列表結構體』;_prop_list_t
類型的『屬性列表結構體』。
_category_t 結構體
中不包含_ivar_list_t
類型,也就是不包含『成員變量結構體』。
以前咱們談到過 Category(分類)是在運行時階段動態加載的。而 Runtime(運行時) 加載的過程,離不開一個叫作 dyld 的動態連接器。
在 MacOS 和 iOS 上,動態連接加載器 dyld 用來加載全部的庫和可執行文件。而加載Runtime(運行時) 的過程,就是在 dyld 加載的時候發生的。
dyld 的相關代碼可在蘋果開源網站上進行下載。 連接地址:dyld 蘋果開源代碼
dyld 加載的流程大體是這樣:
本文中,咱們只須要關心的是第 7 步,由於 Runtime(運行時) 是在這一步初始化的。加載 Category(分類)天然也是在這個過程當中。
初始化主程序中,Runtime 初始化的調用棧以下:
dyldbootstrap::start
--->dyld::_main
--->initializeMainExecutable
--->runInitializers
--->recursiveInitialization
--->doInitialization
--->doModInitFunctions
--->_objc_init
最後調用的 _objc_init
是 libobjc
庫中的方法, 是 Runtime 的初始化過程,也是 Objective-C 的入口。
運行時相關的代碼可在蘋果開源網站上進行下載。 連接地址: objc4 蘋果開源代碼
在 _objc_init
這一步中:Runtime
向 dyld
綁定了回調,當 image
加載到內存後,dyld
會通知 Runtime
進行處理,Runtime
接手後調用 map_images
作解析和處理,調用 _read_images
方法把 Category(分類)
的對象方法、協議、屬性添加到類上,把 Category(分類)
的類方法、協議添加到類的 metaclass
上;接下來 load_images
中調用 call_load_methods
方法,遍歷全部加載進來的 Class
,按繼承層級和編譯順序依次調用 Class
的 load
方法和其 Category
的 load
方法。
加載Category(分類)的調用棧以下:
_objc_init
--->map_images
--->map_images_nolock
--->_read_images(加載分類)
--->load_images
。
既然咱們知道了 Category(分類)的加載發生在 _read_images
方法中,那麼咱們只須要關注_read_images
方法中關於分類加載的代碼便可。
_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);
重建類的方法列表經過這兩個方法達到了兩個目的:
Category(分類)
的對象方法、協議、屬性添加到類上;Category(分類)
的類方法、協議添加到類的 metaclass
上。下面來講說上邊提到的這兩個方法。
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);
方法。
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);
方法。
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(分類)中的方法會覆蓋掉原有類的方法的最直接緣由。
Category(分類)中的的方法、屬性、協議附加到類上的操做,是在 + load
方法執行以前進行的。也就是說,在 + load
方法執行以前,類中就已經加載了 Category(分類)中的的方法、屬性、協議。
而 Category(分類)和 Class(類)的 + load
方法的調用順序規則以下所示:
+ load
方法除非主動調用,不然只會調用一次。經過這樣的調用規則,咱們能夠知道:主類的 + load
方法調用必定在分類 + load
方法調用以前。可是分類 + load
方法調用順序並不不是按照繼承關係調用的,而是依照編譯順序肯定的,這也致使了 + load
方法的調用順序並不必定肯定。一個順序多是:父類 -> 子類 -> 父類類別 -> 子類類別
,也多是 父類 -> 子類 -> 子類類別 -> 父類類別
。
以前咱們提到過,在 Category 中雖然能夠添加屬性,可是不會生成對應的成員變量,也不能生成 getter
、setter
方法。所以,在調用 Category 中聲明的屬性時會報錯。
那麼就沒有辦法使用 Category 中的屬性了嗎?
答案固然是否認的。
咱們能夠本身來實現 getter
、setter
方法,並藉助關聯對象(Objective-C Associated Objects)來實現 getter
、setter
方法。關聯對象可以幫助咱們在運行時階段將任意的屬性關聯到一個對象上。具體須要用到如下幾個方法:
// 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);
複製代碼
下面講解一個示例。
/********************* 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 關聯屬性,並實現了 getter
、setter
方法。
注意:使用
objc_removeAssociatedObjects
能夠斷開全部的關聯。一般狀況下不建議使用,由於它會斷開全部的關聯。若是想要斷開關聯可使用objc_setAssociatedObject
,將關聯對象傳入 nil 便可。
最後說一句,其實一開始只想隨便寫寫關於 Category 與關聯對象。結果不當心觸碰到了 Category 的底層知識。。。而後就不當心寫多了。心累。。。
文中如如有誤,煩請指正,感謝。
iOS 開發:『Runtime』詳解 系列文章:
還沒有完成: