第三部分,Category。

Category 主要作用是为已存在的类添加方法。

Apple 推荐 2 个使用场景:

  • 可以把类的实现分开在几个不同的文件里

    • 可以减少单个文件的体积
    • 可以把不同的功能组织到不同的 Category 里
    • 可以由多个开发者共同完成一个类
    • 可以按需加载想要的 Category
  • 声明私有方法

其他使用场景:

  • 模拟多继承
  • 把 Framework 的私有方法公开

Category 真面目

Runtime 中 Category 用结构体 category_t 表示

1
2
3
4
5
6
7
8
typedef 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;
} category_t;
  1. name:不是 Category 小括号里的名字,而是类的名字
  2. cls:要扩展的类对象,编译期这个值是不会有的,运行时加载时才会根据 name 对应到类对象
  3. instance_methods:这个 Category 中所有的 - 方法
  4. class_methods:这个 Category 中所有的 + 方法
  5. protocols:这个 Category 实现的协议,比较不常用在 Category 里面实现协议,但是确实支持
  6. properties:这个 Category 中所有的属性,这也是 Category 里面可以定义属性的原因,不过这个 property 不会 @synthesize 实例变量,一般有需求添加实例变量属性时,会采用 objc_setAssociatedObjectobjc_getAssociatedObject 方法绑定,不过这种方法生成的与一个普通的实例变量完成是两回事儿。

编译器对 Category 做了什么?

  1. 编译器生成了方法列表
  2. 编译器生成了 Category 本身
  3. 编译器保存了一个 category_t 数组,用于运行期 Category 的加载

Runtime,Category 如何加载?

Category 动态扩展了原来类的方法,在调用者看来好像原来类本来就有这些方法似的,有 2 个事实:

  1. 无论有没有 import Category.h,都可以成功调用 Category 的方法,都不影响 Category 的加载流程,import 只是帮助了编译检查和链接过程。
  2. Runtime 加载完成后,Category 的原始信息在类结构里将不会存在

Runtime,Category 加载过程:

  1. 把 Category 的实例方法、协议以及属性添加到类上
  2. 把 Category 的类方法和协议添加到类的 MetaClass 上

注意:

  1. Category 的方法没有”完成替换掉“原来类已有的方法,也就是说如果 Category 和原来的类都有 method,那么 Category 附加完成后,类的方法列表里会有 2 个 methodA
  2. Category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这就是我们平常说的 Category 的方法会”覆盖“掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表顺序查找的,它只要一找到对应名字的方法,就会停止查找,殊不知后面可能还有一样名字的方法。

Category 和 +load 方法

在类的 +load 方法调用的时候,我们可以调用 Category 中声明的方法么?

可以调用,因为附加 Category 到类的工作会先于 +load 方法的执行。

这么些个 +load 方法,调用顺序是咋样的呢?

+load的执行顺序是先类,后 Category,而 Category 的 +load 执行顺序是根据编译顺序决定的。

Reference