Category 的合并过程本质上是一个**“后进先出”的列表插入操作**,它发生在类加载的第三和第四阶段(Realization & Category Attachment)。
我们可以将这个过程拆解为三个具体的步骤:
1. 数据的提取 (Preparation)
当 libobjc 扫描到一个镜像(Image)中的分类时,它会从 Mach-O 文件的 __objc_catlist 段中提取出 category_t 结构体。
这个结构体包含了:
- 分类名和归属的主类。
- 方法列表 (method_list_t) 。
- 属性列表 (property_list_t)。
- 协议列表 (protocol_list_t)。
2. 内存中的“倒序”排列 (Sorting)
如果一个主类有多个分类,Runtime 会收集这些分类。
- 加载顺序:取决于编译顺序(在 Xcode 的
Compile Sources中的排列顺序)。 - 处理逻辑:Runtime 会将这些分类存入一个数组。注意,后编译的分类会被放在数组的前面。
3. 核心步骤:列表的“平移与插入” (Attach Categories)
这是合并最关键的一步,发生在 attachCategories 函数中。假设主类原本有一个方法列表:
- 扩容:Runtime 会计算所有分类带来的方法总数,然后给主类的
class_rw_t中的方法列表扩容。 - 平移(Memmove) :将主类原有的方法列表(以及之前已经合并进去的分类方法)向后移动,空出前面的位置。
- 拷贝(Memcpy) :将新分类的方法列表依次拷贝到空出的起始位置。
[Image illustrating category methods being inserted at the beginning of the class method list, pushing original methods back]
4. 结果:为什么分类会“覆盖”主类方法?
通过上面的步骤你会发现,主类的方法并没有被“删除”或“替换”,它只是被挤到了后面。
- 当你调用
[obj method]时,Runtime 的objc_msgSend会在类的方法列表中顺着索引从前往后找。 - 由于分类的方法被插在了最前面,Runtime 找到第一个匹配的方法名后就会立即去执行,不再往后找了。
- 结论:分类方法之所以生效,是因为它在搜索路径上抢占了“先机”。
5. 两个分类有同名方法怎么办?
根据第 2 步的逻辑:
- 后编译的分类,其方法会排在更前面。
- 因此,在 Xcode 的
Compile Sources中排在最后的那个分类,其方法会最终“胜出”。
💡 进阶知识:如何调用被分类“覆盖”掉的原生方法?
既然原生方法只是被挤到了后面,理论上我们是可以找到它的。你可以通过 class_copyMethodList 获取完整的方法列表,然后遍历这个数组,找到最后一个同名的方法手动调用。