前言
前面已经分析完了类的加载流程以及引出了分类的加载,并且得出了分类加载的两条路线。那么这篇文章还是继续往下分析分类是如何加载的以及分类和主类之间加载不同的情况。
分类加载的两条线路:
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<List> 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<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(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<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
} else {
return extAlloc(v.get<const class_ro_t *>(&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<class_rw_ext_t>();
//设置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中到底在哪里存了分类的数据:
最后发现:分类的数据存在了
底层的表中,当需要把分类的数据加载到主类的时候,就从表中获取,加载完以后清空对应表中分类的数据。










