学习MJ的视频课程,整理总结知识点–Category的本质

[TOC]

Category基本使用

category 是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

category 还可以对类进行模块拆解,甚至模拟多继承等操作。具体可参考官方文档Category

Category底层结构

我们知道category的基本用法,前面我们也已经知道了NSObject、class的底层结构,及调用机制。那么category的结构是什么样的,OC是通过怎样的机制来实现category?

想知道category的底层结构(基于经验,完全可以大胆的推测它是结构体),结合前面的经验,我们可以去两个地方查线索。

  1. clang重写category的编译文件来查看项目中的源码
  2. 阅读objc-runtime-new.h开源的源码,搜索category关键字进行查看

这里通过Demo,用clang重写category的编译文件,分析重写后生成的cpp文件,来探索category的底层结构。

创建MJPerson.hMJPerson.m,创建MJPerson+Test.hMJPerson+Test.m,然后通过class重写MJPerson+Test.m文件,生成为C++文件,如下:

1
clang -rewrite-objc MJPerson+Test.m -o MJPerson+Test.cpp

打开MJPerson+Test.cpp文件,我们看到如下代码,简单阅读一下,这个看起来就是我们要找到的MJPerson+Test.m重写后的另一种表达样式。

1
2
3
4
5
6
7
8
9
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test,
0,
0,
};

我们还可以找到_CATEGORY_INSTANCE_METHODS_MJPerson__CATEGORY_CLASS_METHODS_MJPerson_的具体实现结构。

1
2
3
4
5
6
7
8
9
10
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_MJPerson_Test_run},
{(struct objc_selector *)"test", "v16@0:8", (void *)_I_MJPerson_Test_test}}
};

同时,我们还能在MJPerson+Test.cpp文件中找打如下代码:

1
2
3
4
5
6
7
8
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

看起来很像category结构体的标准格式。

  1. name注意,并不是category小括号里写的名字,而是类的名字
  2. cls要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
  3. instance_methods这个category所有的-方法
  4. class_methods这个category所有的+方法
  5. protocols这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的
  6. properties这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。

上面这些是我们利用编译器重写OC代码生成的C++源码,同时我们还可以在apple开源的objc中找到category的源码,在objc-runtime-new.h文件中看到定义的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

Category底层结构部分Demo的下载地址

Category源码分析

通过解读category的源码,我们知道category的底层结构长这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

我们知道OC中所有的方法调用都会转成runtime的objc_msgSend,这个函数需要两个参数id, SEL,函数通过这个id参数查找对象的isa指针,通过isa找到class的方法列表,在方法列表中匹配SEL进行调用。

但是在之前的class源码解读中(struct objc_class : objc_object {...}),我们已经知道,实例方法信息都是存储在class的方法列表中(struct objc_class -> class_rw_t* data() -> method_array_t methods,注:在不同的objc源码版本中可能会有细微差异)

那么category里的方法信息存储在哪里?是否就在本来的category_t中? 程序运行时如何调用到category里面的信息?

实际上,category的信息最终也会合并到class中,且在runtime加载完成后,category的原始信息在类结构里将不会存在。

我们先通过打印class的方法列表

1
2
3
4
5
6
7
// 打印class的方法列表,发现category中的方法也会被打印出来,看起来分类的方法似乎存在于Class中了
unsigned int outCount;
Method *methodlist = class_copyMethodList([MJPerson class], &outCount);
for (NSInteger i = 0; i < outCount; i++) {
Method method = methodlist[i];
NSLog(@"%@", NSStringFromSelector(method_getName(method)));
}

发现Class的方法列表中包含了category的方法,可以说明category的信息会添加到class中。

下面我们通过runtime的加载入口为切入点,来解读category的信息是如何添加到到class中。

这需要探究下runtime对category的加载过程,这里就简单说一下

  1. objc runtime的加载入口是一个叫_objc_init的方法,在library加载前由libSystem dyld调用,进行初始化操作
  2. 调用map_images方法将文件中的imagemap到内存
  3. 调用_read_images方法初始化map后的image,这是一个很重要的函数。这里面干了很多的事情,像load所有的类、协议和category,著名的+ load方法就是这一步调用的
    仔细看category的初始化,调用了_getObjc2CategoryList方法获取所有的categoriescategory_t **catlist = _getObjc2CategoryList(hi, &count);,接下来runtime终于开始了category的处理,简化的代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Process this category. 
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols || cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}

if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
}

首先分成两拨,一拨是实例对象相关的调用addUnattachedCategoryForClass,一拨是类对象相关的调用addUnattachedCategoryForClass,判断每一拨里面是否实现有方法、协议、属性,有的话就remethodizeClass重新组织class信息结构。

remethodizeClass函数中有一个重要方法attachCategoriesattachCategories(cls, cats, true);将分类列表cats的附加到类cls

接下来讲解attachCategories函数,不粘贴大片的代码,我用注释说明主要做的事情,需要的可以去看源码如下:

1
2
3
4
5
6
7
8
9
10
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
// 1. 分配方法数组、属性数组、协议数组空间
// 2. 遍历分类数组cats,取出方法、属性、协议并添加到对应的数组中
// 3. 拿到cls的信息表auto rw = cls->data(); 往cls里面添加categories的方法数组、属性数组、协议数组,最后把数组空间释放。
}

在上面的第3步(rw->methods.attachLists(mlists, mcount)),往cls里面添加数组数据的时候,我们讲解一下个重要方法attachLists.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
// 计算新数组个数后重新分配空间
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// cls自身的数组部分移动到新数组0位置
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
// 增加的部分copy到新数组0位置
memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
}

attachLists流程有3个关键的地方,reallocmemmovememcpy,有一个非常重要的地方,就是memcpy,在categories加载的过程中就是把categories的信息(方法、属性、协议)添加到新数组的其实位置。因为runtime的方法调用机制就是找到对应的方法后方法查找流程就结束,然后进行方法调用。正是因为每次categories的信息都是添加到cls的方法数组的最前面,所以遇到category和class方法同名时,优先调用的是category的方法。

学习了categories的加载机制,我们还可以解释另外一个问题,就是同一个class的多个category,每个category都有同样方法,则最后参与编译的分类方法优先调用。

因为在attachCategories过程中,有这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建用于存储同一个class的所有category方法的数组
method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));

while (i--) {
auto& entry = cats->list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 向数组中添加方法
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// ...
}

因为是i--的while循环,所以最后加载的category会添加到数组的最前端。这样runtime在方法查找的时候,会优先查找到最前面的方法,也就意味着最后参与编译的分类方法优先调用。

总结

  1. Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
  2. category信息添加到class时有几个重要的函数
    • _objc_init // runtime的加载入口
    • map_images // 将文件中的imagemap到内存
    • _read_images // 重要:初始化map后的image
    • _getObjc2CategoryList // 获取所有的categories
    • remethodizeClass // 为了添加category重修Class
    • attachCategories // 添加categories到Class
    • attachLists // Class的结构体重新分配大小,把category中的信息添加的最前面

参考和源码

参考:
objc category的秘密

Demo源码:
Category的底层结构-Demo.zip

评论