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_$_Test
0
0
-
查找
_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
是在运行时, 才会将数据合并到类信息中