Category一般用来给已有类添加新的功能, 或者给自定义类分模块
一、Category的使用
- 定义
Person类, 继承自NSObject

- 定义
Person类的Category, Test

- 定义
Person类的Category, Eat

- 在
Person,Test和Eat中, 都有一个实例方法和一个类方法, 其中Test和Eat都是对Person的扩展 - 我们可以再
main.m中调用这三个文件中的方法

- 已知, 实例方法存在于
类对象中, 类方法存在于元类对象中 - 对象在调用方法的时候, 会有以下顺序:
- 调用实例方法: 实例通过
isa找到类对象, 然后查看是否有方法, 有就调用 - 调用类方法: 类对象通过
isa找到元类对象, 然后查看是否有方法, 有就调用
- 调用实例方法: 实例通过
问: Category中的方法, 存储在什么地方呢?
二、查看Category的底层实现
- 首先使用命令行, 获取
Person+Test.m在底层的C++实现
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
- 然后使用Xcode打开生成的
Person+Test.cpp文件

- 查看文件, 可以发现有如下
结构体

- 在这个结构体中, 定义了
类名,实例方法列表,类方法列表,协议列表和属性列表 - 接着还可以看到如下的代码

-
这是一个
_category_t结构体类型的实例_OBJC_$_CATEGORY_Person_$_Test -
在里面可以看到:
"Person"0(const struct _method_list_t)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test(const struct _method_list_t)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test00
-
查找
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test:

- 查找
_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test:

-
可以看到, 结构体每一个有值的部分, 都是
Person+Test中定义的方法 -
这说明, Category中的
对象方法和类方法在编译后, 在底层是以结构体的形式存在, 而不是并入到Person的类对象和元类对象中 -
也可以在
Person+Test添加属性, 并遵守协议, 此时在编译文件中, 就可以看到结构体里有属性和协议的值


三、当Category中的方法名与类定义的方法名相同时, 会有怎样的效果
- 给
Person, Person+Eat, Person+Test中分别添加相同的方法, 如下图



- 添加了相同的
-(void)say和+(void)say1方法, 此时调用这些方法, 结果如下

- 根据结果, 可以知道
Person的-(void)say和+(void)say1方法已经被Person+Eat中-(void)say和+(void)say1方法覆盖了
疑问: Person中的
-(void)say和+(void)say1方法真的被覆盖了吗? 为什么调用的是Person+Eat中的方法, 而不是Person+Test中的方法
四、查看加载分类方法的源码
- 源码下载地址
- 找到版本最新的源码, 并下载

- 下载后, 打开程序, 找到
objc-os.mm文件中的void _objc_init(void)函数

- 进入
map_images函数

- 进入
map_images_nolock函数

- 在
map_images_nolock函数中, 可以找到_read_images函数的调用

- 进入
_read_images函数

- 在
_read_images函数中, 可以找到加载categorys的代码

- 在这些代码里, 可以找到重组类方法的代码
remethodizeClass(cls)函数调用

- 进入
static void remethodizeClass(Class cls)函数

- 进入
static void attachCategories(Class cls, category_list *cats, bool flush_caches)函数

- 在
attachCategories函数中, 进行的就是对categorys进行加载的代码
// 将Category中的方法, 属性, 协议等加入到类对象和元类对象中
// cls: 类对象或元类对象
// cats: 类所有的category组成的数组
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
// 如果没有分类, 直接返回, 不进行处理
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
// 判断是否是 元类对象
bool isMeta = cls->isMetaClass();
// 创建二维数组, 存放 分类中 对象方法(对象包括: 类对象和元类对象)
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
// 创建二维数组, 存放 分类中 属性(对象包括: 类对象和元类对象)
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
// 创建二维数组, 存放 分类中 协议(对象包括: 类对象和元类对象)
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// 记录category中方法的数量索引
int mcount = 0;
// 记录category中属性的数量索引
int propcount = 0;
// 记录category中协议的数量索引
int protocount = 0;
// 获取Category的数量
int i = cats->count;
bool fromBundle = NO;
// 从后向前遍历
while (i--) {
// 取出传入类对象的 category
auto& entry = cats->list[i];
// 找出category中的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 将category中的方法列表, 加入到容器mlists中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 找出category中的属性列表
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
// 将category中的属性列表, 加入到容器proplists中
proplists[propcount++] = proplist;
}
// 找出category中的协议列表
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
// 将category中的属性列表, 加入到容器protolists中
protolists[protocount++] = protolist;
}
}
// 取出类对象中的数据(属性, 协议, 方法等)
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 将所有的Category中的方法, 合并到类对象和元类对象中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
// 将所有的Category中的属性, 合并到类对象和元类对象中
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 将所有的Category中的协议, 合并到类对象和元类对象中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
- 在上述的代码, 最后面, 调用了
attachLists函数, 将Category中的数据合并到类对象和元类对象中

- 进入
void attachLists(List* const * addedLists, uint32_t addedCount)函数, 可以找到合并的代码

- 首先重新分配内存, 可以足够放下类和Category中所有的方法数据
- 接着使用
void *memmove(void *__dst, const void *__src, size_t __len);函数, 将类中原有方法移动到方法列表的最后边 - 最后将
Category中的方法copy到方法列表的最前边 - 属性和协议也是同样的方式
此时,
Category的方法就放在了方法列表中的前面, 而类中的原有方法则存在于方法列表的最后边
- 此时, 如果在调用类的方法, 就会从方法列表中从前往后查询, 而如果
Category中有相同的方法, 那么就会直接使用Category中的方法 - 这就是上面
Person类调用say和say1两个方法时, 会调用分类Person+Eat中的方法
问: 为什么调用
Person的say和say1方法时, 调用的是Person+Eat中方法, 而不是Person+Test中方法呢?可以控制调用哪个Category中的方法吗?
- 我们继续看
static void attachCategories(Class cls, category_list *cats, bool flush_caches)函数 - 这个函数传入的类的
Category列表, 但是在遍历的时候却是从后向前遍历

- 这就说明, 在运行时, 后加载的
Category会合并在先加载的Category的面前, 我们也可以在项目中找到这一事实

-
由上图可知, 程序运行时先加载的
Person+Test, 后加载的Person+Eat, 所以在合并后的Person的方法列表里,Person+Eat中的方法排在了Person+Test中方法的前面 -
我们可以在
Target -> Build Phases -> Comple Sources中看到代码在运行时加载的顺序

- 代码是根据上图中的顺序, 从上到下的顺序加载的。
- 我们可以手动修改文件的加载顺序, 从而达到调用某个
Category中方法的目的

- 将
Person+Eat上移, 接着重新运行程序, 此时就是调用的Person+Test中的方法

五、面试题
-
Category的实现原理Category编译之后的底层结构是struct category_t, 里面存储着分类的对象方法、类方法、属性、协议信息- 在程序运行的时候,
runtime会将Category的数据, 合并到类信息中(类对象、元类对象中)
-
Category和Class Extension的区别是什么?Class Extension在编译的时候, 他的数据就已经包含在类信息中Category是在运行时, 才会将数据合并到类信息中