和谐学习!不急不躁!!我是你们的老朋友小青龙~
前一篇文章iOS底层分析之类的加载(中)分析了ro、rw的初始化以及分类的本质,本文针对分类的加载
展开进一步的探究。
上篇文章,我们分析到:rwe的初始化是在extAllocIfNeeded函数
,调用extAllocIfNeeded函数有好几个地方:
-
attachCategories
函数(将类别的属性、协议、方法添加到类) -
demangledName函数
-
class_setVersion函数(类的版本设置)
-
addMethods_finish函数(添加方法)
-
class_addProtocol函数(添加协议)
-
_class_addProperty函数(添加属性)
-
objc_duplicateClass函数
添加属性、方法、协议等会调用extAllocIfNeeded函数,让我不禁想到了文章《iOS类里面的数据为什么要分为ro、rw、rwe?》的一句话:
- 由于
运行时
的存在,Methods、Protocols、Properties都是可以通过category
或手动使用API
,动态添加、修改,所以rw里也存了一份这些数据。 而rwe是rw的优化,所以可以理解为: category
或手动使用API
修改/添加Methods、Protocols、Properties等会触发extAllocIfNeeded函数,从而给rwe赋值。
回归正题,我们要探究的是跟category有关的,核心函数是attachCategories
,而调用到attachCategories的函数有两个:
- attachToClass
- load_categories_nolock
查看哪里调用了attachToClass
objc源码
搜索attachToClass
,发现只有methodizeClass这个函数内部调用了:
这里有个if判断,我们需要知道previously
值,发现它是由methodizeClass
函数第二个参数传进来的。
继续搜索methodizeClass
:
跟上面一样,也是函数之间参数的传递,继续搜索realizeClassWithoutSwift
:
把上面寻找的过程画了个图:
回到methodizeClass
函数(参数previously为nil
):
static void methodizeClass(Class cls, Class previously){
...
// 经过上传的探索,确认previously为nil
if (previously) {
if (isMeta) {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_METACLASS);
} else {
objc::unattachedCategories.attachToClass(cls, previously,
ATTACH_CLASS_AND_METACLASS);
}
}
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
...
}
等于说调用attachToClass
的地方只有这一个地方:
static void methodizeClass(Class cls, Class previously){
...
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
...
}
查看哪里调用了load_categories_nolock
objc源码
搜索load_categories_nolock
,发现这有这两个地方调用了:
- loadAllCategories
- _read_images
static void loadAllCategories() {
...
load_categories_nolock(hi);
...
}
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
...
load_categories_nolock(hi);
...
}
整理一下调用attachCategories的两条线:
-
realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories
-
load_categories_nolock -> attachCategories
category的load方法实现与否
我们知道,Class根据是否实现load方法,分为懒加载类和非懒加载类,那么category是否也有这样的区分呢?
根据前面分析,调用attachCategories
的地方分别是realizeClassWithoutSwift
和load_categories_nolock
。我们在这几个地方打上断点:
测试一:主类实现load方法,category实现load方法
运行代码:
所以我们得出这样一条走向路线:
graph TD
_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass --> load_categories_nolock --> attachCategories
测试二:主类实现load方法,category不实现load方法
把category里的load方法注视,运行代码,然后会发现走向如下:
- _read_images
- realizeClassWithoutSwift
- methodizeClass
- attachToClass
- 结束
它并没有走
attachCategories
这个函数。
测试三:主类不实现load方法,category实现load方法
相应的处理主类和分类的load方法,运行代码,会发现走向如下:
- _read_images
- realizeClassWithoutSwift
- methodizeClass
- attachToClass
- 结束
和第二种测试结果一样
测试四:主类不实现load方法,category不实现load方法
相应的处理主类和分类的load方法,运行代码:
测试总结:要执行attachCategories
,需要「主类」和「分类」都实现load方法
category的数据在什么时候加载到主类
前面的测试是为了让我们知道:load方法实现与否,它的流程走向。我们真正关心的是category的数据是什么时候加载到ro里。
针对测试一分析(主类load、分类load)
我们知道,类的数据读写分配是在realizeClassWithoutSwift
函数:
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
...
/******* 辅助代码Direction START *********/
const char * className = "Direction";
if (strcmp(class_getName(cls), className) == 0)
{
printf("I'm Direction...");
}
/******* 辅助代码Direction END *********/
auto ro = (const class_ro_t *)cls->data();
...
}
下断点,控制台打印ro
数据:
第二遍进-》
控制台指令如下:
p ro
p *$0
p $0->baseMethods()
p *$2
p $3.get(0).big()
p $3.get(1).big()
p $3.get(2).big()
p $3.get(3).big()
// 图片上显示$4,是因为我前面有一步输入错误,流程没问题
从上图我们可以看到,「realizeClassWithoutSwift」函数并没有将分类数据加载到主类。
按照前面流程,接下来应该是走methodizeClass
函数:
我们发现「methodizeClass」函数,还是没有加载category数据。
继续进入下一个流程attachToClass
函数:
我们发现「attachToClass」函数,还是没有加载category数据。
放过断点,发现它又走了一遍methodizeClass --> attachToClass
继续进入下一个流程load_categories_nolock
函数:
通过断点定位到Direction类,通过methodsForMeta
函数得到cat里的元类方法列表
通过methodsForMeta
函数得到cat里的实例方法列表
再来对比下category里的方法:
由此,我们猜测:
- category的方法是在
load_categories_nolock
函数里,装载到类里的。
断点继续往下走,会发现进入一个attachCategories
函数。
探究attachCategories做了什么事情
断点继续往下走:
我们发现会调用attachLists
函数,第一个参数是个二维指针。
接下来在attachLists
函数打上断点,并通过断点一步步往下走:
发现进入到这一段代码:
void attachLists(List* const * addedLists, uint32_t addedCount) {
...
else {
// 1 list -> many lists
Ptr<List> oldList = list;
// oldList有可能为nil
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
// 将oldList整体插入到下标为addedCount的位置,addedCount是category的方法个数,也就是当前函数传入来的第二个参数
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
// 遍历将addedCount的元素,从0下标开始插入
array()->lists[i] = addedLists[i];
validate();
}
...
}
控制台打印下list:
控制台打印下oldList:
控制台打印下addedLists:
- list为空。
- oldList存放的是主类的方法类表。
- addedLists前面已经分析过了,它是一个二维指针,最终指向的是cateogry里的方法列表。
断点继续走:
我们发现,array()->lists里面存放的
不是一个个method_list_t,而是一个个method_list_t指针
。为了验证这一点,我们继续打印:
我们再来看看array()->lists[0]
存放着什么:
array存放着category方法列表的指针。
针对测试二分析(主类load、分类无load)
测试结果和针对测试三分析
一样。
针对测试三分析(主类无load、分类load)
得出结论:
- 主类未实现load方法,分类实现load方法的情况下,category的数据伴随着image的加载,直接写到data()里;
针对测试四分析(主类无load、分类无load)
发现程序一启动,没有进_read_images函数断点、也没有进realizeClassWithoutSwift函数断点。
放过断点 进入下一步"
我们发现,main.m里执行了Direction *dr = [Direction alloc];
相当于发送了一个alloc消息,具体详情可以点击左边栏:
小断点,移动到执行完ro赋值,然后控制台打印ro:
得出结论:当主类和分类都没有实现load方法,category的数据绑定推迟到第一次发送消息。
综合以上4个测试,对category方法什么时候加载,做一个整理:
-
主类和分类
都实现
load方法: attachCategories加载 -
主类
实现
load,分类不实现
load方法:data()读取的时候加载 -
主类
不实现
load,分类实现
load方法:data()读取的时候加载 -
主类和分类
都不实现
load方法:第一次消息发送的时候加载
拓展:主类实现load,一个分类实现load,另一个分类不实现load
最终还是会走到这里:
这里的count决定着对多个category进行处理,那么count数值来自哪里呢?我们发现在当前函数下,有着两行代码:
processCatlist是一个类似block的声明,因为执行了processCatlist(hi->catlist(&count));这行代码,才会触发里面的for循环。
继续进入catlist
:
category_t * const *header_info::catlist(size_t *outCount) const
{
...
return _getObjc2CategoryList(mhdr(), outCount);
...
}
说明count的数据是从mach-O里面读取的__objc_catlist
字段数据。
由此,我们可以get到一个点:
load方法乱写,会增加程序启动过程中的负担,延长启动时间。
多个分类
现实过程中,对于一个类,可能存在多个分类,我们在原来一个分类的基础之上,再添加以一个分类Direction+categoryTwo
:
// Direction+categoryTwo.h
@interface Direction (categoryTwo)
- (void)look_categoryTwo;
- (void)dreamSome_categoryTwo;
+ (void)cleanSome_categoryTwo;
@end
// Direction+categoryTwo.m
#import "Direction+categoryTwo.h"
@implementation Direction (categoryTwo)
- (void)look_categoryTwo{
NSLog(@"Direction (categoryTwo)---look_categoryTwo");
}
- (void)dreamSome_categoryTwo{
NSLog(@"Direction (categoryTwo)---dreamSome_categoryTwo");
}
+ (void)cleanSome_categoryTwo{
NSLog(@"Direction (categoryTwo)---cleanSome_categoryTwo");
}
@end
我们依旧是从load_categories_nolock函数开始分析(前面已经分析过了):
通过通知台打印,发现i=0的情况下,cat的名字是categoryTwo,再查看
Build Phases:
可以得出一个结论:
- category的加载顺序是看Build Phases的先后顺序
言归正传,我们要分析的是多个category的走向,i=0(也就是category只有一个)的情况我们前面分析过了,我们想探究的是当i=1甚至i=2等等的流程走向,(这里取i=1进行分析)。
继续走断点......我们要看的是attachLists函数
,看看会不会进入if (hasArray()) {
这个语句块:
发现它进入了,我们还顺便打印了下addedLists参数的内容。
继续查看array~
所以可以看到,array依次保存着两个元素:
- 指向category方法列表的指针
- 指向本类方法列表的指针
继续分析:
再结合上图array里保存的数据,我们可以得出一个结论: 当存在多个category的情况,array内部会按照先后顺序排列:categoryX方法列表指针、categoryXX方法列表指针....本类方法类表指针。
经过上图分析,我们得出list的内存结构大概是这样的:
objc源码搜索attachCategories
:
//将方法列表、属性和协议从类别附加到类。
//假设cats中的类别都已加载并按加载顺序排序,
//首先是最古老的类别。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
...
// ********* 针对性分析,我们研究的是Direction *********
/// 辅助代码 START *********/
const char *mangledName = cls->nonlazyMangledName();
const char *comName = "Direction";
if (strcmp(comName, mangledName) == 0) {
//经测试,如果category里面不实现+load方法,不会进入这里
printf("类的加载处理|| mangledName-->%s\n",mangledName);
}
/// 辅助代码 END *********/
// 核心代码
for (uint32_t i = 0; i < cats_count; i++) {
/**** 方法处理 ***/
auto& entry = cats_list[i];///遍历一个个category,得到category地址&entry
// methodsForMeta:如果是元类,返回类方法列表;否则返回实例方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// 判断mcount(category总个数)是否等于64,如果为真就进入语句块
if (mcount == ATTACH_BUFSIZ) {
// cls: 类或元类
// mlists: 从category读取的方法列表,可能是类方法列表,也可能是实例方法类表;
// prepareMethodLists内部:调用fixupMethodList,实现对mlists的SEL绑定、方法排序
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
// 将category的方法列表追加到类的方法列表后面
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 第一个从下标记63的位置,开始插入 -- 也就是倒着插入 (ATTACH_BUFSIZ = 64)
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;//这一行,mcount会做累加
// 类的header信息
fromBundle |= entry.hi->isBundle();
}
/**** 属性处理 ***/
// 根据isMeta,返回: 类属性列表 或 实例属性列表
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
// 将category的属性列表追加到类的属性列表后面
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
// 跟mlists一样,也是从下表63的位置,向前倒着插入
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
/**** 协议处理 ***/
// 根据isMeta,返回: 空 或 协议列表
// 注意:元类没有协议
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
// 将category的协议列表追加到类的协议列表后面
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
// 跟mlists一样,也是从下表63的位置,向前倒着插入
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
...
}
attachLists
分析
void attachLists(List* const * addedLists, uint32_t addedCount) {
...
// 第一次hasArray可能为空
if (hasArray()) {
...
//倒叙拆入,假设lists里已经有3个里,addedLists里有2个,那就
// newArray->list[4] = list[2]
// newArray->list[3] = list[1]
// newArray->list[2] = list[0]
// 流出来newArray->list[0]和newArray->list[1]是给addedLists存放的
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
// addedLists里的从0开始插入到newArray->lists里
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
...
}
else if (!list && addedCount == 1) {
// 如果list为空,且addedLists只有一个
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr<List> oldList = list;
// oldList有可能为nil
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
// 将oldList整体插入到下表为addedCount的位置,addedCount是category的方法个数,也就是当前函数传入来的第二个参数
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
// 遍历将addedCount的元素,从0下标开始插入
array()->lists[i] = addedLists[i];
validate();
}
}
我们可以发现,attachLists
函数的目的是:把category的List
插入到原来List的最前面。
对attachCategories
代码的静态分析已经结束,接下来开始实操、下断点、控制台打印验证:
- 下断点
运行工程,等进入第一个断点,说明当前cls是Direction类,然后把第二个断点打开:
这边可以看一下category里的内容:
放过断点,会再一次进入attachCategories
函数,按照前面一次的样子打印mlist:
如此看来:
-
isMeta为false,methodsForMeta(isMeta)返回的是分类-实例方法列表
-
isMeta为true,methodsForMeta(isMeta)返回的是分类-类方法列表
属性列表也同样的操作
第二次进断点,元类打印为nil,因为当前没有类属性。
本文总结
- 针对主类和分类是否实现load方法,衍生出了4种(多个分类这种没算上)情况。
- 针对不同的情况,通过断点大概有了一个走位了解。
- 通过断点,控制台ro的打印,定位category方法是什么时候加载。
- 分析多个category,具体的流程走向,及attachCategories和attachLists的源码分析
代码:
链接: pan.baidu.com/s/1cXT7f_Na…
密码: mu1m
--来自百度网盘超级会员V2的分享