前言
前面已经分析完了类的加载流程以及引出了分类的加载,并且得出了分类加载的两条路线。那么这篇文章还是继续往下分析分类是如何加载的以及分类和主类之间加载不同的情况。
分类加载的两条线路:
1. methodizeClass
-> attachToClass
-> attachCategories
2. load_images
-> loadAllCategories
-> load_categories_nolock
-> attachCategories
attachCategories
里面调用了attachList
。
分类加载
和动态修改
类时候会创建rwe
数据。
准备工作
- objc-818.2
- MachOView 工具
attachCategories 反推法
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t
cats_count, int flags)
{
......(省略一部分代码)......
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();
//遍历所有的分类,分类不能超过64个
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//如果你的分类的个数超过64个那么把这64个分类的方法列表加载到主类中
//ATTACH_BUFSIZ = 64
//把后面的数据继续放到 mlists[]中
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//如果 mcount = 0,mlist存放的位置在63个位置,总共是0 ~ 63
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) {
//将分类方法添加到主类之前进行排序
//通过地址偏移的方式获取mlist成员
//此时的mlists + ATTACH_BUFSIZ - mcount 是一个二维指针,里面存放的是方法列表的首地址
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
方法添加到主类
中。
attachLists流程分析
attachLists
是核心方法,attachLists
作用是将分类数据
加载到主类
中。
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
//对array的存在判断,存在的话就进入判断
if (hasArray()) {
// many lists -> many lists
//oldCount = 获取array()->lists的个数
uint32_t oldCount = array()->count;
//新的个数 = oldCount + 新添加的个数
uint32_t newCount = oldCount + addedCount;
//根据`newCount`开辟内存,类型是 array_t
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
//设置新数组的个数等于`newCount`
newArray->count = newCount;
//设置原有数组的个数等于`newCount`
array()->count = newCount;
//遍历原有数组中list将其存放在newArray->lists中 且是放在数组的末尾
//也就是说新加的数据在数组前面
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
//遍历二维指针`addedLists`中的list将其存放在newArray->lists中 且是从开始的位置进行存放
//将新加入的addedLists依次加入新数组,index从0 ~ addedCount-1
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
//释放原有的array()
free(array());
//设置新的 newArray
setArray(newArray);
validate();
}
//本类中没有方法的时候进入这个判断
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
validate();
}
//当list时一维数组时,此时进入下面的判断创建`array_t`类型的结构体类型
else {
// 1 list -> many lists
//将list数组赋值给oldList
Ptr oldList = list;
//oldList 存在 oldCount = 1,否则为0
uint32_t oldCount = oldList ? 1 : 0;
//新的newCount = 原有的count + 新增的count
uint32_t newCount = oldCount + addedCount;
//根据`newCount`开辟内存,类型是 array_t, array()->lists是一个二维数组
setArray((array_t *)malloc(array_t::byteSize(newCount)));
//设置数组的个数
array()->count = newCount;
//将原有数据的数据放在末尾
if (oldList) array()->lists[addedCount] = oldList;
//遍历addedLists将遍历的数据从数组的开始位置存储
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
总结出attachLists
主要有以下三步:
-
0 list
-->1 list
- 将
addedLists[0]
的指针赋值给list
。
- 将
-
1 list --> many lists
- 计算旧的
list
的个数。 - 计算新的
list
个数 ,新的list
个数 = 原有的list
个数 + 新增的list
个数。 - 根据
newCount
开辟相应的内存
,类型是array_t
类型,并设置数组setArray
。 - 将原有的
list
放在数组的末尾
,因为最多只有一个
不需要遍历存储
。 - 遍历
addedLists
将遍历的数据从数组的开始
位置存储
。
- 计算旧的
-
many lists --> many lists
- 判断
array()
是否存在。 - 计算原有的数组中的
list
个数array()
->lists
的个数。 - 新的
newCount
= 原有的count
+新增的count
。 - 根据
newCount
开辟相应的内存,类型是array_t
类型。 - 设置新数组的个数等于
newCount
。 - 设置原有数组的个数等于
newCount
。 - 遍历原有数组中
list
将其存放在newArray->lists
中 且是放在数组的末尾
。 - 遍历
addedLists
将遍历的数据从数组的开始位置
存储。 - 释放原有的
array()
。 - 设置新的
newArray
。
- 判断
补充:List* const * addedLists
是 二级指针
。 就像 XJLPerson * p = [XJLPerson alloc]
, p
叫做一级指针
,&p
就叫二级指针
(指针的地址
)。
attachLists的流程图
验证attachLists
在上一篇文章中已经粗略验证过attachCategories
方法了,那么我们在此详细的验证一番。
验证attachCategories
首先创建分类XJLPerson+test
:
#import "XJLPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface XJLPerson (test)
@property (nonatomic,strong) NSString *name_test;
@property (nonatomic,assign) NSInteger age_test;
-(void)test;
-(void)sayNB;
@end
NS_ASSUME_NONNULL_END
#import "XJLPerson+test.h"
@implementation XJLPerson (test)
+(void)load{
NSLog(@"我是分类XJLPerson");
}
-(void)test{
NSLog(@"---xjl---%s",__func__);
}
-(void)sayNB{
NSLog(@"---xjl---%s",__func__);
}
@end
添加拦截代码:(方便调试,过滤系统级别的)
const char *clsName = cls->nonlazyMangledName();
if(strcmp(clsName, "XJLPerson") == 0){
printf("XJLPerson来了");
}
断点调试结果:
mlist
中放的是XJLPerson+test
分类的方法。
查看
mlists
的结构:
mlists
中最后一位存方的是test分类
的方法列表的地址
,mlists
是一个二维指针
类型。
mlists + ATTACH_BUFSIZ - mcount
其实就是地址偏移
取值,mlists
是首地址,ATTACH_BUFSIZ - mcount
具体的第几个位置。
验证attachLists
实例验证我们用 查看macho
+ 动态调试
进行相互验证,在验证之前先补充下怎么读取macho
文件 ,源码中出现_getObjc2ClassList
、_getObjc2NonlazyClassList
等方法,进入方法看看具体实现:
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) {
return getDataSection(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection(hi->mhdr(), sectname, nil, outCount); \
}
// function name content type section name
//refs结尾的都是需要修复的类和方法等
GETSECT(_getObjc2SelectorRefs, **SEL**, "__objc_selrefs");
GETSECT(_getObjc2MessageRefs, message_ref_t, "__objc_msgrefs");
GETSECT(_getObjc2ClassRefs, Class, "__objc_classrefs");
GETSECT(_getObjc2SuperRefs, Class, "__objc_superrefs");
//macho section 等于__objc_classlist 所有类的列表(不包括分类)
GETSECT(_getObjc2ClassList, classref_t **const**, "__objc_classlist");
//macho section 等于__objc_nlclslist 懒加载类的列表
GETSECT(_getObjc2NonlazyClassList, classref_t **const**, "__objc_nlclslist");
//macho section 等于__objc_catlist 分类的列表
GETSECT(_getObjc2CategoryList, category_t * **const**, "__objc_catlist");
//macho section 等于__objc_catlist2 分类的列表
GETSECT(_getObjc2CategoryList2, category_t * **const**, "__objc_catlist2");
//macho section 等于__objc_nlcatlist 懒加载分类
GETSECT(_getObjc2NonlazyCategoryList, category_t * **const**, "__objc_nlcatlist");
//macho section 等于__objc_protolist 协议列表
GETSECT(_getObjc2ProtocolList, protocol_t * **const**, "__objc_protolist");
//macho section 等于__objc_protolist 协议修复列表
GETSECT(_getObjc2ProtocolRefs, protocol_t *, "__objc_protorefs");
//macho section 等于__objc_init_func __objc_init初始化方法列表
GETSECT(getLibobjcInitializers, UnsignedInitializer, "__objc_init_func");
通过machOview工具查看可执行文件如下:
macho
文件左边section
的名称对应着右边数据的列表,图中应该很清晰了。
0对1
之前说过如果rwe
赋值的话必须调用了extAllocIfNeeded
方法,extAllocIfNeeded
方法的调用时机也很多,如动态添加类数据
的时候或者分类添的数据动态添加到主类
的时候,那么我们先看看extAllocIfNeeded的
具体实现:
class_rw_ext_t *extAllocIfNeeded() {
auto v = get_ro_or_rwe();
if (fastpath(v.is())) {
return v.get(&ro_or_rw_ext);
} else {
return extAlloc(v.get(&ro_or_rw_ext));
}
}
rwe
不存在的话就会进去extAlloc
方法开辟内存空间。那么我们进去extAlloc
方法如下:
class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
runtimeLock.assertLocked();
//为rwe开辟内存空间
auto rwe = objc::zalloc();
//设置rwe的版本
rwe->version = (ro->flags & RO_META) ? 7 : 0;
//如果主类有方法,那么就会将主类的方法列表进行`attachLists`此时是 0 对 多
method_list_t *list = ro->baseMethods();
if (list) {
if (deepCopy) list = list->duplicate();
rwe->methods.attachLists(&list, 1);
}
//如果主类有属性,那么就会将主类的属性列表进行`attachLists`此时是0 对 多
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rwe->properties.attachLists(&proplist, 1);
}
//如果主类有协议,那么就会将主类的协议列表进行`attachLists`此时是0 对 多
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
//设置rwe
set_ro_or_rwe(rwe, ro);
//返回rwe
return rwe;
}
由源码可知,上面的判断只有两种情况,一种就是主类有有方法
的时候,走attachLists
方法,一种是主类中没有方法
的时候什么都不做,那么我们就根据以下两种情况进行操作分析。
主类有方法,分类有方法
创建主类XJLPerson
,以及分类XJLPerson+test
,如下:
@implementation XJLPerson
+(void)load{
NSLog(@"xjl----%s",__func__);
}
-(void)run{
NSLog(@"run"):
}
-(void)eat{
NSLog(@"eat");
}
@end
@implementation XJLPerson (test)
+(void)load{
NSLog(@"我是分类XJLPerson");
}
-(void)test{
NSLog(@"---xjl---%s",__func__);
}
-(void)sayNB{
NSLog(@"---xjl---%s",__func__);
}
@end
运行程序,下断点调试,运行objc
源码定位到attachCategories
方法,因为现在研究的是分类
:
然后进入
extAllocIfNeeded
方法中:
此时rwe还没有值所以需要开辟内存走
extAlloc
方法:
此时list有值,所以调用
attachLists
方法,进入attachLists
方法:
- 断点进入
0
对1
的判断中。 - 通过
lldb
调试,是主类的方法列表。
主类没有方法,分类有方法
将eat
和run
方法从XJPerson
类中移除,重复以上的步骤。
按照上面的调试流程,进入
extAlloc
方法,此时的list = NULL
,所以不会调用attachLists
方法,但是要想动态添加到主类0对1
这个流程流程一定要进入的,接着调试。
attachCategories
方法传进来的第二个
参数是对分类
进行了一层包装,包装成locstamped_category_t
类型 通过lldb
调试里面分类是test
分类,接着往下调试:
此时会调用
attachLists
方法,进入attachLists
方法:
总结
0对1
流程有两种情况:
- 主类
有方法
,分类有方法:以主类的方法
为基础将分类的方法加载到主类中。 - 主类
没有方法
,分类有方法:以第一个编译的分类
为基础将其它分类一起合并
,最后加载到主类中。
注意:分类是有编译顺序的!!
1对多
把sayNB
方法添加到主类XJLPerson中
,运行代码:
进入
attachLists
方法:
list
是主类
中的方法列表,addedLists
中存放着分类的指针
,从addedLists
取出分类中数据,现在看合并后的数据:
array()->lists
中存放着两个
方法列表的地址, 分类
的方法列表是是放在前面
的。
多对多
添加分类secTest
,里面声明sayHello
方法并实现,然后按照1对多
的步骤往下走,直到进入多对多
的判断里面,请看图:
此时加载的是
secTest
分类,进入attachLists
方法:
hasArray()=ture
进入到多对多
流程,addedLists
二级指针中只有一个
方法列表是secTest
分类的。
newArray
的lists
中存放3
个方法列表,分别是分类secTest
的方法列表,分别是分类test
的方法列表以及主类
的方法列表。最后编译的分类是在整个lists
的最前面。
类和分类的搭配
非懒加载类 + 非懒加载分类
非懒加载类实现load
,非懒加载分类实现load
。非懒加载类的数据加载是通过_getObjc2NonlazyClassList
方法从macho
文件获取,非懒加载分类的数据加载是通过_getObjc2NonlazyCategoryList
方法从macho
文件获取。
非懒加载类获取数据示意图:
非懒加载分类获取数据示意图:
- 非懒加载类加载流程:
map_images
-->map_images_nolock
-->_read_images
-->realizeClassWithoutSwift
-->methodizeClass
-->attachToClass
- 非懒加载分类加载流程:
load_images
-->loadAllCategories
-->load_categories_nolock
-->attachCategories
-->attachLists
日志打印示意图:
非懒加载类 + 懒加载分类
-
非懒加载类
还是走map_images
流程 -
懒加载分类
没有走attachCategories
,那么分类中方法列表
是什么时候加载的呢?
macho
中分类的列表是没有数据的,那就说明不可能是动态时
加载分类的数据,那么到底在什么时间去加载分类的数据呢?
ro
中不仅有主类的方法
,同时还有分类的方法
。ro
是在编译期
就确定的,也就是说懒加载分类
中的数据在编译期
就已经合并
到了主类
中。而且分类的数据
也是放在主类的方法
前面。
懒加载类 + 懒加载分类
同理查看日志打印:
打印信息显示加载类没有走
map_images
流程,表示没有XJLPeson
没有在非懒加载列表
中。
堆栈信息显示是通过
消息慢速查找
流程调用了realizeClassWithoutSwift
方法,是不是感觉非常熟悉!!哈哈。
查看分类的加载:
懒加载类
的流程是第一次
发消息的时会进行类的加载
,而懒加载分类
的数据是在编译时
就合并到ro
中。
这种情况流程比较复杂,因为
非懒加载分类
的个数是对整个加载流程
是有影响的。
懒加载类 + 一个非懒加载分类
查看日志情况:
这种方式和
非懒加载类 + 懒加载分类
是一样
的,非懒加载分类
强制把懒加载类
加载提前到非懒加载类
加载的时候,而且编译时
也是把懒加载类
变成了非懒加载类
,然后非懒加载的分类
的数据合并到了主类
中。
macho
文件数据展示已经很明显了,下面看下是不是在分类合并在ro
中:
·懒加载类·变成·非懒加载类·,分类的数据在编译期间合并到
ro
中。
懒加载类 + 多个非懒加载分类
查看打印信息:
打印的信息分析:
主类的加载
没有走map_images
流程,调用两次load_categories_nolock
说明是有两个分类
,但是最后没有走attachCategories
方法,而是走realizeClassWithoutSwift
加载主类
,然后调用attachCategories
流程。
根据上面的分析引申出两个问题:
- 分类加载过程中没有走
attachCategories
方法,那么它的流程是什么。 - 怎么调用到
realizeClassWithoutSwift
流程的?也会是消息的慢速转发
时候调用的么?
macho
中分类列表
和·非懒加载分类列表,有
LWB分类和
LWA分类,但是没有
非懒加载类`列表。
在load_categories_nolock
添加断点,运行源码:
cls
如果初始化则走attachCategories
方法,如果没有则走unattachedCategories.addForClass
方法。进入addForClass
方法:
void addForClass(locstamped_category_t lc, Class cls)
{
runtimeLock.assertLocked();
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: found category %c%s(%s)",
cls->isMetaClassMaybeUnrealized() ? '+' : '-',
cls->nameForLogging(), lc.cat->name);
}
// 先到哈希表中的去查找又没有lc
auto result = get().try_emplace(cls, lc);
// 如果有 判断result.second 是否有数据,没有将lc存入result.second
if (!result.second) {
result.first->second.append(lc);
}
}
- 首先到哈希表中根据
key
是cls
是查找lc
,lc
是系统底层统一封装的数据类型
。 - 如果表中有
lc
,判断lc.second
是否有数据,如果没有则赋值。
注意:现在的分类
数据直接存在哈希表
中和类现在没有关系
探究怎么去加载类
的在realizeClassWithoutSwift
添加断点,运行源码:
堆栈信息显示是
load_images
--> prepare_load_methods
-->realizeClassWithoutSwift
探究下prepare_load_methods
的具体实现:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//从macho中获取类的非懒加载列表
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//将重新映射的类添加到load列表中
schedule_class_load(remapClass(classlist[i]));
}
//从macho中获取非懒加载类的列表
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
//类的加载
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
//将分类添加到分类的load列表中
add_category_to_loadable_list(cat);
}
}
-
macho
中获取非懒加载类
的列表 - 将重新
映射
的类
添加到类的load
列表中 -
macho
中获取非懒加载分类
列表 -
类
的初始化加载 - 将
分类
添加到分类的load
列表中
进入schedule_class_load
方法查看源码:
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
//递归`cls`的父类
// Ensure superclass-first ordering
schedule_class_load(cls->getSuperclass());
//将类还有父类都添加到load表中
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED); `IMP`
}
发现add_category_to_loadable_list
和 add_class_to_loadable_list
基本一样的。
{
...//省略部分代码
//获取类中load方法的IMP
method = cls->getLoadMethod();
//类的load表
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
...
...
//获取分类中load方法的IMP
method = _category_getLoadMethod(cat);
//分类的load表
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
...
}
add_category_to_loadable_list
和add_class_to_loadable_list
保存的是封装
的类型,这个类型有两个变量一个是cls
保存类,一个是method
保存的是load
方法的IMP
。
根据日志打印发现后面的流程是:realizeClassWithoutSwift
--> attachToClass
--> attachCategories
。
在attachToClass
中断点调试:
it != map.end()
成立进入判断流程,只要哈希表中存分类的数据
条件就成立,下面探究下it
中到底在哪里存了分类的数据:
最后发现:分类的数据存在了
底层的表
中,当需要把分类的数据加载到主类的时候,就从表中获取,加载完以后清空
对应表中分类的数据
。