在Objective-C中,Category(分類)是一種為已有類動(dòng)態(tài)添加方法、屬性、協(xié)議的機(jī)制,無(wú)需修改原類的源碼,也無(wú)需創(chuàng)建子類。其底層實(shí)現(xiàn)依賴于編譯期的結(jié)構(gòu)體定義和運(yùn)行時(shí)(Runtime)的動(dòng)態(tài)合并邏輯,核心原理可從編譯期結(jié)構(gòu)和運(yùn)行時(shí)加載兩個(gè)階段解析。
一、編譯期:Category的底層結(jié)構(gòu)
編譯時(shí),每個(gè)Category會(huì)被編譯器轉(zhuǎn)換為一個(gè)名為category_t的結(jié)構(gòu)體(定義在Objective-C Runtime源碼的objc-runtime-new.h中),存儲(chǔ)分類的核心信息。簡(jiǎn)化后的結(jié)構(gòu)如下:
struct category_t {
const char *name; // 所屬類的類名(如"UIViewController")
classref_t cls; // 所屬類的指針(編譯時(shí)為NULL,運(yùn)行時(shí)綁定)
struct method_list_t *instanceMethods; // 實(shí)例方法列表(分類中定義的-方法)
struct method_list_t *classMethods; // 類方法列表(分類中定義的+方法)
struct protocol_list_t *protocols; // 分類實(shí)現(xiàn)的協(xié)議列表
struct property_list_t *instanceProperties; // 分類中聲明的屬性列表
// ... 其他字段(如版本信息)
};
-
關(guān)鍵字段說(shuō)明:
-
name:標(biāo)識(shí)該分類屬于哪個(gè)類(如為UIViewController添加分類,name就是"UIViewController")。 -
instanceMethods/classMethods:分別存儲(chǔ)分類中定義的實(shí)例方法和類方法(方法列表是method_t結(jié)構(gòu)體的數(shù)組,每個(gè)method_t包含方法名、實(shí)現(xiàn)、類型編碼等)。 -
instanceProperties:存儲(chǔ)分類中聲明的屬性(僅包含屬性名、類型等元信息,不包含實(shí)例變量,這也是分類不能直接添加實(shí)例變量的原因)。 -
protocols:存儲(chǔ)分類實(shí)現(xiàn)的協(xié)議,用于運(yùn)行時(shí)將協(xié)議添加到原類中。
-
二、運(yùn)行時(shí):Category與原類的合并
Category的核心功能(為原類添加方法/屬性/協(xié)議)并非在編譯期完成,而是在程序啟動(dòng)時(shí)(runtime初始化階段) 通過(guò)Runtime的邏輯動(dòng)態(tài)合并到原類中。具體流程如下:
1. 加載所有Category
程序啟動(dòng)時(shí),Runtime會(huì)加載可執(zhí)行文件(Mach-O)和所有依賴的動(dòng)態(tài)庫(kù),通過(guò)_read_images函數(shù)(負(fù)責(zé)加載類和分類的核心函數(shù))掃描其中的__DATA段,找到所有category_t結(jié)構(gòu)體(編譯時(shí)會(huì)被存放在這里)。
2. 綁定Category到原類
對(duì)于每個(gè)category_t,Runtime會(huì)根據(jù)其name字段找到對(duì)應(yīng)的原類(cls字段此時(shí)被賦值為原類的指針)。例如,name為"UIViewController"的分類,會(huì)綁定到UIViewController類。
3. 合并方法/屬性/協(xié)議到原類
Runtime通過(guò)attachCategories函數(shù),將Category中的方法、屬性、協(xié)議合并到原類的對(duì)應(yīng)列表中,核心邏輯如下:
(1)方法合并(最核心)
實(shí)例方法:合并到原類的
class_rw_t結(jié)構(gòu)體的methods列表中(class_rw_t是類的“可讀寫數(shù)據(jù)”,包含方法、屬性、協(xié)議等動(dòng)態(tài)信息)。類方法:合并到原類的元類(meta-class) 的
class_rw_t的methods列表中(因?yàn)轭惙椒ū举|(zhì)是元類的實(shí)例方法)。-
合并規(guī)則:
Category的方法會(huì)被插入到原類方法列表的前面(而非替換)。這意味著:- 若Category與原類有同名方法,調(diào)用時(shí)會(huì)優(yōu)先執(zhí)行Category的方法(因?yàn)椴檎曳椒〞r(shí)從列表頭部開始,找到第一個(gè)匹配的就返回);
- 原類的方法并未被刪除,只是被“隱藏”(可通過(guò)Runtime的
class_getInstanceMethod強(qiáng)制獲取原類方法)。
示例:原類
A有方法-foo,分類A+Test也有-foo,合并后A的方法列表為[A+Test的-foo, A的-foo],調(diào)用[A new] foo時(shí)執(zhí)行分類的實(shí)現(xiàn)。
(2)屬性合并
Category中聲明的屬性(如@property (nonatomic, copy) NSString *test;)會(huì)被合并到原類的class_rw_t的properties列表中,但不會(huì)自動(dòng)生成實(shí)例變量(ivar)和setter/getter實(shí)現(xiàn)。
- 若需讓屬性生效,需手動(dòng)實(shí)現(xiàn)setter/getter(或通過(guò)
@dynamic延遲實(shí)現(xiàn)),或通過(guò)關(guān)聯(lián)對(duì)象(Associated Objects) 模擬實(shí)例變量存儲(chǔ)(如objc_setAssociatedObject)。
(3)協(xié)議合并
Category實(shí)現(xiàn)的協(xié)議會(huì)被合并到原類的class_rw_t的protocols列表中,效果等同于原類直接實(shí)現(xiàn)了這些協(xié)議(可通過(guò)class_conformsToProtocol檢測(cè))。
4. 多個(gè)Category的優(yōu)先級(jí)
若多個(gè)Category為同一類添加了同名方法,最終調(diào)用哪個(gè)取決于編譯順序:
- Xcode中,編譯順序在
Target > Build Phases > Compile Sources中定義,最后編譯的Category的方法會(huì)被插入到方法列表的最前面,因此會(huì)被優(yōu)先調(diào)用。
三、為什么Category不能添加實(shí)例變量?
實(shí)例變量(ivar)的內(nèi)存布局在編譯期就已確定(存儲(chǔ)在類的class_ro_t結(jié)構(gòu)體的ivars中,class_ro_t是類的“只讀數(shù)據(jù)”,編譯后不可修改)。
而Category是在運(yùn)行時(shí)才被加載并合并到原類中,此時(shí)原類的內(nèi)存布局已固定,無(wú)法動(dòng)態(tài)擴(kuò)展ivars的內(nèi)存空間,因此不能直接添加實(shí)例變量。
若需“模擬”添加變量,需通過(guò)Runtime的關(guān)聯(lián)對(duì)象(Associated Objects) 機(jī)制,將變量存儲(chǔ)在一個(gè)全局哈希表中,與原類實(shí)例綁定(本質(zhì)并非真正的實(shí)例變量)。
四、Category與Class Extension的區(qū)別
常有人混淆兩者,核心差異在于合并時(shí)機(jī)和功能范圍:
| 特性 | Category(分類) | Class Extension(類擴(kuò)展) |
|---|---|---|
| 合并時(shí)機(jī) | 運(yùn)行時(shí)(動(dòng)態(tài)合并) | 編譯期(與原類一起編譯) |
| 能否添加實(shí)例變量 | 不能(需用關(guān)聯(lián)對(duì)象模擬) | 能(直接聲明@private等變量) |
| 能否添加方法 | 能(實(shí)例方法/類方法) | 能(通常用于擴(kuò)展私有方法) |
| 能否聲明屬性 | 能(但需手動(dòng)實(shí)現(xiàn)setter/getter) | 能(編譯器自動(dòng)生成setter/getter) |
| 文件名規(guī)范 | 原類名+分類名(如A+Test.h) |
通常在原類.m中定義(無(wú)單獨(dú)文件) |
總結(jié)
Category的底層原理可概括為:
- 編譯期:被打包為
category_t結(jié)構(gòu)體,存儲(chǔ)方法、屬性、協(xié)議等信息; - 運(yùn)行時(shí):Runtime通過(guò)
attachCategories函數(shù)將其內(nèi)容合并到原類(實(shí)例方法到類,類方法到元類),且Category的方法優(yōu)先級(jí)高于原類同名方法; - 限制:因類的內(nèi)存布局編譯期固定,無(wú)法添加實(shí)例變量,需通過(guò)關(guān)聯(lián)對(duì)象模擬。
這一機(jī)制使得Objective-C具備了靈活的動(dòng)態(tài)擴(kuò)展能力,也是iOS開發(fā)中“無(wú)侵入式擴(kuò)展”的核心實(shí)現(xiàn)方式。