前言
前文iOS底层原理之OC类的加载原理(中)已经分析了类的加载,并探索了懒加载类和非懒加载类的不同流程,同时还初步确定了分类加载的两条流程,本文就来详细分析下分类加载的流程,以及分类加载和主类加载之间的联系与区别。
准备工作
- objc4-818.2源码。
- MachOView。
一: 分类的加载
前文iOS底层原理之OC类的加载原理(中)分析确定了分类的加载有两条流程:
-
map_images
-->map_images_nolock
-->_read_images
-->realizeClassWithoutSwift
-->methodizeClass
-->attachToClass
-->attachCategories
-->attachLists
。 -
load_images
-->loadAllCategories
-->load_categories_nolock
-->attachCategories
-->attachLists
。
但是这两条流程只是我们根据代码推测出来的,今天就来实际分析验证一下到底对不对。
1.1: attachCategories
两条流程最终都会调用到attachCategories
函数,加载分类的方法列表、属性列表和协议列表到rwe
。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count, int flags)
{
...
/*
* 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.
*/
// 阈值,如果分类数量超过64个,就先附加到类,然后再存剩下的
// 从后往前存,63、62、61...2、1、0
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,创建完就会将主类的方法、属性、协议添加进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) {
// 如果分类个数超过64个,就把这64个分类的方法列表先排序再附加到主类
// count设为0,重新存剩下的
if (mcount == ATTACH_BUFSIZ) {
// 方法列表修正且排序(按sel地址排序)
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
// 如果 mcount 为 0,那么 ATTACH_BUFSIZ - ++mcount 为 63
// 存放下标为63 ~ 0,从后往前存
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;
}
}
// 分类总数或剩下的没有超过64个,下面一起附加到类
if (mcount > 0) {
// 在将分类方法添加到主类之前将方法进行排序
// 地址偏移还记得d+n吗 mlists = d n = ATTACH_BUFSIZ - mcount
// mlists + ATTACH_BUFSIZ - mcount = d + n
// 此时的mlists + ATTACH_BUFSIZ - mcount 是一个二维指针,里面存放的是方法列表的首地址
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
// 将分类方法添加到rwe的methods中
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中
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// 将分类协议添加到rwe的protocols中
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
- 获取或创建
rwe
,创建完就会将主类的方法、属性、协议添加进rwe
。 - 遍历所有分类,获取方法列表,属性列表,协议列表,存储到对应的总列表中,总列表容量
64
,从后往前存。 - 如果分类超过
64
个,就调用attachLists
函数先将已获取的方法列表,属性列表,协议列表添加到rwe
中,再存剩下的。 - 遍历完所有分类后, 分类总数或剩下的没有超过
64
个,调用attachLists
函数将获取的方法列表,属性列表,协议列表添加到rwe
中。
1.2: attachLists
attachCategories
函数获取分类的数据,然后调用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;
// 新的newCount = 原有的count + 新增的count
uint32_t newCount = oldCount + addedCount;
// 根据`newCount`开辟内存,类型是 array_t, array()->lists是一个二维数组
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中
// 且是从开始的位置开始存放
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`结构体类型的数组
// `array_t`的lists存放的是各个分类数组的地址指针
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;
// 将原来的list放在数组的末尾
if (oldList) array()->lists[addedCount] = oldList;
// 遍历二维指针`addedLists`中的list将其存放在array()->lists中
// 且是从开始的位置开始存放
// 因为 attachCategories 函数中已经是倒序插入的,现在顺序插入就可以了
// 越后面加载的分类,附加到主类时越在前面
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
attachLists
函数分为三种情况:
0 lists -> 1 list
- 将
addedLists[0]
赋值给list
。
如果主类中有方法,第一次进来是主类的方法列表;如果主类中没有方法,则进来的就是分类的方法列表。属性,协议也是如此。
1 list -> many lists
- 计算原有的总数,原有
list
只有一个,所以不是1
就是0
。 - 新的总数等于原有总数加上新增总数,
newCount = oldCount + addedCount
。 - 根据新的总数开辟相应的内存,数据类型为
array_t
,并设置数组setArray
。 - 设置数组的总数等于
newCount
。 - 将原有的
list
放入数组的末尾,之前只有一个,所以不需要遍历。 - 遍历获得新增数组中的
list
,从0
号位开始存入数组的lists
中。
many lists -> many lists
(array()
存在才会进入)
- 计算原有数组的总数。
- 新的总数等于原有总数加上新增总数,
newCount = oldCount + addedCount
。 - 根据新的总数开辟相应的内存,数据类型为
array_t
。 - 设置新数组的总数等于
newCount
。 - 设置原有数据的总数等于
newCount
。 - 遍历获得原有数组中的
list
,存入新数组的lists
中,且是从最后开始取和存。 - 遍历获得新增数组中的
list
,从0
号位开始存入新数组的lists
中。 - 释放原有数组。
- 设置新的数组。
注意:
List* const * addedLists
是二维指针
。就像XJPerson *p = [XJPerson alloc]
,p
是一维指针
,&p
就是二维指针。
array_t
结构体相关探索:
class list_array_tt {
struct array_t {
uint32_t count;
Ptr<List> lists[0];
static size_t byteSize(uint32_t count) {
return sizeof(array_t) + count*sizeof(lists[0]);
}
size_t byteSize() {
return byteSize(count);
}
};
protected:
...
private:
union {
Ptr<List> list;
uintptr_t arrayAndFlag;
};
bool hasArray() const {
return arrayAndFlag & 1;
}
array_t *array() const {
return (array_t *)(arrayAndFlag & ~1);
}
void setArray(array_t *array) {
arrayAndFlag = (uintptr_t)array | 1;
}
void validate() {
for (auto cursor = beginLists(), end = endLists(); cursor != end; cursor++)
cursor->validate();
}
...
}
setArray
函数arrayAndFlag = (uintptr_t)array | 1
,arrayAndFlag
的第0
位一定是1
。hasArray
函数arrayAndFlag & 1
,如果arrayAndFlag
的第0
位是1
,就返回YES
,反之返回NO
。- 所以只要调用了
setArray
函数之后,释放之前,hasArray
函数返回的就是YES
。 - 在联合体里面
list
和arrayAndFlag
互斥,也就是说有list
就无array
,有array
就无list
。
1.2.1: attachLists
流程图:
1.3: 实例验证attachCategories
和attachLists
前面分析了attachCategories
和attachLists
的流程,下面通过实例来进行相关验证(以方法为例)。
创建主类XJPerson
和分类XJPerson+XJA
、XJPerson+XJB
。
@implementation XJPerson
+ (void)load
{
NSLog(@"__%s__", __func__);
}
- (void)instanceMethod1
{
NSLog(@"%s", __func__);
}
- (void)instanceMethod2
{
NSLog(@"%s", __func__);
}
@end
@implementation XJPerson (XJA)
+ (void)load
{
NSLog(@"%s", __func__);
}
- (void)xja_instanceMethod1
{
NSLog(@"%s", __func__);
}
- (void)xja_instanceMethod2
{
NSLog(@"%s", __func__);
}
@end
@implementation XJPerson (XJB)
+ (void)load
{
NSLog(@"%s", __func__);
}
- (void)xjb_instanceMethod1
{
NSLog(@"%s", __func__);
}
- (void)xjb_instanceMethod2
{
NSLog(@"%s", __func__);
}
@end
1.3.1: attachCategories
验证
添加调试代码,运行源码,定位到attachCategories
函数进行调试。
- 加载的是
XJA
分类。
接着输出变量mlists
查看数据。
mlists
最后一个存储的就是XJA
分类的方法列表的地址。
mlists
是method_list_t *[]
类型(指针数组),里面存储的是method_list_t *
类型数据。
mlists
首地址是一个二维指针。mlists + ATTACH_BUFSIZ - mcount
就是地址平移,mlists
是首地址,ATTACH_BUFSIZ - mcount
是具体的第几个位置,获取到的就是一个二维指针。
1.3.2: attachLists
验证
验证方式采用动态源码调试
和macho文件(可执行文件)
进行相互验证,所以先介绍下源码是怎么读取macho
文件的,源码中遇到了_getObjc2NonlazyClassList
和_getObjc2NonlazyCategoryList
等相关方法,进入查看具体实现。
#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_protorefs 协议修复列表
GETSECT(_getObjc2ProtocolRefs, protocol_t *, "__objc_protorefs");
//macho section 等于__objc_init_func __objc_init初始化方法列表
GETSECT(getLibobjcInitializers, UnsignedInitializer, "__objc_init_func");
__objc_nlclslist
等对应macho
文件中的section
名字,右边就是对应section
的数据。
1.3.2.1: 0 lists -> 1 list
前文分析过extAlloc
函数在创建完rwe
之后,如果主类有方法,就会调用attachLists
函数将其添加到rwe
对应的列表中(属性和协议也是如此,本文以方法为例),如果没有就什么也不做。那么此时就会出现主类有方法和主类无方法的两种情况(默认分类都有方法)。
1.3.2.1.1: 主类有方法
运行源码定位到attachCategories
函数获取rwe
的相关位置。
进入extAllocIfNeeded
函数获取或创建rwe
。
因为此时还没有rwe
,就会进入extAlloc
函数创建rwe
。
- 此时主类有方法,
list
有值,所以调用attachLists
函数将主类方法添加到rwe
相应的列表中。
- 此时
rwe
刚刚创建,list
和array
都为空,所以进入了0 lists -> 1 list
的分支。 lldb
调试显示此时添加的是主类的方法列表。
1.3.2.1.2: 主类无方法
注释掉主类的方法,重复上面的调试步骤,进入extAlloc
函数。
- 此时主类无方法,
list
为NULL
,所以不会调用attachLists
函数。
继续调试,回到attachCategories
函数,lldb
查看cats_list
相关信息。
- 此时加载的是被包装成
locstamped_category_t *
类型的分类XJA
。
继续往下调试。
- 分类数据读取完之后调用
attachLists
函数添加到rwe
中(此文只分析方法,属性、协议类似)。
- 主类无方法,此时
list
和array
都为空,所以分类加载进入了0 lists -> 1 list
的分支。 lldb
调试显示此时添加的是分类XJA
的方法列表。
现在有
XJA
和XJB
两个分类,为什么先加载的是XJA
而不是XJB
呢?这是由分类文件的编译顺序决定的,按编译的先后顺序进行加载。
如果将分类
XJB
放到分类XJA
的前面,那么XJB
就会先编译先加载。
1.3.2.1.3: 0 lists -> 1 list
总结
0 lists -> 1 list
分支分为两种情况(默认分类有方法):
- 主类有方法:创建完
rwe
之后就将主类方法添加进去。 - 主类无方法:第一个添加进
rwe
的是第一个加载的分类。
1.3.2.2: 1 list -> many lists
重新将主类方法添加回去,并将分类XJB
放到XJA
前面(验证加载顺序),运行源码。
- 将分类
XJB
放到XJA
前面,就会先加载XJB
。 - 读取完分类
XJB
数据之后调用attachLists
函数添加到rwe
中(此文只分析方法,属性、协议类似)。
list
是主类方法列表指针,addedLists
中存放的是分类XJB
方法列表指针。
合并完之后继续查看。
array()->lists
中存放着两个方法列表的地址, 分类的方法列表地址放在前面。
1.3.2.3: many lists -> many lists
继续调试加载分类XJA
,进入many lists -> many lists
分支。
- 读取完分类
XJA
数据之后调用attachLists
函数添加到rwe
中(此文只分析方法,属性、协议类似)。
addedLists
中存放的是分类XJA
方法列表指针。
合并完之后继续查看。
- 合并完之后
newArray->lists
中存放了3
个方法列表地址,从前往后依次是分类XJA
、分类XJB
、主类
。主类在lists
最后面,最后编译的分类在lists
最前面。
二: 类与分类搭配加载情况
前面分析过类的加载根据是否实现+load
方法分为懒加载类和非懒加载类,那么分类的加载是否与+load
方法有关呢,同时类与分类搭配加载又会有那些情况呢?下面就通过几种搭配方式来探索一下:
- 类和分类都实现
+load
方法。 - 类实现
+load
方法,分类不实现。 - 类和分类都不实现
+load
方法。 - 类不实现,分类实现
+load
方法。 - 多分类
为了方便跟踪,在前面分析出的两条路线的关键函数中添加调试代码和断点。
2.1: 类和分类都实现+load
方法
非懒加载类的数据加载是通过_getObjc2NonlazyClassList
函数从macho
文件获取,非懒加载分类的数据加载是通过_getObjc2NonlazyCategoryList
函数从macho
文件获取。
非懒加载类读取数据示意图:
非懒加载分类读取数据示意图:
- 非懒加载类加载流程:
map_images -> map_images_nolock -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
。 - 非懒加载分类加载流程:
load_images -> loadAllCategories -> load_categories_nolock -> attachCategories -> attachLists
。
日志打印流程:
此时类为非懒加载类,在realizeClassWithoutSwift
函数中查看ro
数据。
- 此时只有主类的方法,没有分类的方法。
主类加载完之后,调用load_images
函数。
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// didInitialAttachCategories 控制只来一次。
// didCallDyldNotifyRegister 在 _objc_init 中注册完回调之后赋值为true
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
// 加载所有分类
loadAllCategories();
}
……
}
didInitialAttachCategories
控制只执行一次loadAllCategories
(因为load_images
会执行多次),didCallDyldNotifyRegister
在_objc_init
函数中注册完回调之后赋值为true
。
2.1.1: load_categories_nolock
loadAllCategories
函数中根据header_info
循环调用load_categories_nolock
函数,核心代码如下:
static void load_categories_nolock(header_info *hi) {
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
size_t count;
// 声明并实现函数 processCatlist
auto processCatlist = [&](category_t * const *catlist) {
for (unsigned i = 0; i < count; i++) { // 分类数量
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
// 将cat和hi包装成 locstamped_category_t
locstamped_category_t lc{cat, hi};
……
// Process this category.
if (cls->isStubClass()) {
……
} else {
// 实例方法/协议/实例属性
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
if (cls->isRealized()) { // 非懒加载类
attachCategories(cls, &lc, 1, ATTACH_EXISTING);
} else { // 懒加载类
objc::unattachedCategories.addForClass(lc, cls);
}
}
// 类方法/协议/类属性
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
if (cls->ISA()->isRealized()) { // 非懒加载类
attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
} else { // 懒加载类
objc::unattachedCategories.addForClass(lc, cls->ISA());
}
}
}
}
};
// 调用 processCatlist
// 加载分类 __objc_catlist,count从macho中读取。
processCatlist(hi->catlist(&count));
//__objc_catlist2
processCatlist(hi->catlist2(&count));
}
- 声明并实现
processCatlist
函数。 - 调用
processCatlist
函数,读取macho
文件名为__objc_catlist
与__objc_catlist2
的section
,获取分类信息(有共享缓存的情况下才会生成__objc_catlist2
)。 processCatlist
函数根据读取的分类信息循环处理分类。- 此时非懒加载分类是一个一个加载的,所以
attachCategories
函数传值的count
都是1
。
lldb
查看分类信息:
attachCategories
函数和attachLists
函数在1.1
和1.2
小节已经分析过了,此处就不再赘述了。
更改实现+load
方法的分类数量,会发现只要有一个分类实现了+load
方法,所有分类加载都走非懒加载分类的加载流程,而不会合并到主类ro
中。
结论:类实现了+load
方法,分类只要有一个实现+load
方法,所有分类都不合并到主类的ro
中,而是走非懒加载分类流程合并到rwe
中。
2.2: 类实现+load
方法,分类不实现
- 非懒加载类加载流程:
map_images -> map_images_nolock -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
。
日志打印流程:
- 非懒加载类还是走的
map_images
流程。 - 懒加载分类没有走
attachCategories
流程。
那么分类数据是什么时候加载的呢?
macho
文件中分类列表是没有数据的,说明分类数据不是动态时加载的。
不是动态时加载,最有可能就是在编译器,那么就在realizeClassWithoutSwift
函数加载类的时候,获取ro
数据,看看有没有分类相关的数据。
ro
中不仅有主类的方法,还有分类的方法。
ro
是在编译期就确定的,也就是说懒加载分类中的数据在编译期就已经合并到了主类中。而且分类的方法也是放在主类的方法前面。
2.3: 类和分类都不实现+load
方法
日志打印流程:
打印信息显示类加载流程和分类加载流程都没有走,查看macho
文件。
macho
文件中没有非懒加载类和非懒加载分类的section
,分类列表也没有数据。
在main
函数里实例化XJPerson
并调用实例方法,调试进入加载类的realizeClassWithoutSwift
函数中(消息慢速查找流程进入),查看函数调用栈。
- 函数调用栈显示是
消息慢速查找
流程调用了realizeClassWithoutSwift
函数,这里就与前面分析过的消息慢速查找流程关联起来了。
lldb
查看ro
数据:
- 根据
lldb
输出ro
数据,可以发现分类的数据也是在编译时就合并到ro
中了。
懒加载类的加载流程是推迟到第一次消息发送的时候了,而懒加载分类的数据是在编译时就合并到
ro
中了。
2.4: 类不实现,分类实现+load
方法
这种情况比较复杂,因为非懒加载类的个数会对整个加载流程有影响。
2.4.1: 类不实现,一个分类实现+load
方法
运行源码,查看日志打印流程:
- 这种情况的流程和
类实现+load方法,分类不实现
是一样的。
非懒加载分类强制把懒加载类变成非懒加载类了(类被迫营业了),而且非懒加载分类的数据也合并到了主类中。
XJPerson
类被强制变成非懒加载类了。
- 分类列表没有数据,已经在编译时合并到
ro
中了。
运行源码,断点停在realizeClassWithoutSwift
函数后,lldb
查看ro
数据:
- 懒加载类被强制变成非懒加载类了,而且分类的数据也在编译期间就合并到
ro
中了。
2.4.2: 类不实现,多个分类实现+load
方法
重新添加分类XJB
,运行源码,查看日志打印流程:
- 主类加载没有走
map_images
流程。 - 调用两次
load_categories_nolock
函数加载两个分类后,没有调用attachCategories
函数,而是调用了realizeClassWithoutSwift
函数加载主类,然后再调用attachCategories
函数。
那么现在就有了两个问题:
- 两个分类加载完之后为什么没有走
attachCategories
函数? - 怎么调用到
realizeClassWithoutSwift
函数的?
macho
文件里分类列表和非懒加载分类列表有分类XJA
和XJB
的数据,但是没有非懒加载类列表。
打开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);
}
}
- 首先到哈希表中根据
cls
为key
去查找lc
,没有就插入。 - 判断
lc.second
是否有数据,如果没有则赋值。
此时分类数据只是以
cls
为key
存储在哈希表中,还没有加载到rwe
中。
接下来探索类是怎么加载的,打开realizeClassWithoutSwift
函数里调试代码处的断点,继续往下调试。
- 函数调用栈显示在调用流程为:
load_images -> prepare_load_methods -> realizeClassWithoutSwift
。
在函数调用栈选中load_images
看看prepare_load_methods
是怎么调用的。
loadAllCategories
函数走完之后,调用hasLoadMethods
函数判断是否有+load
方法,没有就返回,有就调用prepare_load_methods
函数。
bool hasLoadMethods(const headerType *mhdr)
{
size_t count;
if (_getObjc2NonlazyClassList(mhdr, &count) && count > 0) return true;
if (_getObjc2NonlazyCategoryList(mhdr, &count) && count > 0) return true;
return false;
}
- 调用
_getObjc2NonlazyClassList
函数读取macho
文件名为__objc_nlclslist
的section
的数据,如果有,返回true
。 - 调用
_getObjc2NonlazyCategoryList
函数读取macho
文件名为__objc_nlcatlist
的section
的数据,如果有,返回true
。 - 都没有就返回
false
。
此时分类XJA
和XJB
都实现了+load
方法,所以返回true
,接着调用了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++) {
// 将重新映射的类添加到 loadable_classes 列表中
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());
// 将分类添加到分类的loadable_categories列表中
add_category_to_loadable_list(cat);
}
}
- 从
macho
中获取非懒加载类列表。 - 调用
schedule_class_load
函数,将重新映射的类添加到loadable_classes
列表中。 - 从
macho
中获取非懒加载分类列表。 - 重新映射分类的类。
- 调用
realizeClassWithoutSwift
函数加载类。 - 调用
add_category_to_loadable_list
函数,将分类添加到分类的loadable_categories
列表中。
查看下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_class_to_loadable_list
函数。
add_class_to_loadable_list
函数和add_category_to_loadable_list
函数逻辑差不多。
static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;
struct loadable_class {
Class cls; // may be nil
IMP method;
};
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
// 获取load方法
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
...
// 扩容,第一次为16
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
存储
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;
struct loadable_category {
Category cat; // may be nil
IMP method;
};
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
...
// 扩容,第一次为16
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
// 存储
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
- 将类或者分类和他们的
+load
方法的IMP
一起存入列表中。
继续调试,realizeClassWithoutSwift
后面的流程为realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories -> attachLists
。
void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
(flags & ATTACH_METACLASS) ||
(flags & ATTACH_CLASS_AND_METACLASS));
auto &map = get();
// 以previously(cls)为key去哈希表中查找在addForClass函数中插入的分类数据
auto it = map.find(previously);
// 如果it和表中的最后一个数据不相等进入判断,最后一个算是标识位
if (it != map.end()) {
// 获取分类的数据list的地址
category_list &list = it->second;
if (flags & ATTACH_CLASS_AND_METACLASS) {
// 给元类类加载分类数据
int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
} else {
// 给类加载分类数据
attachCategories(cls, list.array(), list.count(), flags);
}
// 擦除列表中cls对应的分类数据
map.erase(it);
}
}
- 以
previously(cls)
为key
去哈希表中查找在addForClass
函数中插入的分类数据。 - 获取到的数据和表中
end
标记不相等,就调用attachCategories
函数(此时传入的分类数量cats_count
为所有分类的数量),然后擦除哈希表中cls
对应的分类数据。
lldb
调试查看分类数据:
it != map.end()
条件成立,调用attachCategories
函数。
调用流程:load_images -> loadAllCategories -> load_categories_nolock -> addForClass -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories -> attachLists
。
结论:主类不实现+load
方法,至少2
个分类实现+load
方法,则在load_images
函数的loadAllCategories
流程走完之后,接着走load_images
函数里的prepare_load_methods
流程强制让主类加载,然后将分类加入到rwe
中。
2.5: 小结
-
类实现
+load
方法(非懒加载类):-
至少一个分类实现
+load
方法,会在load_images
过程中通过loadAllCategories
将所有分类数据加入rwe
中,不会合并进主类ro
。 -
所有分类不实现
load
会将分类的方法合并到类的ro
中。
-
-
类不实现
+load
方法:-
只有一个分类实现
+load
方法,分类方法会被合并到类的ro
中(由于合并了ro
,类被强制变成非懒加载类了)。 -
至少两个分类实现
+load
方法。分类方法不会被合并进主类ro
中,在load_images
函数的loadAllCategories
流程走完之后,接着走load_images
函数里的prepare_load_methods
流程强制让主类加载,然后将分类加入到rwe
中。(由于没有合并ro
,类本身是懒加载类。分类导致它被加载)。
-
-
类和分类都不实现
+load
方法,所有分类方法会被合并进类的ro
中,在第一次消息发送的时候加载(类是懒加载类)。
类本身是非懒加载类或者子类是非懒加载类是在
map_images
过程中实例化的。类本身是懒加载类,由于自身或者子类的非懒加载分类导致的类被实例化是在
load_images
函数里的prepare_load_methods
流程中的。(ro
合并的情况如果分类有+load
方法会导致类变为非懒加载类)。本质上只有类自身实现
+load
方法才是非懒加载类。其它情况都是被迫,本质上不属于非懒加载类。类与分类方法列表会不会合并,取决于
+load
方法的总个数。只有一个或者没有则会合并,否则不合并。空的分类不会被加载。
⚠️为什么类和分类实现
+load
方法加起来大于等于2个之后,分类就不会在编译时合并到ro
中了呢?因为所有
+load
方法在load_images
函数中都是要被调用的,每个类或分类的+load
方法可能都做了自己相应的操作,不能暴力的将它们合并到一起。所以实际开发中,尽量少实现+load
方法。实现过多+load
方法需要单独调用,会导致分类不会合并到ro
,而且类和分类的加载流程也提前到了程序启动阶段,会导致程序启动变慢。
三: 类中同名方法的查找
3.1: 方法查找流程分析
消息慢速查找流程lookUpImpOrForward
函数中调用getMethodNoSuper_nolock
函数查找方法。
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
...
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
...
} else {
// curClass method list.
// 在类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
...
}
...
}
...
}
3.1.1: getMethodNoSuper_nolock
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
……
// 获取methods
auto const methods = cls->data()->methods();
// 循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
for (auto mlists = methods.beginLists(),
end = methods.endLists();
// 没到最后一个,就地址平移 1
mlists != end;
++mlists)
{
//查找方法
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
- 获取
cls
的methods
(二维指针)。 - 遍历
methods
,获取里面存储的method_list_t *
类型的方法列表mlists
,调用search_method_list_inline
函数。 - 从
beginLists
开始遍历,相当于从数组最开始遍历。也就是后加载的分类会被先查找。
const Ptr<List>* beginLists() const {
if (hasArray()) {
// 这里不是返回的array,直接返回的lists数组首地址。
return array()->lists;
} else {
// 这里进行了&地址操作,相当于包装了一层,* -> **。
return &list;
}
}
const Ptr<List>* endLists() const {
if (hasArray()) {
return array()->lists + array()->count;
} else if (list) {
return &list + 1;
} else {
return &list;
}
}
- 直接判断是否存在
hasArray
从而返回array()->lists
或&list
。 &list
相当于直接包装了一层(* -> **
),所以在for
循环中就是同一个数据类型的调用了。
3.1.2: 方法列表结构验证:
3.1.2.1: ro
合并(此处采用2.2
小节)的情况:
- 没有
array
,&list
包装一层(* -> **
),以便在getMethodNoSuper_nolock
函数里的for
循环中数据类型统一。 ro
合并了,所以只有list
,没有array
。
ro
合并了,所有方法都在list
中。
3.1.2.2: ro
不合并(此处采用2.1
小节)的情况:
ro
不合并,所以有array
,里面存放类、分类的方法列表指针。
- 分类
XJB
后加载,放在分类XJA
前面,先查找。最后加载的分类放在array
最前面,最先查找,主类如果有数据,放在最后面。
3.1.3: findMethodInSortedMethodList
getMethodNoSuper_nolock
之后的调用流程:
getMethodNoSuper_nolock -> search_method_list_inline -> findMethodInSortedMethodList -> findMethodInSortedMethodList
。
findMethodInSortedMethodList
函数的核心是使用二分查找法查找方法。
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin(); // 第一个method的位置
auto base = first;
decltype(first) probe;
// 将key直接转换成uintptr_t值,因为修复过后的method_list_t中的元素是排过序的
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// count = 数组的个数,count >> 1 相当于 count / 2 取整
// count >>= 1 = (count = count >> 1) = (count = count / 2)
/*
案例一:例如 count = 8 需要查找的sel的index为2
1(第一次).count = 8
2(第二次).count = (8 >>= 1) = 4
*/
/*
案例二:例如 count = 8 需要查找的sel的index为7
1(第一次).count = 8
2(第二次).count = (7 >>= 1) = 3(下面count--了)
3(第三次).count = (2 >>= 1) = 1(下面count--了)
*/
for (count = list->count; count != 0; count >>= 1) {
// 内存平移,获取探查值(中间值)
/*
案例一:
1.probe = 首地址(0) + (count / 2) = 0 + 4
2.prebe = 0 + (4 / 2) = 2
*/
/*
案例二:
1.probe = 首地址(0) + (count / 2) = 0 + 4
2.probe = 5 + (3 / 2) = 6
3.probe = 7 + (1 / 2) = 7
*/
probe = base + (count >> 1);
// 获取探查的sel的uintptr_t值
uintptr_t probeValue = (uintptr_t)getName(probe);
/*
案例一:
1.key = 2,probe = 4,不相等
2.key = 2,prebe = 2,相等,返回method_t *
*/
/*
案例二:
1.key = 7,probe = 4,不相等
2.key = 7,prebe = 6,不相等
3.key = 7,probe = 7,相等,返回method_t *
*/
if (keyValue == probeValue) { // 如果目标sel的uintptr_t值和探查sel的uintptr_t值匹配成功
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 探查值不是第一个 && 上一个sel的uintptr_t值也等于keyValue
// 说明有分类同名方法覆盖,获取分类的方法,多个分类最后编译的在最前面
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
// 返回
return &*probe;
}
// 如果keyValue > 探查值
/*
案例一:
1. 2 不大于 4,不进入,继续循环
*/
/*
案例二:
1. 7 大于 4,进入
2. 7 大于 6,进入
*/
if (keyValue > probeValue) {
/*
案例二:
1.base = 4 + 1 = 5,count-- = 8-- = 7
2.base = 6 + 1 = 7,count-- = 3-- = 2
*/
base = probe + 1;
count--;
}
}
return nil; // 查询完没找到就返回nil
}
while (probe > first && keyValue == (uintptr_t)getName((probe - 1)))
逻辑就是为了分类和主类有同名方法,在编译阶段合并到ro
中,获取的时候需要获取到最后编译的分类而做的处理。
这就是分类会覆盖主类同名方法的原因。
3.1.4: 二分查找验证
在主类XJPerson
和分类XJA
和XJB
中都添加并实现方法- (void)sameMethod
。
3.1.4.1: ro
合并的情况
此处依然采用2.2
小节的情况,仅主类实现+load
方法,并且在main
函数中调用sameMethod
方法。
运行源码,首先在类加载过程的realizeClassWithoutSwift
函数验证ro
数据:
- 主类和分类的方法都合并到
ro
中了。
但是在方法查找的时候同名方法应该是放在一起的,目前还没有在一起,在查看下修正、排序前后的对比:
sel
修正前后:
方法排序前后:
- 经过修正排序后,同名方法确实放到了一起。
这也就是为什么在二分查找函数findMethodInSortedMethodList
内部会在找到方法后继续往前找的原因了。
那么按照前面分析的,方法查找的时候最后面加载的分类XJB
的sameMethod
方法就应该在最前面。
- 在合并
ro
的情况下,类和分类的同名方法,类的放在最后面,越后加载的分类放在越前面。
3.1.4.2: ro
不合并的情况
此处依然采用2.1
小节的情况,主类和任一分类实现+load
方法,并且在main
函数中调用sameMethod
方法。
- 由于不合并
ro
,现在的方法列表就有array
,是一个二维指针
,所以首先查找的是分类XJB
的方法列表。
继续运行到二分查找函数findMethodInSortedMethodList
验证。
- 找到对应的
sameMethod
方法就返回了,没有走while
流程。
四:initialize
流程分析
经过类的加载原理这三篇文章的分析,我们知道了+load
方法是在load_images
流程调用的,那么+initialize
方法是在什么时候调用的呢?下面就一起来探索下。
我们都知道+initialize
方法是在第一次调用方法之前调用的,那么就新建一个非源码工程,并添加XJPerson
类,同时给XJPerson
类添加+initialize
方法,然后在main
函数实例化XJPerson
,并调用方法。
+initialize
方法是被消息慢速查找lookUpImpOrForward
流程调用的。
在+initialize
方法被调用前一步的汇编里发现了与objc
源码相关的内容。
源码里全局搜索CALLING_SOME_+initialize_METHOD
字段,定位到了callInitialize
函数。
接着全局搜索callInitialize
函数,发现只有initializeNonMetaClass
函数调用了。
继续全局搜索initializeNonMetaClass
函数,找到调用者initializeAndMaybeRelock
函数。
继续搜索initializeAndMaybeRelock
函数,找到调用者initializeAndLeaveLocked
函数。
继续搜索initializeAndLeaveLocked
函数,找到调用者realizeAndInitializeIfNeeded_locked
函数。
继续搜索realizeAndInitializeIfNeeded_locked
函数,找到调用者lookUpImpOrForward
函数。
这样就一直找到了消息慢速查找
流程了。说明了+initialize
方法,确实是在第一次消息发送的时候调用的。
+initialize
方法调用流程:
lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> initializeAndLeaveLocked -> initializeAndMaybeRelock -> initializeNonMetaClass -> callInitialize -> initialize
。
+initialize
方法在第一次消息发送的时候才调用,所以并不会影响类和分类的加载情况。
五: 总结
-
类与分类的合并:取决于
+load
方法的实现总个数是否存在多个(+initialize
不影响)(因为所有+load
方法在load_images
函数中都是要被调用的,每个类或分类的+load
方法可能都做了自己相应的操作,不能暴力的将它们合并到一起)。-
合并:
0
/1
个+load
方法实现,分类方法列表会被合并进主类ro
中,后编译的分类同名方法在前。0
个+load
方法实现,类为懒加载类。1
个+load
方法实现,类为非懒加载类(由于合并,谁实现load
已经无所谓了)。
-
不合并:
2
个及以上+load
。分类的方法列表会被加载到rwe
中。- 主类实现
+load
:load_images
过程中通过loadAllCategories
将分类数据加载到rwe
中。 - 主类没有实现
+load
:load_images
过程中通过prepare_load_methods
流程最终实例化类和加载分类方法到rwe
中。
- 主类实现
-
-
类的实例化(加载)
- 分类或者子类的分类(
+load
)导致类被实例化是在load_images
过程中。类本身是懒加载类,被迫实例化。 - 子类或者类的
+load
方法导致类被实例化是在map_images
中。 - 其它情况类为懒加载类,在消息慢速查找
lookUpImpOrForward
流程中实例化。
- 分类或者子类的分类(
-
类的懒加载&非懒加载
-
懒加载:类、子类、分类、子类分类没有实现
+load
方法的情况,类为懒加载类。 -
非懒加载
-
完全非懒加载:类实现
+load
方法,此时类为非懒加载类 -
依赖非懒加载:类本身没有实现
+load
方法。- 子类实现
+load
方法:由于递归实例化导致父类被实例化,父类本质上还是懒加载类,在这里相当于非懒加载类。 - 分类/子类分类实现
+load
方法:在prepare_load_methods
中由于分类是非懒加载分类导致类被实例化,也相当于类变成了非懒加载类。
- 子类实现
-
-
类和分类加载流程: