从底层视角看,Extension 和 Category 的区别不仅仅是“能不能加成员变量”,而是编译时静态合并与运行时动态注入的本质差异。
1. 核心区别:合并时机
Extension:编译期的“内生增长”
Extension 是类的一部分。在编译器编译 .m 文件时,Extension 中声明的属性、成员变量和方法会被直接编译进类的 class_ro_t(read-only data)结构体中。
- 内存布局确定:因为在编译期完成,编译器可以精确计算出类实例需要的内存大小(Instance Size)。这就是为什么 Extension 可以添加成员变量。
Category:运行时的“外部寄生”
Category 的数据在编译后被存放在一个单独的 category_t 结构体中。直到 App 启动、Runtime 加载镜像时,才通过 attachCategories 函数动态地合并到类的 class_rw_t(read-write data)中。
- 内存布局已封死:此时类的实例内存布局已经由
class_ro_t决定并固定了,运行时无法再往里面插数据。
2. 成员变量 (ivar) 的底层差异
这是两者最显著的区别,原因在于 ABI(应用二进制接口)的稳定性:
- Extension 可以添加 ivar:因为它参与了类的原始定义。编译器在生成
class_ro_t时,会将 Extension 里的 ivar 偏移量(Offset)计算进去。 - Category 不可以添加 ivar:如果允许 Category 添加 ivar,那么在运行时,为了给新变量腾出空间,必须移动该类及其所有子类在内存中的位置。这会导致所有已经编译好的二进制代码(记录了旧偏移量的代码)全部失效,引发灾难性的崩溃。
3. 方法实现的强制性
- Extension:在 Extension 中声明的方法,如果在该类的
@implementation中没有实现,编译器会直接报 Warning(警告)。它被视为类必须履行的承诺。 - Category:分类中声明的方法如果不实现,编译器通常不会报错。只有在运行时调用该方法时,才会因为找不到
IMP而导致unrecognized selector崩溃。
4. 封装与可见性
| 特性 | Extension | Category |
|---|---|---|
| 源码依赖 | 必须有原类源码(通常写在 .m 中)。 | 不需要源码(可以扩展 NSString 等)。 |
| 头文件暴露 | 隐藏在 .m 中,外界完全不可见。 | 可以在 .h 中公开,供全局使用。 |
| 主要功能 | 实现私有化、修改权限(readonly 变 readwrite)。 | 模块化、动态扩展公共功能。 |
5. 底层数据结构对比
在 objc-runtime 源码中,两者的承载方式完全不同:
Extension (属于类定义)
C++
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // 包含 Extension 中 ivar 的总大小
const ivar_list_t * ivars; // 包含 Extension 中的 ivar
const method_list_t * baseMethods; // 包含 Extension 中的方法
// ...
};
Category (独立结构体)
C++
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
总结
- Extension 是在**“造房子”**时(编译期)决定增加一间密室,它改变了地基和承重(内存布局)。
- Category 是在**“房子盖好后”**(运行时)在墙上挂一个挂钩,它不影响房子的结构,但能增加功能。