iOS 底层 day06 Category-01
Category 当我们对一个类的功能进行按模块进行拆分的时候,就会用到Category
一、探究 Category 的底层结构
1. 通过生成 cpp 文件来观察 Category 的底层结构
- 我们编写如下代码:
// .h文件
#import "Person.h"
@interface Person (Walk)<NSCoding, NSCopying>
@property(nonatomic, assign) int walkSpeed;
- (void)walk;
@end
// .m文件
#import "Person+Walk.h"
@implementation Person (Walk)
- (void)walk {
NSLog(@"Person walk");
}
@end
-
通过指令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Walk.m
生成Person+Walk.m
对应的Person+Walk.cpp
文件,这样方便我们观察Category 到底是什么样子的。 -
我们可以在
Person+Walk.cpp
文件中找到如下代码
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的定义,我们也可以在源码中找到相同的结构体
static struct _category_t _OBJC_$_CATEGORY_Person_$_Walk __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Walk,
0,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Walk,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Walk,
};
-
上面代码就是Category结构体的实例,也就是我们的
Person+walk
分类 -
将
"Person"
赋值给_category_t
结构体的name
-
将
"0"
赋值给_category_t
结构体的cls
-
将
&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Walk
赋值给_category_t
结构体的instance_methods
-
将
0
赋值给_category_t
结构体的class_methods
(因为我们没有定义类方法) -
将
&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Walk
赋值给_category_t
结构体的protocols
-
将
&_OBJC_$_PROP_LIST_Person_$_Walk
赋值给_category_t
结构体的properties
-
然后我们再观察一下,
instance_methods
中的&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Walk
到底是什么?
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Walk __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"walk", "v16@0:8", (void *)_I_Person_Walk_walk}}
};
-
我们找到对应的代码,可以发现就是一个
_method_list_t
方法列表,里面描述着我们唯一一个walk
实例方法。 -
协议和属性以及类方法都有类似的情况
-
至此,我们完整分析了,我们在 OC 中的 Category 到底变成了什么结构?
二、Category 的源码分析
1. 先说几个结论,我们带着结论去阅读源码,才有目的性
-
所有的分类(Category)的实例方法, 都会统一被放到类对象的方法列表中去
-
每个分类都会被编译成
_category_t
这种结构体对象,在运行时再加入到类对象或者元类对象中 -
分类方法调用
的优先级与加载时候的加载顺序有关系,后编译、后加载
的优先级高
2. 开始分析 objc4-723
源码
- 在
objc-os.mm
中找到_objc_init
, 这是 objc 初始化入口 - 接着找到
map_images
,这里是 images 是镜像的意思 - 接着找到
map_images_nolock
- 接着找到
_read_images
- 再找到
remethodizeClass
,从字面意思就是Class重新方法化
,我们从这里开始仔细分析代码(主要观察有中文注释的代码)
/***********************************************************************
* remethodizeClass
* Class重新方法化
* Attach outstanding categories to an existing class.
* 将未处理的 categories 附加到现存的 class 中
* Fixes up cls's method list, protocol list, and property list.
* 修正 cls 的方法列表、协议列表、属性列表
* Updates method caches for cls and its subclasses.
* 更新 cls 的和它子类的 方法缓存
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
// 元类判断
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
// 找出所有未处理的 categories ,放入 cats 中
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// 将 categories 进行归属
attachCategories(cls, cats, true /*flush caches*/);
// 释放 cats
free(cats);
}
}
- 我们进入
attachCategories
方法中继续分析
// Attach method lists and properties and protocols from categories to a class.
// 将 categories 中的方法列表、属性列表和协议列表链接到 class 上
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
// 假设cats中的所有categories都已加载,并且按照加载顺序排序,最早的categories 放在前。
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
// 给三个二维数组分配内存(方法列表数组、协议列表数组、属性列表数组)
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));
// Count backwards through cats to get newest categories first
// i-- ,就是倒叙遍历cats,最后加载的会放到最前面(这也是为什么后加载的分类,调用方法的时候优先级高的原因之一)
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 在这里对 rw 的methods进行链接了
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
- 我们进入
attachLists
方法中继续分析
// addedLists 是categories方法列表数组,是二维数组
// addedCount 是二维数组的长度
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
// 原来的方法列表list长度
uint32_t oldCount = array()->count;
// 原来的方法列表list个数 + 新增list个数
uint32_t newCount = oldCount + addedCount;
// 对原来数组进行扩容
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 将原来方法list的位置,利用 memmove 向后移动 addedCount 个位置
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 讲新的分类方法list,利用 memcpy 拷贝到 上一步 挪开的来位置(这一步是为什么分类的方法优先级高于原类的原因)
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
- 至此我们可以得出,categories完整链接到类上的过程
三、至此,我们可以轻松解决如下问题
1. Category的使用场景是什么?
- 将一个类按照某种需求(比如功能、特点)进行模块化
2. Category的实现原理是什么?
- 从底层结构来讲,Category 是一个包含
类方法数组
、实例方法数组
、属性数组
、协议数组
的一个结构体 - 从调用过程来讲,分如下两步骤:
- Runtime 在
运行时
将所有的Categories
(里面是类方法数组
、实例方法数组
、属性数组
、协议数组
的结构体) 放到一个大数组中
- Runtime 在
- 然后将大数组的数据按照倒序取出(后编译的先取出),并且按照
类方法数组
、实例方法数组
、属性数组
、协议数组
进行分类
- 然后将大数组的数据按照倒序取出(后编译的先取出),并且按照
- 将分类好的
类方法数组
、实例方法数组
、属性数组
、协议数组
的分别插入到原来类的类方法数组
、实例方法数组
、属性数组
、协议数组
数组前面
- 将分类好的
3. Category 和 Class Extension 的区别是什么?
- Category 是在程序运行时加入到类中的
- Class Extension 程序一编译就合并到类中了