前面已经探究了类的加载
流程,类分为懒加载类
和非懒加载类
,他们有不同加载流程,下面来探究下分类的加载
,以及分类和类搭配使用的情况
分类的本质
准备工作
在 main.m
中定义 HTPerson
的分类HT
, 代码如下
探索分类本质的三种方法
探索分类的本质,有以下三种方式
- 【方式一】通过
clang
- 【方式二】通过
Xcode
文档搜索Category
- 【方式三】通过
objc
源码搜索category_t
【方式一】:通过clang
通过clang -rewrite-objc main.m -o main.cpp
命令,查看编译后的 c++文件
- 其中
分类
的类型是_category_t
,存储了相应的实例方法
、类方法
、属性
、协议
等信息
搜索struct _category_t
,如下所示
- 其中有两个
_method_list_t
,分别对应对象方法
和类方法
- 全局搜索
_CATEGORY_INSTANCE_METHODS_HTPerson_
,找到其底层实现
- 查看
协议
和属性
的结构
这里我们发现一个【问题】:分类中定义的属性没有相应的set、get方法
,我们可以通过关联对象
来设置(关于如何设置关联对象
,我们将在下一篇中进行分析)
【方式二】:通过Xcode文档搜索 Category
通过快捷键command+shift+0
,搜索Category
【方式三】:通过objc源码搜索 category_t
通过objc818
源码搜索category_t
类型
分类的加载的源码分析
分类的底层结构是结构体category_t
,下面我们就来探究 分类是何时加载进来的,以及加载的过程
分类加载的引入
WWDC2020中关于数据结构的变化(Class data structures changes)视频地址,苹果为分类和动态添加专门分配的了一块内存rwe
,因为rwe
属于dirty memory
,所以肯定是需要动态开辟内存。下面从class_rw_t
中去查找相关rwe
的源码
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
// ...省略代码
class_rw_ext_t *ext() const {
return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
}
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
// 判断rwe是否存在
if (fastpath(v.is<class_rw_ext_t *>())) {
// 如果已经有rwe数据,直接返回地址指针
return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
} else {
// 为rwe开辟内存并且返回地址指针
return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
}
}
class_rw_ext_t *deepCopy(const class_ro_t *ro) {
return extAlloc(ro, true);
}
// ...省略代码
}
从代码可以看出,extAllocIfNeeded
方法用来开辟rwe
内存,全局搜索extAllocIfNeeded
,在下列几个地方有相关调用:
-
attachCategories
方法:添加分类信息 demangledName
-
class_setVersion
:设置类的版本 -
addMethods_finish
:动态添加方法 -
class_addProtocol
:动态添加协议 -
_class_addProperty
:动态添加属性 objc_duplicateClass
本文主要来探究分类的加载
,👇我们来分析attachCategories
方法做了什么
attachCategories 反推法
attachCategories
方法,源码如下:
// 将分类的 方法列表、属性、协议等数据加载到 类中
// 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, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
// 获取rwe
auto rwe = cls->data()->extAllocIfNeeded();
// 遍历所有的分类
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[I];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
// 当mlists的个数为 64时,对方法进行排序,然后将 mlists加载到rwe中
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 如果 mcount = 0,mlist存放的位置在63个位置,总共是0 ~ 63, mlists最多存放64个方法列表(mlist)
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 处理属性数据
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
// 处理协议相关信息
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__, [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
attachCategories
准备分类的数据,然后调用attachLists
将数据添加到rwe
中,那么到底哪些地方调用attachCategories
方法,
- 全局搜索
attachCategories
,发现有两处进行了调用,分别是attachToClass
方法和load_categories_nolock
方法
attachToClass流程流程分析
全局搜索attachToClass
,发现只有methodizeClass
方法中进行了调用
-
methodizeClass
方法,我们应该不陌生,在上一篇类的加载
中有分析,从源码我们发现previously
的值为nil
-
previously
作为备用参数,这种设计可能是苹果内部调试用的 -
attachToClass
调用流程:_read_images
-->realizeClassWithoutSwift
-->methodizeClass
-->attachToClass
-->attachCategories
-->attachLists
load_categories_nolock流程分析
- 全局搜索
load_categories_nolock
,在loadAllCategories
方法中调用
- 接着全局搜索
loadAllCategories
,在load_images
方法中调用
-
didInitialAttachCategories
默认值是false
,当执行完loadAllCategories()
后将didInitialAttachCategories
的值设为true
,其实就是只调用一次loadAllCategories()方法
-
load_categories_nolock
的调用流程:load_images
-->loadAllCategories
-->load_categories_nolock
-->attachCategories
attachLists方法分析
attachLists
方法得源码如下:
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;
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; I--)
newArray->lists[i + addedCount] = array()->lists[I];
for (unsigned i = 0; i < addedCount; I++)
newArray->lists[i] = addedLists[I];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; I++)
array()->lists[i] = addedLists[I];
validate();
}
}
从源码可以看出,attachLists
方法总共有三个流程分支:
【流程1】:0 lists -> 1 list
- 将
addedLists[0]
的指针赋值给list
【流程2】:1 list -> many lists
- 计算
旧的list
的个数 - 计算
新的list
个数 ,新的list个数 = 原有的list个数 + 新增的list个数
- 根据
newCount开辟相应的内存
,类型是array_t
类型,并设置数组标识位
-setArray
- 将原有的
list
放在数组的末尾,因为最多只有一个不需要遍历存储
- 遍历
addedLists
将遍历的数据从数组的开始位置存储
【流程3】:many lists -> many lists
判断
hasArray()
是否存在计算
原有的
数组中的list
个数,array()->lists
计算
新的list
个数 ,新的list个数 = 原有的list个数 + 新增的list个数
根据
newCount
开辟相应的内存,类型是array_t
类型设置新数组的个数等于
newCount
设置原有数组的个数等于
newCount
遍历原有数组中list将其存放在
newArray->lists
中 且是放在数组的末尾
遍历
addedLists
将遍历的数据从数组的开始位置存储释放原有的
array()
设置新的
newArray
list_array_tt
结构和方法分析
-
rwe
结构中的方法
、属性
、协议
的类型都是继承自list_array_tt
,在底层是二维数组的形式存储
实例验证分类的加载
通过上面的两个例子,我们可以大致将类
和 分类
是否实现+load
方法的情况分为4种
类和分类 | 分类实现+load | 分类未实现+load |
---|---|---|
类实现+load | 非懒加载类+非懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> | 非懒加载类+懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> |
类未实现+load | 懒加载类+非懒加载分类<span class="Apple-tab-span" style="white-space:pre"></span> | 懒加载类+懒加载分类 |
准备工作
- 创建
HTPerson
类以及分类HTPerson (HTA)
非懒加载类和非懒加载分类的加载
即主类实现了+load方法,分类同样实现了+load方法
,在前文分类的加载时机时,我们已经分析过这种情况,所以可以直接得出结论,这种情况下
- 程序启动,会直接加载
非懒加载类
,加载主类的方法 -
分类的数据加载
是通过load_images
加载到类中的
运行代码,发现会调用attachCategories
方法,来加载分类信息,通过bt
查看函数调用栈
在相应函数出设置断点,打印结果如下
- 通过
MachOView
查看可执行文件
非懒加载类与懒加载分类
即主类实现了+load方法,分类未实现+load方法
- 运行程序,发现并没有调用
attachCategories
方法,那么分类是如何加载的呢?
- 在
realizeClassWithoutSwift
方法处设置断点,我们来看一下ro
是否有分类方法
- 获取ro的方法列表:
p ro->baseMethods()
- 打印第i个方法信息:
p $2.get(i).big()
从上面的打印输出可以看出,分类的方法和类的方法已经合并到一起了,方法的顺序是 HTA分类-HTPerson类
,此时分类已经 加载进来了,但是还没有排序,说明这种情况下分类数据在编译时
就与类数据合并到一起了,不需要运行时添加进去
- 通过
MachOView
查看可执行文件
懒加载类与懒加载分类
即主类和分类均未实现+load方法
- 程序启动时,类数据不会加载,只有在
首次接收消息时
才加载
其中realizeClassMaybeSwiftMaybeRelock
是消息流程中慢速查找中的函数,即在第一次调用消息时
才会去加载懒加载类
- 在
realizeClassWithoutSwift
方法处设置断点,我们来看一下ro
是否有分类方法
- 通过
MachOView
查看可执行文件
【结论】:
- 懒加载类与懒加载分类的数据加载是在
消息第一次调用
时加载 - 分类数据与类数据,在
编译时
已合并到一起,MachO
文件中的分类列表__objc_catlist
中无分类
懒加载类与非懒加载分类
即主类未实现+load方法,分类实现了+load方法
- 运行程序,会调用
realizeClassWithoutSwift
方法,即程序一启动,就会记载类数据,产看函数调用栈如下图:
- 在
realizeClassWithoutSwift
方法处设置断点,我们来看一下ro
是否有分类方法
从上面的打印输出可以看出,分类的方法和类的方法已经合并到一起了,方法的顺序是
HTA分类-HTPerson类
,此时分类已经 加载进来了,但是还没有排序,说明这种情况下分类数据在编译时
就与类数据合并到一起了,不需要运行时添加进去通过
MachOView
查看可执行文件
结论:
- 懒加载类变成非懒加载类,分类的数据在
编译期间
合并到类数据中
多分类的情况
新增两个分类,HTPerson (HTB)
和HTPerson (HTC)
通过不同组合来,验证类和分类的加载,总结如下
实现+load方法的分类个数 | 非懒加载类 | 懒加载类 |
---|---|---|
0 | 编译时类数据与分类数据已合并 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 | 首次接收消息时,才加载类数据,分类数据与类数据,在编译时 已合并到一起 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 |
1 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:1 | 程序启动加载类数据(编译器将类标记为非懒加载类 ),分类数据与类数据,在编译时 已合并到一起 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:0 </br> __objc_nlcatlist:0 |
2 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:2 | 编译后,类仍是懒加载类 ,程序启动(load_images 方法中)会加载类和分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3 </br> __objc_nlcatlist:2 |
3 | 程序启动加载类数据,load_images时加载分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:1 </br> __objc_catlist:3 </br> __objc_nlcatlist:3 | 编译后,类仍是懒加载类 ,程序启动(load_images 方法中)会加载类和分类数据 </br> MachO文件中类和分类数据如下:</br> __objc_classlist:1 </br> __objc_nlclslist:0 </br> __objc_catlist:3 </br> __objc_nlcatlist:3 |
- 1、非懒加载类 + 3个懒加载分类
- 2、非懒加载类 + 2个懒加载分类 + 1个非懒加载分类
程序启动加载类数据,load_images
时按照MachO中 __objc_catlist中的顺序
挨个加载分类数据
- 3、非懒加载类 + 1个懒加载分类 + 2个非懒加载分类
程序启动加载类数据,load_images
时按照MachO中 __objc_catlist中的顺序
挨个加载分类数据
- 4、非懒加载类 + 3个非懒加载分类
程序启动加载类数据,load_images
时按照MachO中 __objc_catlist中的顺序
挨个加载分类数据
- 5、懒加载类 + 3个懒加载分类
首次接收消息时,才加载类数据,分类数据与类数据,在编译时
已合并到一起
- 6、懒加载类 + 2个懒加载分类 + 1个非懒加载分类
程序启动加载类数据(编译器将类标记为非懒加载类
),分类数据与类数据,在编译时
已合并到一起
- 7、懒加载类 + 1个懒加载分类 + 2个非懒加载分类
编译后,类仍是懒加载类
,程序启动(load_images
方法中)会加载类和分类数据,类和分类的加载流程:load_images
--> prepare_load_methods
--> realizeClassWithoutSwift
--> methodizeClass
--> attachToClass
--> attachCategories
- 8、懒加载类 + 3个非懒加载类
编译后,类仍是懒加载类
,程序启动(load_images
方法中)会加载类和分类数据,类和分类的加载流程:load_images
--> prepare_load_methods
--> realizeClassWithoutSwift
--> methodizeClass
--> attachToClass
--> attachCategories