- 本文首發(fā)于我的個(gè)人博客:「程序員充電站」
- 文章鏈接:「?jìng)魉烷T(mén)」
- 本文更新時(shí)間:2019年07月24日20:15:36
本文用來(lái)介紹 iOS 開(kāi)發(fā)中『Runtime』中的 Category 底層原理跑芳。通過(guò)本文,您將了解到:
- Category (分類)簡(jiǎn)介
- Category 的實(shí)質(zhì)
- Category 的加載過(guò)程
- Category(分類)和 Class(類)的 +load 方法
- Category 與關(guān)聯(lián)對(duì)象
文中示例代碼在: bujige / YSC-Category-Demo
1. Category (分類)簡(jiǎn)介
1.1 什么是 Category(分類)毯欣?
Category(分類) 是 Objective-C 2.0 添加的語(yǔ)言特性趟庄,主要作用是為已經(jīng)存在的類添加方法。Category 可以做到在既不子類化,也不侵入一個(gè)類的源碼的情況下树枫,為原有的類添加新的方法,從而實(shí)現(xiàn)擴(kuò)展一個(gè)類或者分離一個(gè)類的目的景东。在日常開(kāi)發(fā)中我們常常使用 Category 為已有的類擴(kuò)展功能砂轻。
雖然繼承也能為已有類增加新的方法,而且還能直接增加屬性斤吐,但繼承關(guān)系增加了不必要的代碼復(fù)雜度搔涝,在運(yùn)行時(shí),也無(wú)法與父類的原始方法進(jìn)行區(qū)分和措。所以我們可以優(yōu)先考慮使用自定義 Category(分類)庄呈。
通常 Category(分類)有以下幾種使用場(chǎng)景:
- 把類的不同實(shí)現(xiàn)方法分開(kāi)到不同的文件里。
- 聲明私有方法派阱。
- 模擬多繼承抒痒。
- 將 framework 私有方法公開(kāi)化。
1.2 Category(分類)和 Extension(擴(kuò)展)
Category(分類)看起來(lái)和 Extension(擴(kuò)展)有點(diǎn)相似颁褂。Extension(擴(kuò)展)有時(shí)候也被稱為 匿名分類。但兩者實(shí)質(zhì)上是不同的東西傀广。 Extension(擴(kuò)展)是在編譯階段與該類同時(shí)編譯的颁独,是類的一部分。而且 Extension(擴(kuò)展)中聲明的方法只能在該類的 @implementation
中實(shí)現(xiàn)伪冰,這也就意味著誓酒,你無(wú)法對(duì)系統(tǒng)的類(例如 NSString 類)使用 Extension(擴(kuò)展)。
而且和 Category(分類)不同的是贮聂,Extension(擴(kuò)展)不但可以聲明方法靠柑,還可以聲明成員變量,這是 Category(分類)所做不到的吓懈。
為什么 Category(分類)不能像 Extension(擴(kuò)展)一樣添加成員變量歼冰?
因?yàn)?Extension(擴(kuò)展)是在編譯階段與該類同時(shí)編譯的,就是類的一部分耻警。既然作為類的一部分,且與類同時(shí)編譯,那么就可以在編譯階段為類添加成員變量缩多。
而 Category(分類)則不同拆又, Category(分類)的特性是:可以在運(yùn)行時(shí)階段動(dòng)態(tài)地為已有類添加新行為。 Category(分類)是在運(yùn)行時(shí)期間決定的温兼。而成員變量的內(nèi)存布局已經(jīng)在編譯階段確定好了秸滴,如果在運(yùn)行時(shí)階段添加成員變量的話募判,就會(huì)破壞原有類的內(nèi)存布局荡含,從而造成可怕的后果咒唆,所以 Category(分類)無(wú)法添加成員變量。
2. Category 的實(shí)質(zhì)
2.1 Category 結(jié)構(gòu)體簡(jiǎn)介
在第一篇 iOS 開(kāi)發(fā):『Runtime』詳解(一)基礎(chǔ)知識(shí) 中我們知道了:Object(對(duì)象)
和 Class(類)
的實(shí)質(zhì)分別是 objc_object 結(jié)構(gòu)體
和 objc_class 結(jié)構(gòu)體
内颗,這里 Category 也不例外钧排,在 objc-runtime-new.h
中,Category(分類)被定義為 category_t 結(jié)構(gòu)體
均澳。category_t 結(jié)構(gòu)體
的數(shù)據(jù)結(jié)構(gòu)如下:
typedef struct category_t *Category;
struct category_t {
const char *name; // 類名
classref_t cls; // 類恨溜,在運(yùn)行時(shí)階段通過(guò) clasee_name(類名)對(duì)應(yīng)到類對(duì)象
struct method_list_t *instanceMethods; // Category 中所有添加的對(duì)象方法列表
struct method_list_t *classMethods; // Category 中所有添加的類方法列表
struct protocol_list_t *protocols; // Category 中實(shí)現(xiàn)的所有協(xié)議列表
struct property_list_t *instanceProperties; // Category 中添加的所有屬性
};
從 Category(分類)的結(jié)構(gòu)體定義中也可以看出, Category(分類)可以為類添加對(duì)象方法找前、類方法糟袁、協(xié)議、屬性躺盛。同時(shí)项戴,也能發(fā)現(xiàn) Category(分類)無(wú)法添加成員變量。
2.2 Category 的 C++ 源碼
想要了解 Category 的本質(zhì)槽惫,我們需要借助于 Category 的 C++ 源碼周叮。
首先呢,我們需要寫(xiě)一個(gè)繼承自 NSObject 的 Person 類界斜,還需要寫(xiě)一個(gè) Person+Additon 的分類仿耽。在分類中添加對(duì)象方法,類方法各薇,屬性项贺,以及代理。
例如下邊代碼中這樣:
/********************* 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;
// 對(duì)象方法
- (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 轉(zhuǎn) C++ 源碼方法如下:
- 在項(xiàng)目中添加 Person 類文件 Person.h 和 Person.m峭判,Person 類繼承自 NSObject 开缎。
- 在項(xiàng)目中添加 Person 類的 Category 文件 Person+Addition.h 和 Person+Addition.m,并在 Category 中添加的相關(guān)對(duì)象方法林螃,類方法奕删,屬性,以及代理疗认。
- 打開(kāi)『終端』急侥,執(zhí)行
cd XXX/XXX
命令,其中XXX/XXX
為 Category 文件 所在的目錄侮邀。- 繼續(xù)在終端執(zhí)行
clang -rewrite-objc Person+Addition.m
- 執(zhí)行完命令之后坏怪,Person+Addition.m 所在目錄下就會(huì)生成一個(gè) Person+Addition.cpp 文件,這就是我們需要的 Category(分類) 相關(guān)的 C++ 源碼绊茧。
當(dāng)我們得到 Person+Addition.cpp 文件之后铝宵,就會(huì)神奇的發(fā)現(xiàn):這是一個(gè) 3.7M 大小,擁有近 10W 行代碼的龐大文件。
不用慌鹏秋。Category 的相關(guān) C++ 源碼在文件的最底部尊蚁。我們刪除其他無(wú)關(guān)代碼,只保留 Category 有關(guān)的代碼侣夷,大概就會(huì)剩下差不多 200 多行代碼横朋。下邊我們根據(jù) Category 結(jié)構(gòu)體 的不同結(jié)構(gòu),分模塊來(lái)講解一下百拓。
2.2.1 『Category 結(jié)構(gòu)體』
// Person 類的 Category 結(jié)構(gòu)體
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 結(jié)構(gòu)體賦值
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 數(shù)組琴锭,如果 Person 有多個(gè)分類,則 Category 數(shù)組中對(duì)應(yīng)多個(gè) 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 結(jié)構(gòu)體』源碼中我們可以看到:
- Categor 結(jié)構(gòu)體衙传。
- Category 結(jié)構(gòu)體的賦值語(yǔ)句决帖。
- Category 結(jié)構(gòu)體數(shù)組。
第一個(gè) Categor 結(jié)構(gòu)體和 2.1 Category 結(jié)構(gòu)體簡(jiǎn)介 中的結(jié)構(gòu)體其實(shí)質(zhì)是一一對(duì)應(yīng)的蓖捶〉鼗兀可以看做是同一個(gè)結(jié)構(gòu)體。第三個(gè) Category 結(jié)構(gòu)體數(shù)組中存放了 Person 類的相關(guān)分類俊鱼,如果有多個(gè)分類刻像,則數(shù)組中存放對(duì)應(yīng)數(shù)目的 Category 結(jié)構(gòu)體。
2.2.2 Category 中『對(duì)象方法列表結(jié)構(gòu)體』
// - (void)printName; 對(duì)象方法的實(shí)現(xiàn)
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; 方法的實(shí)現(xiàn)
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 分類中添加的『對(duì)象方法列表結(jié)構(gòu)體』
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}}
};
從『對(duì)象方法列表結(jié)構(gòu)體』源碼中我們可以看到:
- (void)printName;
對(duì)象方法的實(shí)現(xiàn)并闲。- (void)personProtocolMethod;
方法的實(shí)現(xiàn)绎速。- 對(duì)象方法列表結(jié)構(gòu)體。
只要是 Category 中 實(shí)現(xiàn)了 的對(duì)象方法(包括代理中的對(duì)象方法)焙蚓。都會(huì)添加到 對(duì)象方法列表結(jié)構(gòu)體
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Addition
中來(lái)。如果只是在 Person.h 中定義洒宝,而沒(méi)有實(shí)現(xiàn)购公,則不會(huì)添加。
2.2.3 Category 中『類方法列表結(jié)構(gòu)體』
// + (void)printClassName; 類方法的實(shí)現(xiàn)
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; 方法的實(shí)現(xiàn)
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 分類中添加的『類方法列表結(jié)構(gòu)體』
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}}
};
從『類方法列表結(jié)構(gòu)體』源碼中我們可以看到:
+ (void)printClassName;
類方法的實(shí)現(xiàn)雁歌。+ (void)personProtocolClassMethod;
類方法的實(shí)現(xiàn)宏浩。- 類方法列表結(jié)構(gòu)體。
只要是 Category 中 實(shí)現(xiàn)了 的類方法(包括代理中的類方法)靠瞎。都會(huì)添加到 類方法列表結(jié)構(gòu)體
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Addition
中來(lái)比庄。如果只是在 Person.h 中定義,而沒(méi)有實(shí)現(xiàn)乏盐,則不會(huì)添加佳窑。
2.2.4 Category 中『協(xié)議列表結(jié)構(gòu)體』
// Person 分類中添加的『協(xié)議列表結(jié)構(gòu)體』
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
};
// 協(xié)議列表 對(duì)象方法列表結(jié)構(gòu)體
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}}
};
// 協(xié)議列表 類方法列表結(jié)構(gòu)體
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 結(jié)構(gòu)體賦值
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;
從『協(xié)議列表結(jié)構(gòu)體』源碼中我們可以看到:
- 協(xié)議列表結(jié)構(gòu)體。
- 協(xié)議列表 對(duì)象方法列表結(jié)構(gòu)體父能。
- 協(xié)議列表 類方法列表結(jié)構(gòu)體神凑。
- PersonProtocol 協(xié)議結(jié)構(gòu)體賦值語(yǔ)句。
2.2.5 Category 中『屬性列表結(jié)構(gòu)體』
// 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"}}
};
從『屬性列表結(jié)構(gòu)體』源碼中我們看到:
只有 Person 分類中添加的 屬性列表結(jié)構(gòu)體
_OBJC_$_PROP_LIST_Person_$_Addition
,沒(méi)有成員變量結(jié)構(gòu)體_ivar_list_t 結(jié)構(gòu)體
溉委。更沒(méi)有對(duì)應(yīng)的set 方法 / get 方法
相關(guān)的內(nèi)容鹃唯。這也直接說(shuō)明了 Category 中不能添加成員變量這一事實(shí)。
2.3 Category 的實(shí)質(zhì)總結(jié)
下面我們來(lái)總結(jié)一下 Category 的本質(zhì):
Category 的本質(zhì)就是
_category_t 結(jié)構(gòu)體
類型瓣喊,其中包含了以下幾部分:
_method_list_t
類型的『對(duì)象方法列表結(jié)構(gòu)體』坡慌;_method_list_t
類型的『類方法列表結(jié)構(gòu)體』;_protocol_list_t
類型的『協(xié)議列表結(jié)構(gòu)體』藻三;_prop_list_t
類型的『屬性列表結(jié)構(gòu)體』洪橘。
_category_t 結(jié)構(gòu)體
中不包含_ivar_list_t
類型,也就是不包含『成員變量結(jié)構(gòu)體』趴酣。
3. Category 的加載過(guò)程
3.1 dyld 加載大致流程
之前我們談到過(guò) Category(分類)是在運(yùn)行時(shí)階段動(dòng)態(tài)加載的梨树。而 Runtime(運(yùn)行時(shí)) 加載的過(guò)程,離不開(kāi)一個(gè)叫做 dyld 的動(dòng)態(tài)鏈接器岖寞。
在 MacOS 和 iOS 上抡四,動(dòng)態(tài)鏈接加載器 dyld 用來(lái)加載所有的庫(kù)和可執(zhí)行文件。而加載Runtime(運(yùn)行時(shí)) 的過(guò)程仗谆,就是在 dyld 加載的時(shí)候發(fā)生的指巡。
dyld 的相關(guān)代碼可在蘋(píng)果開(kāi)源網(wǎng)站上進(jìn)行下載。 鏈接地址:dyld 蘋(píng)果開(kāi)源代碼
dyld 加載的流程大致是這樣:
- 配置環(huán)境變量隶垮;
- 加載共享緩存藻雪;
- 初始化主 APP;
- 插入動(dòng)態(tài)緩存庫(kù)狸吞;
- 鏈接主程序勉耀;
- 鏈接插入的動(dòng)態(tài)庫(kù);
- 初始化主程序:OC, C++ 全局變量初始化蹋偏;
- 返回主程序入口函數(shù)便斥。
本文中,我們只需要關(guān)心的是第 7 步威始,因?yàn)?Runtime(運(yùn)行時(shí)) 是在這一步初始化的枢纠。加載 Category(分類)自然也是在這個(gè)過(guò)程中。
初始化主程序中黎棠,Runtime 初始化的調(diào)用棧如下:
dyldbootstrap::start
--->dyld::_main
--->initializeMainExecutable
--->runInitializers
--->recursiveInitialization
--->doInitialization
--->doModInitFunctions
--->_objc_init
最后調(diào)用的 _objc_init
是 libobjc
庫(kù)中的方法晋渺, 是 Runtime 的初始化過(guò)程,也是 Objective-C 的入口脓斩。
運(yùn)行時(shí)相關(guān)的代碼可在蘋(píng)果開(kāi)源網(wǎng)站上進(jìn)行下載木西。 鏈接地址: objc4 蘋(píng)果開(kāi)源代碼
在 _objc_init
這一步中:Runtime
向 dyld
綁定了回調(diào),當(dāng) image
加載到內(nèi)存后随静,dyld
會(huì)通知 Runtime
進(jìn)行處理户魏,Runtime
接手后調(diào)用 map_images
做解析和處理,調(diào)用 _read_images
方法把 Category(分類)
的對(duì)象方法、協(xié)議叼丑、屬性添加到類上关翎,把 Category(分類)
的類方法、協(xié)議添加到類的 metaclass
上鸠信;接下來(lái) load_images
中調(diào)用 call_load_methods
方法纵寝,遍歷所有加載進(jìn)來(lái)的 Class
,按繼承層級(jí)和編譯順序依次調(diào)用 Class
的 load
方法和其 Category
的 load
方法星立。
加載Category(分類)的調(diào)用棧如下:
_objc_init
--->map_images
--->map_images_nolock
--->_read_images(加載分類)
--->load_images
爽茴。
既然我們知道了 Category(分類)的加載發(fā)生在 _read_images
方法中,那么我們只需要關(guān)注_read_images
方法中關(guān)于分類加載的代碼即可绰垂。
3.2 Category(分類) 加載過(guò)程
3.2.1 _read_images
方法
忽略 _read_images
方法中其他與本文無(wú)關(guān)的代碼室奏,得到如下代碼:
// 獲取鏡像中的分類數(shù)組
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 遍歷分類數(shù)組
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 處理這個(gè)分類
// 首先,使用目標(biāo)類注冊(cè)當(dāng)前分類
// 然后劲装,如果實(shí)現(xiàn)了這個(gè)類胧沫,重建類的方法列表
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());
}
}
}
主要用到了兩個(gè)方法:
-
addUnattachedCategoryForClass(cat, cls, hi);
為類添加未依附的分類 -
remethodizeClass(cls);
重建類的方法列表
通過(guò)這兩個(gè)方法達(dá)到了兩個(gè)目的:
- 把
Category(分類)
的對(duì)象方法、協(xié)議占业、屬性添加到類上绒怨; - 把
Category(分類)
的類方法、協(xié)議添加到類的metaclass
上谦疾。
下面來(lái)說(shuō)說(shuō)上邊提到的這兩個(gè)方法南蹂。
3.2.2 addUnattachedCategoryForClass(cat, cls, hi);
方法
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertLocked();
// 取得存儲(chǔ)所有未依附分類的列表:cats
NXMapTable *cats = unattachedCategories();
category_list *list;
// 從 cats 列表中找到 cls 對(duì)應(yīng)的未依附分類的列表: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 中,會(huì)覆蓋舊的 list
NXMapInsert(cats, cls, list);
}
addUnattachedCategoryForClass(cat, cls, hi);
的執(zhí)行過(guò)程可以參考代碼注釋念恍。執(zhí)行完這個(gè)方法之后六剥,系統(tǒng)會(huì)將當(dāng)前分類 cat
放到該類 cls
對(duì)應(yīng)的未依附分類的列表 list
中。這句話有點(diǎn)拗口峰伙,簡(jiǎn)而言之疗疟,就是:把類和分類做了一個(gè)關(guān)聯(lián)映射。
實(shí)際上真正起到添加加載作用的是下邊的 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);
方法主要就做了一件事:調(diào)用 attachCategories(cls, cats, true);
方法將未依附分類的列表 cats 附加到 cls 類上。所以权均,我們就再來(lái)看看 attachCategories(cls, cats, true);
方法顿膨。
3.2.4 attachCategories(cls, cats, true);
方法
我發(fā)誓這是本文中加載 Category(分類)過(guò)程的最后一段代碼。不過(guò)也是最為核心的一段代碼叽赊。
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 創(chuàng)建方法列表恋沃、屬性列表、協(xié)議列表必指,用來(lái)存儲(chǔ)分類的方法囊咏、屬性、協(xié)議
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; // 記錄方法的數(shù)量
int propcount = 0; // 記錄屬性的數(shù)量
int protocount = 0; // 記錄協(xié)議的數(shù)量
int i = cats->count; // 從分類數(shù)組最后開(kāi)始遍歷,保證先取的是最新的分類
bool fromBundle = NO; // 記錄是否是從 bundle 中取的
while (i--) { // 從后往前依次遍歷
auto& entry = cats->list[i]; // 取出當(dāng)前分類
// 取出分類中的方法列表梅割。如果是元類霜第,取得的是類方法列表;否則取得的是對(duì)象方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; // 將方法列表放入 mlists 方法列表數(shù)組中
fromBundle |= entry.hi->isBundle(); // 分類的頭部信息中存儲(chǔ)了是否是 bundle户辞,將其記住
}
// 取出分類中的屬性列表泌类,如果是元類,取得的是 nil
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 取出分類中遵循的協(xié)議列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 取出當(dāng)前類 cls 的 class_rw_t 數(shù)據(jù)
auto rw = cls->data();
// 存儲(chǔ)方法底燎、屬性刃榨、協(xié)議數(shù)組到 rw 中
// 準(zhǔn)備方法列表 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);
// 將新協(xié)議列表添加到 rw 中的協(xié)議列表中
rw->protocols.attachLists(protolists, protocount);
// 釋放協(xié)議列表
free(protolists);
}
從 attachCategories(cls, cats, true);
方法的注釋中可以看出這個(gè)方法就是存儲(chǔ)分類的方法、屬性双仍、協(xié)議的核心代碼枢希。
但是需要注意一些細(xì)節(jié)問(wèn)題:
- Category(分類)的方法、屬性朱沃、協(xié)議只是添加到原有類上苞轿,并沒(méi)有將原有類的方法、屬性为流、協(xié)議進(jìn)行完全替換呕屎。
舉個(gè)例子說(shuō)明就是:假設(shè)原有類擁有MethodA
方法,分類也擁有MethodA
方法敬察,那么加載完分類之后秀睛,類的方法列表中會(huì)擁有兩個(gè)MethodA
方法。- Category(分類)的方法莲祸、屬性蹂安、協(xié)議會(huì)被添加到原有類的方法列表、屬性列表锐帜、協(xié)議列表的最前面田盈,而原有類的方法、屬性缴阎、協(xié)議則被移動(dòng)到了列表后面允瞧。
因?yàn)樵谶\(yùn)行時(shí)查找方法的時(shí)候是順著方法列表的順序依次查找的,所以 Category(分類)的方法會(huì)先被搜索到蛮拔,然后直接執(zhí)行述暂,而原有類的方法則不被執(zhí)行。這也是 Category(分類)中的方法會(huì)覆蓋掉原有類的方法的最直接原因建炫。
4. Category(分類)和 Class(類)的 +load 方法
Category(分類)中的的方法畦韭、屬性、協(xié)議附加到類上的操作肛跌,是在 + load
方法執(zhí)行之前進(jìn)行的艺配。也就是說(shuō)察郁,在 + load
方法執(zhí)行之前,類中就已經(jīng)加載了 Category(分類)中的的方法转唉、屬性皮钠、協(xié)議。
而 Category(分類)和 Class(類)的 + load
方法的調(diào)用順序規(guī)則如下所示:
- 先調(diào)用主類酝掩,按照編譯順序鳞芙,順序地根據(jù)繼承關(guān)系由父類向子類調(diào)用;
- 調(diào)用完主類期虾,再調(diào)用分類原朝,按照編譯順序,依次調(diào)用镶苞;???
-
+ load
方法除非主動(dòng)調(diào)用喳坠,否則只會(huì)調(diào)用一次。
通過(guò)這樣的調(diào)用規(guī)則茂蚓,我們可以知道:主類的 + load
方法調(diào)用一定在分類 + load
方法調(diào)用之前壕鹉。但是分類 + load
方法調(diào)用順序并不不是按照繼承關(guān)系調(diào)用的,而是依照編譯順序確定的聋涨,這也導(dǎo)致了 + load
方法的調(diào)用順序并不一定確定晾浴。一個(gè)順序可能是:父類 -> 子類 -> 父類類別 -> 子類類別
,也可能是 父類 -> 子類 -> 子類類別 -> 父類類別
牍白。
5. Category 與關(guān)聯(lián)對(duì)象
之前我們提到過(guò)脊凰,在 Category 中雖然可以添加屬性,但是不會(huì)生成對(duì)應(yīng)的成員變量茂腥,也不能生成 getter
狸涌、setter
方法。因此最岗,在調(diào)用 Category 中聲明的屬性時(shí)會(huì)報(bào)錯(cuò)帕胆。
那么就沒(méi)有辦法使用 Category 中的屬性了嗎?
答案當(dāng)然是否定的般渡。
我們可以自己來(lái)實(shí)現(xiàn) getter
懒豹、setter
方法,并借助關(guān)聯(lián)對(duì)象(Objective-C Associated Objects)來(lái)實(shí)現(xiàn) getter
驯用、setter
方法脸秽。關(guān)聯(lián)對(duì)象能夠幫助我們?cè)谶\(yùn)行時(shí)階段將任意的屬性關(guān)聯(lián)到一個(gè)對(duì)象上。具體需要用到以下幾個(gè)方法:
// 1. 通過(guò) key : value 的形式給對(duì)象 object 設(shè)置關(guān)聯(lián)屬性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 2. 通過(guò) key 獲取關(guān)聯(lián)的屬性 object
id objc_getAssociatedObject(id object, const void *key);
// 3. 移除對(duì)象所關(guān)聯(lián)的屬性
void objc_removeAssociatedObjects(id object);
下面講解一個(gè)示例晨汹。
5.1 UIImage 分類中增加網(wǎng)絡(luò)地址屬性
/********************* UIImage+Property.h 文件 *********************/
#import <UIKit/UIKit.h>
@interface UIImage (Property)
/* 圖片網(wǎng)絡(luò)地址 */
@property (nonatomic, copy) NSString *urlString;
// 用于清除關(guān)聯(lián)對(duì)象
- (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));
}
// 清除關(guān)聯(lián)對(duì)象
- (void)clearAssociatedObjcet {
objc_removeAssociatedObjects(self);
}
@end
測(cè)試代碼:
UIImage *image = [[UIImage alloc] init];
image.urlString = @"http://www.image.png";
NSLog(@"image urlString = %@",image.urlString);
[image clearAssociatedObjcet];
NSLog(@"image urlString = %@",image.urlString);
打印結(jié)果:
2019-07-24 18:36:31.051789+0800 YSC-Category[74564:17944298] image urlString = http://www.image.png
2019-07-24 18:36:31.051926+0800 YSC-Category[74564:17944298] image urlString = (null)
可以看到:借助關(guān)聯(lián)對(duì)象豹储,我們成功的在 UIImage 分類中為 UImage 類增加了 urlString 關(guān)聯(lián)屬性贷盲,并實(shí)現(xiàn)了 getter
淘这、setter
方法剥扣。
注意:使用
objc_removeAssociatedObjects
可以斷開(kāi)所有的關(guān)聯(lián)。通常情況下不建議使用铝穷,因?yàn)樗鼤?huì)斷開(kāi)所有的關(guān)聯(lián)钠怯。如果想要斷開(kāi)關(guān)聯(lián)可以使用objc_setAssociatedObject
,將關(guān)聯(lián)對(duì)象傳入 nil 即可曙聂。
參考資料
- 美團(tuán)技術(shù)團(tuán)隊(duì):深入理解Objective-C:Category
- CJS_:iOS分類底層實(shí)現(xiàn)原理小記
- 梧雨北辰:Runtime-iOS運(yùn)行時(shí)應(yīng)用篇
- objc4 蘋(píng)果開(kāi)源代碼 | 文中參考:objc4-750 版本
- dyld 蘋(píng)果開(kāi)源代碼 | 文中參考:dyld-635.2 版本
最后
最后說(shuō)一句晦炊,其實(shí)一開(kāi)始只想隨便寫(xiě)寫(xiě)關(guān)于 Category 與關(guān)聯(lián)對(duì)象。結(jié)果不小心觸碰到了 Category 的底層知識(shí)宁脊。断国。。然后就不小心寫(xiě)多了榆苞。心累稳衬。。坐漏。
文中如若有誤薄疚,煩請(qǐng)指正,感謝赊琳。
iOS 開(kāi)發(fā):『Runtime』詳解 系列文章:
- iOS 開(kāi)發(fā):『Runtime』詳解(一)基礎(chǔ)知識(shí)
- iOS 開(kāi)發(fā):『Runtime』詳解(二)Method Swizzling
- iOS 開(kāi)發(fā):『Runtime』詳解(三)Category 底層原理
- iOS 開(kāi)發(fā):『Runtime』詳解(四)獲取類詳細(xì)屬性街夭、方法
尚未完成:
- iOS 開(kāi)發(fā):『Runtime』詳解(五)Crash 防護(hù)系統(tǒng)
- iOS 開(kāi)發(fā):『Runtime』詳解(六)Objective-C 2.0 結(jié)構(gòu)解析
- iOS 開(kāi)發(fā):『Runtime』詳解(七)KVO 底層實(shí)現(xiàn)
- 本文作者: 行走少年郎
- 本文鏈接: http://www.reibang.com/p/b08bbe3613ab
- 版權(quán)聲明: 本文章采用 CC BY-NC-SA 3.0 許可協(xié)議。轉(zhuǎn)載請(qǐng)?jiān)谖淖珠_(kāi)頭注明『本文作者』和『本文鏈接』躏筏!