10-2.【OC】【extension】Extension 与 Category 的底层区别是什么?

27 阅读3分钟

从底层视角看,ExtensionCategory 的区别不仅仅是“能不能加成员变量”,而是编译时静态合并运行时动态注入的本质差异。


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. 封装与可见性

特性ExtensionCategory
源码依赖必须有原类源码(通常写在 .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 是在**“房子盖好后”**(运行时)在墙上挂一个挂钩,它不影响房子的结构,但能增加功能。