根据上篇文章的分析,分类的加载有两条线路:
methodizeClass -> attachToClass -> attachCategoriesload_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<List> 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<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
} else {
//创建rwe
return extAlloc(v.get<const class_ro_t *>(&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<class_rw_ext_t>();
//设置版本,元类为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<List> *lists;
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;
}
//存储赋值
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<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循环中就是同一个数据类型的调用了。
image.png
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,猜测是内部条件控制代码(有可能是切换测试什么的),正常不会进入这个逻辑:
image.png
在load_categories_nolock调用attachCategories的过程中cats_count写死的1,而在这里传递的是所有的分类数量。也就与2.1.1和一中的分析逻辑自洽了。
对于
2.1.1中的逻辑,超过64个会从0开始计数,那么底层是怎么存储的呢?创建68个分类验证下逻辑:
image.png
验证结果,第一次进入:
image.png
第二次进入(这里因为HP66在最后一个):
image.png
结论:
loadAllCategories中加载分类是一个一个加载,attachCategories传递的是1。prepare_load_methods中attachCategories会将分类全部加载。
这也能理解,因为prepare_load_methods中的调用逻辑是针对单个类的,loadAllCategories是针对所有分类的。
在
macho中可以看到对应的section信息。即使主类合并了ro,对应分类也会导致出现对应段,不过内容为空:
image.png
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定位到了对方法的操作:
image.png
但是很遗憾并没有找到合并分类与主类方法的逻辑。目前暂不清楚这块逻辑是怎么处理的。⚠️:待后续再详细研究。
至此已经理解清楚了分类中的方法加载逻辑。
小结:
-
类实现
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<class getNameFunc>
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<method_t, method_list_t, 4294901763, method_t::pointer_modifier>::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中由于分类是非懒加载分类导致类被初始化,也相当于类变成了非懒加载类。
- 子类实现
- 完全非懒加载:类实现
- 懒加载:类、子类、分类、子类分类没有实现
类和分类加载流程(只包含了方法加载的逻辑):








