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

本文用來(lái)介紹 iOS 開(kāi)發(fā)中『Runtime』中的 Category 底層原理跑芳。通過(guò)本文,您將了解到:

  1. Category (分類)簡(jiǎn)介
  2. Category 的實(shí)質(zhì)
  3. Category 的加載過(guò)程
  4. Category(分類)和 Class(類)的 +load 方法
  5. 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++ 源碼方法如下:

  1. 在項(xiàng)目中添加 Person 類文件 Person.h 和 Person.m峭判,Person 類繼承自 NSObject 开缎。
  2. 在項(xiàng)目中添加 Person 類的 Category 文件 Person+Addition.h 和 Person+Addition.m,并在 Category 中添加的相關(guān)對(duì)象方法林螃,類方法奕删,屬性,以及代理疗认。
  3. 打開(kāi)『終端』急侥,執(zhí)行 cd XXX/XXX 命令,其中 XXX/XXX 為 Category 文件 所在的目錄侮邀。
  4. 繼續(xù)在終端執(zhí)行 clang -rewrite-objc Person+Addition.m
  5. 執(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)體』源碼中我們可以看到:

  1. Categor 結(jié)構(gòu)體衙传。
  2. Category 結(jié)構(gòu)體的賦值語(yǔ)句决帖。
  3. 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)體』源碼中我們可以看到:

  1. - (void)printName; 對(duì)象方法的實(shí)現(xiàn)并闲。
  2. - (void)personProtocolMethod; 方法的實(shí)現(xiàn)绎速。
  3. 對(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)體』源碼中我們可以看到:

  1. + (void)printClassName; 類方法的實(shí)現(xiàn)雁歌。
  2. + (void)personProtocolClassMethod; 類方法的實(shí)現(xiàn)宏浩。
  3. 類方法列表結(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)體』源碼中我們可以看到:

  1. 協(xié)議列表結(jié)構(gòu)體。
  2. 協(xié)議列表 對(duì)象方法列表結(jié)構(gòu)體父能。
  3. 協(xié)議列表 類方法列表結(jié)構(gòu)體神凑。
  4. 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)體 類型瓣喊,其中包含了以下幾部分:

  1. _method_list_t 類型的『對(duì)象方法列表結(jié)構(gòu)體』坡慌;
  2. _method_list_t 類型的『類方法列表結(jié)構(gòu)體』;
  3. _protocol_list_t 類型的『協(xié)議列表結(jié)構(gòu)體』藻三;
  4. _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 加載的流程大致是這樣:

  1. 配置環(huán)境變量隶垮;
  2. 加載共享緩存藻雪;
  3. 初始化主 APP;
  4. 插入動(dòng)態(tài)緩存庫(kù)狸吞;
  5. 鏈接主程序勉耀;
  6. 鏈接插入的動(dòng)態(tài)庫(kù);
  7. 初始化主程序:OC, C++ 全局變量初始化蹋偏;
  8. 返回主程序入口函數(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_initlibobjc 庫(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 這一步中:Runtimedyld 綁定了回調(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)用 Classload 方法和其 Categoryload 方法星立。

加載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è)目的:

  1. Category(分類) 的對(duì)象方法、協(xié)議占业、屬性添加到類上绒怨;
  2. 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ī)則如下所示:

  1. 先調(diào)用主類酝掩,按照編譯順序鳞芙,順序地根據(jù)繼承關(guān)系由父類向子類調(diào)用;
  2. 調(diào)用完主類期虾,再調(diào)用分類原朝,按照編譯順序,依次調(diào)用镶苞;???
  3. + 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 即可曙聂。


參考資料


最后

最后說(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』詳解(五)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)頭注明『本文作者』和『本文鏈接』躏筏!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末板丽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子寸士,更是在濱河造成了極大的恐慌檐什,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弱卡,死亡現(xiàn)場(chǎng)離奇詭異乃正,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)婶博,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)瓮具,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人凡人,你說(shuō)我怎么就攤上這事名党。” “怎么了挠轴?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵传睹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我岸晦,道長(zhǎng)欧啤,這世上最難降的妖魔是什么睛藻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮邢隧,結(jié)果婚禮上店印,老公的妹妹穿的比我還像新娘。我一直安慰自己倒慧,他們只是感情好按摘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著纫谅,像睡著了一般炫贤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上付秕,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天照激,我揣著相機(jī)與錄音,去河邊找鬼盹牧。 笑死俩垃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的汰寓。 我是一名探鬼主播口柳,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼有滑!你這毒婦竟也來(lái)了跃闹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤毛好,失蹤者是張志新(化名)和其女友劉穎望艺,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體肌访,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡找默,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吼驶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惩激。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蟹演,靈堂內(nèi)的尸體忽然破棺而出风钻,到底是詐尸還是另有隱情,我是刑警寧澤酒请,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布骡技,位于F島的核電站,受9級(jí)特大地震影響羞反,放射性物質(zhì)發(fā)生泄漏布朦。R本人自食惡果不足惜毛萌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望喝滞。 院中可真熱鬧,春花似錦膏秫、人聲如沸右遭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)窘哈。三九已至,卻和暖如春亭敢,著一層夾襖步出監(jiān)牢的瞬間滚婉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工帅刀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留让腹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓扣溺,卻偏偏與公主長(zhǎng)得像骇窍,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子锥余,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345