iOS - Category底层原理

在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_tmethods列表中(因为类方法本质是元类的实例方法)。

  • 合并规则
    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_tproperties列表中,但不会自动生成实例变量(ivar)和setter/getter实现

  • 若需让属性生效,需手动实现setter/getter(或通过@dynamic延迟实现),或通过关联对象(Associated Objects) 模拟实例变量存储(如objc_setAssociatedObject)。

(3)协议合并

Category实现的协议会被合并到原类的class_rw_tprotocols列表中,效果等同于原类直接实现了这些协议(可通过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的底层原理可概括为:

  1. 编译期:被打包为category_t结构体,存储方法、属性、协议等信息;
  2. 运行时:Runtime通过attachCategories函数将其内容合并到原类(实例方法到类,类方法到元类),且Category的方法优先级高于原类同名方法;
  3. 限制:因类的内存布局编译期固定,无法添加实例变量,需通过关联对象模拟。

这一机制使得Objective-C具备了灵活的动态扩展能力,也是iOS开发中“无侵入式扩展”的核心实现方式。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容