在Objective-C中,Category(分类)是一种为已有类动态添加方法、属性、协议的机制,无需修改原类的源码,也无需创建子类。其底层实现依赖于编译期的结构体定义和运行时(Runtime)的动态合并逻辑,核心原理可从编译期结构和运行时加载两个阶段解析。
一、编译期:Category的底层结构
编译时,每个Category会被编译器转换为一个名为category_t的结构体(定义在Objective-C Runtime源码的objc-runtime-new.h中),存储分类的核心信息。简化后的结构如下:
struct category_t {
const char *name; // 所属类的类名(如"UIViewController")
classref_t cls; // 所属类的指针(编译时为NULL,运行时绑定)
struct method_list_t *instanceMethods; // 实例方法列表(分类中定义的-方法)
struct method_list_t *classMethods; // 类方法列表(分类中定义的+方法)
struct protocol_list_t *protocols; // 分类实现的协议列表
struct property_list_t *instanceProperties; // 分类中声明的属性列表
// ... 其他字段(如版本信息)
};
-
关键字段说明:
-
name:标识该分类属于哪个类(如为UIViewController添加分类,name就是"UIViewController")。 -
instanceMethods/classMethods:分别存储分类中定义的实例方法和类方法(方法列表是method_t结构体的数组,每个method_t包含方法名、实现、类型编码等)。 -
instanceProperties:存储分类中声明的属性(仅包含属性名、类型等元信息,不包含实例变量,这也是分类不能直接添加实例变量的原因)。 -
protocols:存储分类实现的协议,用于运行时将协议添加到原类中。
-
二、运行时:Category与原类的合并
Category的核心功能(为原类添加方法/属性/协议)并非在编译期完成,而是在程序启动时(runtime初始化阶段) 通过Runtime的逻辑动态合并到原类中。具体流程如下:
1. 加载所有Category
程序启动时,Runtime会加载可执行文件(Mach-O)和所有依赖的动态库,通过_read_images函数(负责加载类和分类的核心函数)扫描其中的__DATA段,找到所有category_t结构体(编译时会被存放在这里)。
2. 绑定Category到原类
对于每个category_t,Runtime会根据其name字段找到对应的原类(cls字段此时被赋值为原类的指针)。例如,name为"UIViewController"的分类,会绑定到UIViewController类。
3. 合并方法/属性/协议到原类
Runtime通过attachCategories函数,将Category中的方法、属性、协议合并到原类的对应列表中,核心逻辑如下:
(1)方法合并(最核心)
实例方法:合并到原类的
class_rw_t结构体的methods列表中(class_rw_t是类的“可读写数据”,包含方法、属性、协议等动态信息)。类方法:合并到原类的元类(meta-class) 的
class_rw_t的methods列表中(因为类方法本质是元类的实例方法)。-
合并规则:
Category的方法会被插入到原类方法列表的前面(而非替换)。这意味着:- 若Category与原类有同名方法,调用时会优先执行Category的方法(因为查找方法时从列表头部开始,找到第一个匹配的就返回);
- 原类的方法并未被删除,只是被“隐藏”(可通过Runtime的
class_getInstanceMethod强制获取原类方法)。
示例:原类
A有方法-foo,分类A+Test也有-foo,合并后A的方法列表为[A+Test的-foo, A的-foo],调用[A new] foo时执行分类的实现。
(2)属性合并
Category中声明的属性(如@property (nonatomic, copy) NSString *test;)会被合并到原类的class_rw_t的properties列表中,但不会自动生成实例变量(ivar)和setter/getter实现。
- 若需让属性生效,需手动实现setter/getter(或通过
@dynamic延迟实现),或通过关联对象(Associated Objects) 模拟实例变量存储(如objc_setAssociatedObject)。
(3)协议合并
Category实现的协议会被合并到原类的class_rw_t的protocols列表中,效果等同于原类直接实现了这些协议(可通过class_conformsToProtocol检测)。
4. 多个Category的优先级
若多个Category为同一类添加了同名方法,最终调用哪个取决于编译顺序:
- Xcode中,编译顺序在
Target > Build Phases > Compile Sources中定义,最后编译的Category的方法会被插入到方法列表的最前面,因此会被优先调用。
三、为什么Category不能添加实例变量?
实例变量(ivar)的内存布局在编译期就已确定(存储在类的class_ro_t结构体的ivars中,class_ro_t是类的“只读数据”,编译后不可修改)。
而Category是在运行时才被加载并合并到原类中,此时原类的内存布局已固定,无法动态扩展ivars的内存空间,因此不能直接添加实例变量。
若需“模拟”添加变量,需通过Runtime的关联对象(Associated Objects) 机制,将变量存储在一个全局哈希表中,与原类实例绑定(本质并非真正的实例变量)。
四、Category与Class Extension的区别
常有人混淆两者,核心差异在于合并时机和功能范围:
| 特性 | Category(分类) | Class Extension(类扩展) |
|---|---|---|
| 合并时机 | 运行时(动态合并) | 编译期(与原类一起编译) |
| 能否添加实例变量 | 不能(需用关联对象模拟) | 能(直接声明@private等变量) |
| 能否添加方法 | 能(实例方法/类方法) | 能(通常用于扩展私有方法) |
| 能否声明属性 | 能(但需手动实现setter/getter) | 能(编译器自动生成setter/getter) |
| 文件名规范 | 原类名+分类名(如A+Test.h) |
通常在原类.m中定义(无单独文件) |
总结
Category的底层原理可概括为:
- 编译期:被打包为
category_t结构体,存储方法、属性、协议等信息; - 运行时:Runtime通过
attachCategories函数将其内容合并到原类(实例方法到类,类方法到元类),且Category的方法优先级高于原类同名方法; - 限制:因类的内存布局编译期固定,无法添加实例变量,需通过关联对象模拟。
这一机制使得Objective-C具备了灵活的动态扩展能力,也是iOS开发中“无侵入式扩展”的核心实现方式。