根据上篇文章的分析,分类的加载有两条线路:
methodizeClass -> attachToClass -> attachCategories
load_images -> loadAllCategories -> load_categories_nolock -> attachCategories
attachCategories
最终调用到了attachList
。
一、attachList方法列表处理
既然最终分类的处理调用到了attachList
,那么先看下它的实现逻辑。
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
//之前的oldCount
uint32_t oldCount = array()->count;
//newCount 为新加的与之前的和。
uint32_t newCount = oldCount + addedCount;
//开辟空间
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
//设置新数组容量
newArray->count = newCount;
array()->count = newCount;
//将旧的数组插入新的数组中。从index addedCount ~ oldcount-1。相当于插入到后面
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
//将新加入的addedLists依次加入新数组,index从0 ~ addedCount-1。
for (unsigned i = 0; i < addedCount; i++)
newArray->lists[i] = addedLists[i];
//执行完上面的操作相当于将新插入的数组放入旧的数组前面。
//释放旧数组
free(array());
//设置新数组
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) {//本类没有方法的时候走这个逻辑
// 0 lists -> 1 list
//一维数组
list = addedLists[0];
validate();
}
else {
// 1 list -> many lists
Ptr oldList = list;
//有旧列表,oldCount为1,否则为0。
uint32_t oldCount = oldList ? 1 : 0;
//新count为oldCount + addedCount
uint32_t newCount = oldCount + addedCount;
//开辟新空间,设置新数组
setArray((array_t *)malloc(array_t::byteSize(newCount)));
//设置容量
array()->count = newCount;
//如果有旧列表,array[endIndex] 最后一个元素为 oldList指针。
if (oldList) array()->lists[addedCount] = oldList;
//循环将新加入的放到list前面。从前往后一个一个放。由于addedLists为**类型,所以这里也是地址。
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
-
0 lists ->1 list
:相当于直接赋值给了list
,在本类没有方法并且只有一个分类的时候。内部存储的相当于是元素。(分类多个,但是单个加载的时候第一个也会进入。) -
1 list -> many lists
:相当于两层结构,新加入的addedLists
是一个**
结构,分别加入新的数组的前面,如果有旧的列表(主类的方法列表),旧列表作为一个指针放在最后面。这个数组全部都是指针。 -
2 many lists -> many lists
:首先将旧的数组加入新数组的末尾,接着将新加入的addedLists
,放在新数组的前面。整个数组存储的是指针。
假设oldList
为5
,addedLists
为2
。那么在进行第0
和1
步合并后内存中布局如下:
如果这个时候再继续加入addedLists
这次addedCount
为3
,则有以下布局:
二、分类与类搭配加载情况
由于类的加载与load
方法有关,那么分类的加载与load
是否有关系呢?那么有4
种方式:
- 类和分类都实现
load
方法。 - 类实现
load
,分类不实现。 - 类不实现,分类实现
load
。 - 类和分类都不实现。
为了方便跟踪,对文中开始说的两条线路关键方法否打上调试断点。
2.1 类和分类都实现load
调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_images -> loadAllCategories-> load_categories_nolock -> attachCategories
这个时候类为非懒加载类,在realizeClassWithoutSwift
中查看ro
的数据,这个时候还没有分类的方法:
load_images
的最开始的地方直接调用了loadAllCategories
:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
//didInitialAttachCategories 控制只来一次。 didCallDyldNotifyRegister 在 _objc_init 中赋值
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
//加载所有分类
loadAllCategories();
}
……
}
- 控制条件是
didInitialAttachCategories
只执行一次(由于load_images
会执行多次),didCallDyldNotifyRegister
在_objc_init
中注册完回调后设置。
2.1.1 loadAllCategories
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
调用了两次。读取的是__objc_catlist
与__objc_catlist2
,也就是分类数据的获取。暂不清楚__objc_catlist2
会在什么情况下生成。 -
count
是分类的个数。 -
attachCategories
通过cls
与flags
参数区分类和元类。cats_count
参数写死的是1
。locstamped_category_t
是lc{cat, hi}
分类和header_info
组成。
断点确定分类信息:
2.1.2 attachCategories
为了方便分析,去掉了属性和协议相关内容,只保留方法:
//cls :类/元类 cats_list:分类与header_info cats_count:1 flags: ATTACH_EXISTING | ATTACH_METACLASS
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();
//cats_count 分类数量,这里写死的是1。
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
//分类中方法,通过 isMeta 控制是否类方法。
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {//最大值为64,也就是说64个分类。64个分类后直接存储,之后count从0重新开始计数。
//排序
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
rwe->methods.attachLists(mlists, mcount);
//为64的时候 mcount 初始化为0。
mcount = 0;
}
//mcount在这里变化 mlists中从后往前存分类方法列表,也就是后加载的分类在前面。
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
……
}
if (mcount > 0) {
//排序
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__);
//将所有分类数据存储。超过64个后会清0。相当于再多了一次结构。二层结构了。由于是从后往前存的,所以将前面空白的区域剔除。
//mlists + ATTACH_BUFSIZ - mcount 是一个二维指针
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();
});
}
}
……
}
- 首先通过
extAllocIfNeeded
创建了rwe
数据。 - 由于
cats_count
传值为1
所以这里相当于没有循环。通过methodsForMeta
获取分类方法列表。 - 当
mcount
为64
的时候,重新开始计数。也就是说当cats_count > 64
的时候会重新进行计数。但是目前loadAllCategories
传递的是1
所以不会进入这里的逻辑。那么只有attachToClass
会进入这个逻辑了。根据源码也就是attachLists
一次性做多传入64
个指针数据,多于64
个会进行多次赋值,直接走1 list -> many lists
逻辑。(待后续研究这个。) - 之后调用
prepareMethodLists
进行排序,然后会调用attachLists
将分类数据加入rwe
中。
对mlists
赋值前后做对比:
2.1.3 extAllocIfNeeded
class_rw_ext_t *extAllocIfNeeded() {
//获取rwe
auto v = get_ro_or_rwe();
if (fastpath(v.is())) {
return v.get(&ro_or_rw_ext);
} else {
//创建rwe
return extAlloc(v.get(&ro_or_rw_ext));
}
}
extAllocIfNeeded
内部调用是extAlloc
创建rwe
:
class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
runtimeLock.assertLocked();
//调用alloc创建空间
auto rwe = objc::zalloc();
//设置版本,元类为7,非元类为0。
rwe->version = (ro->flags & RO_META) ? 7 : 0;
//获取ro方法列表
method_list_t *list = ro->baseMethods();
if (list) {
//是否深拷贝,跟踪的流程中 deepCopy 为false
if (deepCopy) list = list->duplicate();
//将ro的方法列表放入rwe中。
rwe->methods.attachLists(&list, 1);
}
//属性
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rwe->properties.attachLists(&proplist, 1);
}
//协议
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
//设置rwe,rwe-ro = ro
set_ro_or_rwe(rwe, ro);
return rwe;
}
- 通过
alloc
创建rwe
。 - 将
ro
中methods
数据拷贝到rwe
中(这里没有深拷贝,其实也就是链接了个地址而已)。 - 链接属性和协议。
- 设置
rwe
,rwe
中的ro
指向ro
。
接着就进入了开始分析的attachLists
方法。
2.1.4 methodsForMeta
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
判断是否元类,元类返回classMethods
,类返回instanceMethods
。
2.1.5 attachLists 流程
实现多个分类,跟踪调用流程attachLists
中对methods
变化。当类没有方法的时候会进入0 lists -> 1 list
分支:
接着第二个分类会进入1 list -> many lists
分支:
加载第三个以及更多分类会进入many lists -> many lists
分支:
此时方法列表分布如下:
在这个流程中attachLists
整个流程与内存分布如下:
0 lists -> 1 list
的进入逻辑,首先是主类没有方法,分为两种情况
1.分类是在load_categories_nolock
过程中加载的(第一个分类会进入)。
2.分类在prepare_load_methods
的时候加载,类只有一个分类。
2.1.6 方法存储列表探究
根据上面的分析,当只有主类或者主类没有方法仅有一个分类的情况下,方法列表是存储在list
中的,否则存储在array()
中。(这里不讨论ro
合并的情况,合并了都是存储在ro
中也就相当于是在list
中)。
2.1.6.1 方法列表的存储逻辑
list
与array()
声明如下:
typename List
const Ptr *lists;
union {
Ptr 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;
}
//存储赋值
array()->lists[i] = addedLists[i];
-
list
与arrayAndFlag
只能存在一个。 - 方法指针列表存储在
lists
中,arrayAndFlag
指向lists
。arrayAndFlag & 1
只是为了标记是否存储了指针数组地址,通过最后一位进行标记。 - 调用方是
rwe->methods.attachLists
。
2.1.6.2 方法列表的读取逻辑
上面分析清楚了方法列表的存储逻辑,但是显然读取方法列表的时候都是先外层再内层进行二分查找的。那么这其中肯定做了区分或者包装:
//获取methods
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
可以看到这里的
mlists
与arrayAndFlag
中的元素内容结构是相同的。
那么核心就是在
beginLists()
与endLists
做的区别了:
const Ptr* beginLists() const {
if (hasArray()) {
//这里不是返回的array,直接返回的lists数组首地址。
return array()->lists;
} else {
//这里进行了&地址操作,相当于包装了一层。
return &list;
}
}
const Ptr* endLists() const {
if (hasArray()) {
return array()->lists + array()->count;
} else if (list) {
return &list + 1;
} else {
return &list;
}
}
- 直接判断是否存在
hasArray
从而返回array()->lists
或&list
。 -
&list
相当于直接包装了一层,所以在for
循环中就是同一个数据类型的调用了。
2.2 类实现load,分类不实现
调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
这个时候没有走attachCategories
的逻辑。
在realizeClassWithoutSwift
中断点验证ro
:
这个时候
ro
中已经有分类中的方法了,也就是说编译阶段就已经将分类的方法合并进类的ro
中了。
2.3 类不实现,分类实现load(只一个分类情况)
调用流程:map_images -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
这个时候也没有走attachCategories
的逻辑。这里逻辑与类实现load
,分类不实现相同。
2.4 类和分类都不实现load
调用流程:没有走入断点流程。这个也能讲的通,此时类为懒加载类。调用类的一个方法继续断点会进入realizeClassWithoutSwift
中(慢速消息查找流程进入):
此时依然是合并到
ro
中了。
根据以上的验证有如下结论:
- 类和分类都实现了
load
方法,在load_images
的时候会进入attachCategories
流程生成rwe
,将分类方法列表拼接在主类之前(都是指针,存储在指针数组中)。 - 类和分类有任一一个实现
load
方法,直接将分类的方法列表合并到了类的ro
数据中。 - 类和分类都没有实现
load
方法,直接将分类的方法列表合并到了类的ro
数据中,并且将类的实例化推迟到了第一次发送消息的时候。
2.5 多分类情况
上面分析了类和分类的四种组合情况,那么如果类有多个分类呢?
2.5.1 类实现load,分类部分实现load
类实现了load
方法,分类部分实现呢?
按照猜想,没有实现load
方法的分类应该直接合并到类的ro
中,实现了load
方法的分类应该在load_images
的attachCategories
流程中与类中的方法放在同一个指针数组中。
创建4
个分类,其中1、2
个不实现load
,3、4
实现load
进行验证,类的ro
数据如下:
这个时候发现
ro
中只有主类的方法(这个时候事情就不简单了)。跟进attachLists
:
可以看到实现了
load
与没有实现load
方法的分类都会走到attachCategories
逻辑。经过验证只要主类实现了load
,分类至少实现一个load
,则所有分类都不合并进主类ro
。
结论:类实现了load
,分类只要有一个实现load
方法,所有分类都不合并进类的ro
数据,在load_images
的时候在attachCategories
中合并放入rwe
中。
2.5.2 类不实现load,分类部分实现
前面讨论了类不实现,分类只有一个实现load
的情况。那么有多个分类呢?
主类不实现load
,4
个分类中的1
个实现load
:
会直接进行合并。
主类不实现load
,4
个分类中的2
个(更多个逻辑一样)实现load
:
这个时候已经没有合并了。还是在
load_images
的时候加入rwe
中(不过走的是load_images->prepare_load_methods->realizeClassWithoutSwift->methodizeClass->attachToClass
逻辑)。
那么多添加几个分类只有两个分类实现load
的情况:
同样会走
prepare_load_methods
的逻辑。
结论:主类不实现load
,分类至少2
个实现load
,则在load_images
的时候分类加入rwe
中,走的是prepare_load_methods
。why
?因为这个时候类无法处理了,所以不能合并。
2.5.2.1 实现两个分类load探究
为什么分类实现至少2
个load
没有走loadAllCategories
进行加载,而是走了prepare_load_methods
。这和主类实现load
有什么不同呢?
这两者的区别就是主类有没有实现load
方法。
在主类实现了load
方法的时候macho
中能找到__objc_nlclslist
:
主类没有实现load
,则macho
中没有__objc_nlclslist
(⚠️没有合并ro
的情况下则没有,合并了的情况下肯定就有了)。
对于load_images
:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
//didInitialAttachCategories 控制只来一次。 didCallDyldNotifyRegister 在 _objc_init 中赋值
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
//加载所有分类
loadAllCategories();
}
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//准备所有load方法
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
//调用 + load方法
call_load_methods();
}
对于主类没有实现load
方法,跟踪流程到load_categories_nolock
:
最终会走
objc::unattachedCategories.addForClass
的逻辑。没有走attachCategories
逻辑。
addForClass
void addForClass(locstamped_category_t lc, Class cls)
{
runtimeLock.assertLocked();
//没有缓存,进行拼接。
auto result = get().try_emplace(cls, lc);
if (!result.second) {
result.first->second.append(lc);
}
}
这里相当于将分类相关信息进行了缓存。最终会来到prepare_load_methods
的逻辑。
prepare_load_methods
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//添加主类的load方法
schedule_class_load(remapClass(classlist[i]));
}
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
//实现类
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
//添加分类的load方法。
add_category_to_loadable_list(cat);
}
}
- 通过判断是否有非懒加载的分类从而决定是否调用
realizeClassWithoutSwift
,在realizeClassWithoutSwift
中如果已经实例化过了类,则不会再继续执行后面的逻辑。
realizeClassWithoutSwift
此时会来到attachToClass
中进入attachCategories
的逻辑:
能不能进入这个流程是
it != map.end()
控制的,也就是上面addForClass
做的处理。这里没有走if
分支,因为在realizeClassWithoutSwift
中会调用元类的realizeClassWithoutSwift
。
那么什么情况下会进入
if
分支?
搜索发现只有在methodizeClass
的previously
分支才有调用,但是这个值一直传递的nil
,猜测是内部条件控制代码(有可能是切换测试什么的),正常不会进入这个逻辑:
在load_categories_nolock
调用attachCategories
的过程中cats_count
写死的1
,而在这里传递的是所有的分类数量。也就与2.1.1
和一
中的分析逻辑自洽了。
对于
2.1.1
中的逻辑,超过64
个会从0
开始计数,那么底层是怎么存储的呢?创建68
个分类验证下逻辑:
验证结果,第一次进入:
第二次进入(这里因为HP66
在最后一个):
结论:
loadAllCategories
中加载分类是一个一个加载,attachCategories
传递的是1
。prepare_load_methods
中attachCategories
会将分类全部加载。
这也能理解,因为prepare_load_methods
中的调用逻辑是针对单个类的,loadAllCategories
是针对所有分类的。
在
macho
中可以看到对应的section
信息。即使主类合并了ro
,对应分类也会导致出现对应段,不过内容为空:
2.6 LLVM探究 load 处理
为什么2
个以上分类实现load
方法,即使主类不实现load
方法也能加入rwe
中?这块逻辑是在哪里处理的呢? 核心显示是有没有合并ro
数据。
因为两个以上load
方法类无法处理,load
方法在load_imags
中都是要调用的,合并后只能调用一个。原理很明显,但是底层是怎么判断处理的呢?
既然合并ro
了,那么可以分别对合并与不合并的case
进行编译生成macho
文件,然后class-dump
头文件查看:
确认是在生成
macho
文件的时候就已经合并了。显示是llvm
阶段处理的事情。(⚠️:目前没有探索出来具体操作步骤在LLVM
哪一部分)
那么在
llvm
源码中核心点肯定在load
的处理上。既然是对load
的处理,那么在源码中搜索下"load"
,在RewriteModernObjC
中找到了RewriteModernObjC::RewriteObjCCategoryImplDecl->ImplementationIsNonLazy
。
同理在CGObjCMac.cpp
的CGObjCNonFragileABIMac::GenerateClass->DefinedNonLazyClasses
以及CGObjCNonFragileABIMac::GenerateCategory-> DefinedNonLazyCategories
。
搜索DefinedNonLazyCategories
以及DefinedNonLazyClasses
都是对生成macho
文件的处理。
在class_ro_t
的构建过程了,搜索到了class_ro_t
的定义,其中有m_baseMethods_ptr
,最终通过搜索m_baseMethods_ptr
定位到了对方法的操作:
但是很遗憾并没有找到合并分类与主类方法的逻辑。目前暂不清楚这块逻辑是怎么处理的。⚠️:待后续再详细研究。
至此已经理解清楚了分类中的方法加载逻辑。
小结:
- 类实现
load
,分类至少一个实现load
,会在load_images
过程中通过loadAllCategories
将所有分类数据加入rwe
中,不会合并进主类ro
。(类本身是非懒加载类) - 类实现
load
,所有分类不实现load
会将分类的方法合并到类的ro
中。(类本身是非懒加载类) - 类不实现
load
:- 分类只有一个并且实现
load
,分类方法会被合并到类的ro
中。(由于合并了ro
,类本身也变成非懒加载类) - 分类有多个,分类中至少
2
个实现load
。分类方法不会被合并进主类ro
中,在load_images
的过程中会走prepare_load_methods
逻辑将分类方法加入rwe
中。(由于没有合并ro
,类本身是懒加载类。分类导致它被加载。为什么不合并?因为两个以上load
方法类无法处理,load
方法在load_imags
中都是要调用的,合并后只能调用一个。)
- 分类只有一个并且实现
- 类和分类都不实现
load
,所有分类方法会被合并进类的ro
中。(类是懒加载类)
- 类本身是非懒加载类或者子类是非懒加载类是在
map_images
过程中实例化的。- 类本身是懒加载类,由于自身或者子类的非懒加载分类导致的类被实例化是在
load_images
过程中的。(ro
合并的情况如果分类有load
方法会导致类变为非懒加载类)- 本质上只有类自身实现
load
才是非懒加载类。其它情况都是被迫,本质上不属于非懒加载类。- 类与分类方法列表会不会合并,取决于
load
方法的总个数。只有一个或者没有则会合并,否则不合并。- 空的分类不会被加载。
既然load
影响类和分类的合并,那么直接验证下initialize
(注意这里要查看元类的ro
数据):
initialize
并不影响分类的合并。
三、类中同名方法的查找
3.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();
mlists != end;
++mlists)
{
//查找方法
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
按照刚才合并分类的逻辑,这里相当于是对外层指针数组的遍历,从而找到方法列表的指针。从beginLists
开始遍历,相当于从数组最开始遍历。也就是后加载的分类会被先查找。具体数据存储这里需要区分数组与数组指针(也就是一维数组与二维数组),具体参考2.1.6
中的分析。
findMethodInSortedMethodList:
template
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
//method list count
uint32_t count;
//count >>= 1 相当于除以2。加入count为8
for (count = list->count; count != 0; count >>= 1) {//7 >> 1 = 3,前一个已经比较了4,这里就完美的避开了4。
//base是为了配合少查找
//probe中间元素,第一次 probe = 0 + 8 >> 1 = 4
probe = base + (count >> 1);
//sel
uintptr_t probeValue = (uintptr_t)getName(probe);
//与要查找的sel是否匹配
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
//查找分类同名sel。
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
//匹配则返回。
return &*probe;
}
//没有匹配
if (keyValue > probeValue) {//大于的情况下,在后半部分
//没有匹配的情况下,如果sel在后半部分,这里base指向了上次查找的后面一个位置。
base = probe + 1;//5
//count对应减1
count--;//7 -- 操作为了少做比较,因为已经比较过了
}
//在前半部分不进行额外操作。
}
return nil;
}
while (probe
的逻辑应该是为了分类在编译阶段合并ro
导致的主类有同名方法而做的处理。
3.2 逻辑验证
由于分类合并进ro
与加载时产生rwe
是互斥的,所以分为两个逻辑验证。
3.2.1 分类同名方法不合并验证
主类和4
个分类都实现instanceMethod
方法,主类与任意一个分类实现load
方法,并且调用instanceMethod
进行验证。
可以看到开始查找是从分类HP4
开始查找的,findMethodInSortedMethodList
中断点验证:
(lldb) p probe
(entsize_list_tt::iterator) $15 = (entsize = 24, index = 1, element = 0x0000000100008270)
(lldb) p &*probe
(method_t *) $16 = 0x0000000100008270
(lldb) p $16->name()
(SEL) $17 = "instanceMethod"
(lldb) p $16->imp()
在HP4
中查找到instanceMethod
就返回了。
3.2.2 分类同名合并情况验证
去掉上面验证中主类的load
方法的实现,首先在类加载的过程中验证ro
数据(需要注意分类load
实现最多只能1
个,否则不会合并,具体看之前的分析):
首先在类加载的过程realizeClassWithoutSwift
中验证ro数据:
这个时候合并在了一起。但是在方法查找的时候同名方法应该是在一起的,这个时候还不在一起。那么就查看下排序前后。
SEL
修正前后:
方法排序前后:
可以看到确实对同名方法进行了排序,这也就是为什么findMethodInSortedMethodList
内部会在找到方法后继续往前找的原因。
那么按照猜想刚才方法查找的时候HP4
应该在数组的最前面。
验证:
验证符合预期。
四、总结
- 类与分类的合并:取决于
load
方法的实现总个数是否存在多个(initialize
不影响)。(因为两个以上load
方法类无法处理,load
方法在load_imags
中都是要调用的,合并后只能调用一个。)- 合并:
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
合并ro
的情况),此时类为非懒加载类 - 依赖非懒加载:类本身没有实现
load
方法- 子类实现
load
方法:由于递归实例化导致父类被实例化,父类父类本质上还是懒加载类,在这里相当于非懒加载类。 - 分类/子类分类实现
load
方法:在prepare_load_methods
中由于分类是非懒加载分类导致类被初始化,也相当于类变成了非懒加载类。
- 子类实现
- 完全非懒加载:类实现
- 懒加载:类、子类、分类、子类分类没有实现
类和分类加载流程(只包含了方法加载的逻辑):