在上篇文章类的加载分析中,分析了非懒加载类的加载流程,ro
、rw
、rwe
的逻辑,方法的排序流程等,本篇将重点分析懒加载类和分类的加载过程。
1.引入问题
首先,回顾一下方法methodizeClass
,上一篇文章也做了分析。见下图:
- 该方法是将
ro
中的方法、协议、属性附加到rwe
中,但是跟踪代码会发现此时的rwe
还是空,那么rwe
何时创建呢?这是我们要摸清的! - 同时通过全局搜索
methodizeClass
方法,previously
的传入值都是nil
,可以理解if(previously)
中的流程应该是为未来的一些处理做的预留。
进入attachToClass
方法,找到了分类处理的一个入口:attachCategories
方法。见下图:
通过上一篇文章分析,很遗憾,跟踪发现attachCategories
没有被调用到!最终我们通过反向推导
的方式找到调用attachCategories
方法的地方,也就是load_categories_nolock
方法。但是从上一篇文章的分析结果发现,load_categories_nolock
方法貌似也只是在处理实现了load
方法的分类有效。那么对于懒加载的分类加载过程又是怎样的呢?attachCategories
做了什么?
- 汇总一下,我们现在要解决的问题:
- 分类的加载流程?
- 不同情况下,方法的排序是怎么样的?
rwe
在何时创建?attachCategories
做了什么?
接着上一篇文章的内容,我们对多中情况的类和分类的加载过程进行分析。
2.分类的加载分析
添加一个案例,创建一个类 LGPerson
,有两个实例方法,并实现了 +load()
,同时创建一个分类 LGPerson (LG)
,有一个实例方法,也实现了 +load()
。见下图:
// LGPerson类
@implementation LGPerson
+(void)load{}
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)sayHello{
NSLog(@"%s",__func__);
}
@end
// 分类
@implementation LGPerson (LG)
+ (void)load{}
- (void)sayHello_cate{
NSLog(@"sayHello_cate %s", __func__);
}
@end
1.非懒加载类和非懒加载分类
-
此种情况在上一篇文章中已经分析了。这里不再分析,直接给出结果,并验证!
-
类初始化
map_images->_read_images->realizeClassWithoutSwift->methodizeClass
-
分类初始化
load_images->load_categories_nolock->attachCategories
-
-
验证一下,在关键位置设置断点,运行程序:
在
methodizeClass
方法中过滤到了LGPerson
,同时查看运行堆栈信息,发现LGPerson
在dyld
应用程序加载时,调用map_images
时即开始了类的初始化流程。在类的ro
中,成功获取了LGPerson
的两个实例方法,说明此时分类的方法还没有添加到类中。见下图:methodizeClass
会对ro
中的方法进行排序,而rwe
还未创建!在
load_categories_nolock
方法中过滤到了LGPerson
类,并获取了对应的分类LG
。通过堆栈信息可以发现,同样是在dyld
应用程序加载时,调用load_images
时开始了分类的初始化流程。见下图:
2.懒加载类和非懒加载分类
运行程序前,先思考一下,类现在是懒加载,那么read_images
过程中就不会进入realizeClassWithoutSwift
了呢?分类依然是非懒加载,那么分类的加载是否还是通过load_categories_nolock
进行初始化呢?
依然采用上面的案例,去掉LGPerson
类中的+load
方法。
在关键位置设置断点,运行程序:
运行结果很意外,应用程序加载阶段,在read_images
中过滤到了LGPerson
,并且调用了realizeClassWithoutSwift
方法。见下图:
继续运行程序,获取LGPerson
对应的ro
数据,打印方法列表结果如下:
方法竟然有3
个,分类的方法已经被放入到了类对应的data()
中,并且处在方法列表的前面。所以可以得出以下结论:
-
懒加载类和非懒加载分类
map_images->_read_images->realizeClassWithoutSwift->methodizeClass
,并且不会调用attachCategories
方法,分类中的方法在编译阶段已添加到data()
中。
3.非懒加载类和懒加载分类
依然采用上面的案例,添加LGPerson
类中的+load
方法,去掉分类中的+load
方法。
在关键位置设置断点,运行程序:
和是第二种(懒加载类和非懒加载分类)情况类似,应用程序加载阶段,在read_images
中过滤到了LGPerson
,并且调用了realizeClassWithoutSwift
方法。获取LGPerson
对应的ro
数据,打印方法列表结果如下:
方法也是3
个,分类的方法已经被放入到了类对应的data()
中,并且处在方法列表的前面。所以可以得出以下结论:
-
非懒加载类和懒加载分类
map_images->_read_images->realizeClassWithoutSwift->methodizeClass
,并且不会调用attachCategories
方法,分类中的方法在编译阶段已添加到data()
中。如果是有多个分类,并且分类都是懒加载,流程一致!
4.懒加载类和懒加载分类
去掉类和分类中的+load
方法。同样在关键位置设置过滤条件,直接运行程序,没有过滤到任何内容,运行结束。此种情况何时加载呢?
回顾一下上一篇文章类的加载分析,我们已经得出结论,懒加载的类在第一次消息发送时进行初始化
。那么此种情况下,分类
是否也是这样呢?
修改一下代码,向LGPerson
类发送一个消息,查看运行结果:
和懒加载类的情况一致,此种情况分类中的数据也自动添加到data()
中。
-
懒加载类和懒加载分类
lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift
,不会调用attachCategories
方法,懒加载,第一次消息发送时初始化,并且分类中的方法自动添加到data()
中。
5.多个分类的情况补充
-
类非懒加载,有多个分类,都是非懒加载
非懒加载类和非懒加载分类,调用
attachCategories
方法初始化分类。 -
类非懒加载,有多个分类,都是懒加载
分类中的方法在编译阶段已添加到
data()
中,不会调用attachCategories
方法。 -
类非懒加载,有多个分类,部分实现
+load
方法非懒加载类和非懒加载分类,调用
attachCategories
方法初始化分类。 -
类懒加载,有多个分类,都是懒加载
懒加载的类和懒加载分类,第一次消息发送时初始化,并且分类中的方法自动添加到
data()
中。 -
类懒加载,有多个分类,部分实现
+load
方法这里有两种情况:
- 只有一个分类实现了
load
方法: 分类中的方法在编译阶段已添加到data()
中,不会调用attachCategories
方法。 - 超过一个分类实现了
load
方法
- 只有一个分类实现了
6.类懒加载,超过一个分类实现了load
方法
针对这种情况单独分析一下,案例说明:LGPerson
类为懒加载,创建三个分类LG
、LG2
、LG3
,其中LG
和LG2
实现了load
方法。
运行程序,发现并没有走map_images
->_read_images
->realizeClassWithoutSwift
的流程。而是在load_categories_nolock
中过滤到了分类处理流程,见下图:
和上面有所区别的是,并未调用attachCategories
方法,而是调用了else
中的objc::unattachedCategories.addForClass(lc, cls);
,说明cls->isRealized()
为false
,也就是说此时类并没有初始化!addForClass
中做了什么呢?
简单理解是,将类和分类进行关联,放在一个BucketT
中,可以理解为一个容器。在load_categories_nolock
中完成三次循环,将分类均存储到对应的BucketT
中。见下图:
继续运行程序,在methodizeClass
中过滤到了LGPerson
见下图:
查看堆栈信息,很熟悉,正是load
方法调用的关键流程,load_images
->prepare_load_methods
。我们来重新回顾一下prepare_load_methods
方法,见下图:
由于类是懒加载,但是分类是非懒加载,而对LGPerson
的分类进行处理时,发现类还没有实现,便调用realizeClassWithoutSwift
方法,对LGPerson
进行初始化。
在methodizeClass
方法中,对LGPerson
本类的相关方法、属性、协议等进行处理,此时分类中的数据还没有附加到本类中。继续运行程序,进而调用attachToClass
方法,见下图:
在attachToClass
中获取类对应的BucketT
,进而获取其分类列表,调用attachCategories
方法。
3.详解attachCategories
方法
1.attachCategories流程分析
创建了三个数组,分别用于处理方法、属性和协议,最大存储空间是64
,见下图:
初始化rwe
:
auto rwe = cls->data()->extAllocIfNeeded();
对分类的方法、属性、协议进行处理,将相关信息attach
到类中。见下图:
通过下面代码,获取对应的方法列表,如果当前类是元类则获取类方法列表,否则获取实例方法列表:
entry.cat->methodsForMeta(isMeta);
// 区分是否为元类
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
对分类列表进行循环处理,在插入数据时,以倒序的方式插入
。同时mLists
是一个二维数组。见下图:
完成方法、属性、协议的整理后,将相关的集合数据插入到rwe
中,见下图:
处理方法列表时,首先对方法列表进行排序,但是需要注意的是,这里的mlist
是一个二维数组,而方法的排序也只是针对各个分类内的方法进行分别排序,并不会将所有方法放到一个集合中进行排序。
2.rwe创建分析
在attachCategories
中对rwe
进行了初始化,见下面代码:
auto rwe = cls->data()->extAllocIfNeeded();
这里我们思考两个问题,
- 什么情况下才会对
rwe
进行创建? - 在
rwe
创建过程中,做了哪些操作呢?
-
首先全局搜索
extAllocIfNeeded
,看哪些地方调用了rwe
创建流程。通过全局搜索,发现在向类添加方法、分类、协议,以及设置版本时,才会对rwe
进行初始化。如添加协议:也就是说除了
ro
数据外,如需要向类添加额外信息时才会进行rwe
的创建。这和我们在类的加载分析中的分析时一致的。 -
在创建
rwe
时,内部做了什么操作呢?跟踪extAllocIfNeeded
源码,其会调用extAlloc
方法进行初始化。流程中会将ro
的数据优先插入到rwe
中,见下图:
3.attachLists分析
查看attachLists
源码,addedLists
是一个指针,指向一个二维数组。针对不同的情形,设置了不同的处理分支,见下图:
-
一维数组变二维数组
分类初次进入,会进行
array()
的初始化,同时设置数组的大小,即为原类的列表数量
添加分类的列表数量
。同时先将类的list
放到最后一个位置,见下图:再开启循环,将分类对应的
list
添加到array()
中,见下图: -
二维数组变二维数组
再次进入时,由于
array()
已经初始化,所以会走到下图中的分支中,验证一下当前array()
的数据顺序,和第一次插入时是一致的,类list
在后,分类list
在前,见下图:在此流程中会开辟
malloc
一个新的newArray
,大小重新初始化,将原数组的数据,进行顺序不变的情况下,插入到新的array
中,同时将新增的分类list
插入到第一个位置,见下图: -
一维数组的创建
一维数组的分支何时进入呢?其实在前面
rwe
的创建过程中,已经进行了一维数组的创建。上面已经分析rwe
的创建流程:extAllocIfNeeded
->extAlloc
->attachLists
。
在调用
attachLists
时,会将ro
的数据优先放入到rwe
对应的一维数组中,见下图:
4.总结
-
通过上面的分析,我们可以发现分类的初始化过程还是比较复杂的,所以在平时的开发过程中尽量不要实现分类的
load
方法,以节省性能。 -
对于方法的排序,并不会将类和分类的方法放在一起排序,在进行初始化过程中,只是针对各自的
list
中的方法进行排序。 -
rwe并不是每个类都有,如需向类添加方法、分类、协议,以及设置版本时,才会对
rwe
进行初始化。