摘要
无论一个类设计的多么完美,在需求的演进当中,总会碰到一些无法预测的情况。那么怎么扩展已有的类?一般而言,有继承和组合两种选择。在 Objective-C 2.0 中,又提供了 category 这个语言特性,可以动态地为已有类添加新行为。本文将对 category 和 extension 进行介绍。
分类(category)
简介
category 是 Objective-C 2.0 之后添加的语言特性,category 的主要作用是为已经存在的类添加方法。apple 还推荐了category 的另外两个使用场景
- 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处:
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的 category 里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的 category 等等
- 声明私有方法
不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:
- 模拟多继承
- 把 framework 的私有方法公开
category 的结构
所有的 OC 类和对象,在 runtime 层都是用 struct 表示的,category 也不例外。在runtime层,category用结构体category_t
1 | typedef struct category_t { |
从 category 的定义也可以看出 category 的可以添加实例方法,类方法,实现协议,添加属性;但是无法添加实例变量。
category 的编译解析
自定义一个类,GGShop.h :
1 | #import <Foundation/Foundation.h> |
GGShop.m:
1 | #import "GGShop.h" |
使用 clang 的命令去看看 category 到底会变成什么:
1 | clang -rewrite-objc GGShop.m |
执行命令后,GGShop.m
被编译成一个 GGShop.cpp
文件,大约 3.5M,代码将近10万行,搜索在文件得最后位置找到分类的代码片段
1 | // 分类实现的实例方法列表,只是在 .h 中声明了但没有实现的方法不会出现在这里 |
可以看到,编译器生成了实例方法,类方法,属性,协议列表。它们的命名遵循
1 | // 公共前缀 + 类名 + 分类名 |
- category 的名字(例如Add)用来给各种列表以及后面的 category 结构体本身命名,而且有 static 来修饰,所以在同一个编译单元里我们的 category 名不能重复,否则会出现编译错误。
- 编译器生成了category 本身
_OBJC_$_CATEGORY_GGShop_$_Add
,并用前面生成的列表来初始化 category 本身。 - 最后,编译器在DATA段下的 objc_catlist section 里保存了一个大小为1的 category_t 的数组
L_OBJC_LABEL_CATEGORY_$
(如果有多个category,会生成对应长度的数组),用于运行期category的加载
category 的加载
Objective-C 的运行是依赖 OC 的runtime
的,而 OC 的runtime
和其他系统库一样,是 OS X 和 iOS 通过dyld
动态加载的。
对于 OC 运行时,入口方法如下(在objc-os.mm文件中):
1 | void _objc_init(void) |
下面来看 remethodizeClass
的具体实现
1 | static void remethodizeClass(class_t *cls) |
- category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 category 和原来类都有 methodA,那么category 附加完成之后,类的方法列表里会有两个 methodA
- category 的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category 的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。
- 如果有多个 category 都重新实现同一个方法,后加载的 category 的方法会加到其他同名方法的前面
category 和方法覆盖
怎么调用到原来类中被category覆盖掉的方法? 对于这个问题,我们已经知道 category 其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法:
1 | Class className = [GGShop class]; |
category 和 +load 方法
类在加载时,会调用 +load 方法,如果分类中有 +load 方法,会怎样调用
1 | + (void)load { |
在类和分类中分别重写 +load 方法。
在Xcode中点击Edit Scheme -> Run -> Arguments -> Environment Variables,添加如下两个环境变量,设置 Value 为YES(可以在执行load方法以及加载 category 的时候打印 log 信息,更多的环境变量选项可参见objc-private.h)。
1 | // OBJC_PRINT_LOAD_METHODS |
按照 原类,Category1,Category2 的顺序加载类,运行输出结果如下:
1 | REPLACED: -[GGShop doSomething] by category Category1 |
由打印结果可以看出:
- 附加 Category 方法到类的工作会先于 +load 方法的执行
- +load 方法会先后分被执行,顺序为首先执行原类的,然后按照 Category 文件加载的顺序先后执行。
- 在 +load 方法中不要调用父类的 +load 方法,因为父类在加载时会自动调用它的这个方法。
- 与其他运行时才调用的方法不同,+load 方法在 APP 启动时,可执行文件加载到内存时便调用了。其他运行时的方法,最后加载的会加到其他同名方法前边,调用时只执行这一个。而 +load 方法 会按照 文件加载的顺序先后分别执行。
category 和关联对象
在 category 里面是无法为类添加实例变量,但是可以通过 Runtime 的方法给类添加关联的值
1 | // .h |
但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?
在 runtime 的源码中,objc-references.mm 文件中有个方法 _object_set_associative_reference:
1 | void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { |
- 可以看到所有的关联对象都由
AssociationsManager
管理。 AssociationsManager
里面是由一个静态AssociationsHashMap
来存储所有的关联对象的。- 这相当于把所有对象的关联对象都存在一个全局 map 里面。而 map 的 key 是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个 map 的 value 又是另外一个
AssociationsHashMap
,里面保存了关联对象的kv对。
而在对象的销毁逻辑里面,见objc-runtime-new.mm:
1 | void *objc_destructInstance(id obj) |
runtime 的销毁对象函数objc_destructInstance
里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations
做关联对象的清理工作。
类扩展(extension)
extension 被开发者称之为扩展、延展、匿名分类。extension 看起来很像一个匿名的category,但是 extension 和category 几乎完全是两个东西。和 category 不同的是 extension 不但可以声明方法,还可以声明属性、成员变量。extension 一般用于声明私有方法,私有属性,私有成员变量。
category是拥有.h文件和.m文件的东西。但是extension不然。extension只存在于一个.h文件中,或者extension只能寄生于一个类的.m文件中。比如,viewController.m文件中通常寄生这么个东西,其实这就是一个extension:
1 | @interface ViewController () |
当然我们也可以创建一个单独的extension文件,但是,extension 常用的形式并不是以一个单独的.h文件存在,而是寄生在类的.m文件中。
category 和 extension 对比
extension 看起来像匿名的 category,但是 extension 和有名字的 category 几乎完全是两个东西。
- extension 在编译期决议,它就是类的一部分,在编译期和头文件里的
@interface
以及实现文件里的@implement
一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension 一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加 extension,所以你无法为系统的类比如NSString
添加 extension。 - 但是 category 则完全不一样,它是在运行期决议的。
- extension 可以添加实例变量,而 category 是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。
- extension 和 category 都可以添加属性,但是 category 的属性不能生成成员变量和 getter、setter方法的实现