前言
“这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战”
根据前两篇文章 类的加载 上 和 类的加载 中,我们对类的加载有了一定的了解,并且梳理两条流程:
-
宏观流程:
dyld-->images-->内存-->类(方法、协议、属性。。。) -
细节流程
read_images-->read_class(名称-类) -->realizeClassWithoutSwift(ro-rw、superClass、isa...) -->methodizeClass()-->prepareMethodLists(写入方法名 + 排序)
因为当程序编译完成后, ro 已经是固定不变内存(clean memory)空间,只读,加载后不会发生改变的内存空间,包括类名称、方法、协议和实例变量的信息;当处于运行时,数据空间是会发生改变的,而 ro 是不能改变的,那么就有了 rw 的产生,把 ro 的内容赋值给 rw ,而 rw 是属于 dirty memory ,由于其动态性,可以往类中添加属性、方法、协议。在运行时会发生变更的内存。但是 rw 的内存比较昂贵,而且,并不是每一个的类都有分类,也不是每一个类都会进行动态添加方法、属性、协议,所以就对 rw 进行扩展,然后就有了 rwe,在这里面处理对应的类的信息。
资源准备
objc源码下载:多个版本的objc源码- 小板凳、冰🍺
attachCategories 反推思路
根据上篇文章的分析,分类的加载有两条线路:
methodizeClass -> attachToClass -> attachCategoriesload_images -> loadAllCategories -> load_categories_nolock -> attachCategories从attachCategories最终调用到了attachList。
既然attachList重点,那么就进入函数里面,查看他的实现:
-
当类里面没有方法数组,且有一个分类时,也就是
(!list && addedCount == 1)判断,那么不开辟新数组,直接赋值给了list,此时,内部存储的相当于是元素; -
当没有方法数组,且有不确定数量的分类时,也就是
else的判断,先开辟新数组,然而由源码可知,新加入的addedLists是一个* *结构,且通过for循环插入在数组的最前面,然后再判断有没有oldList(主类方法列表) ,如果有,就直接把他作为一个整体放入到新开辟的数组中,此时oldList就是一个数组指针。那么这个新开辟的数组就是一个二维数组了,如下图注解图①。 -
当有方法数组,且有不确定数量的分类时,也就是
(hasArray())判断,先开辟新数组,然后通过for循环把旧数组里面的元素倒序插入新开辟的数组中,然后再通过for循环把要加入的addedLists的元素,插入到新数组的最前面,最后释放旧数组。那么下次过来的时候,又设置新的array,如下图注解图②。
注解图①:
注解图②:
分类与主类加载情况
根据前面几篇文章分析,类的加载与load方法有关,那么分类的加载与load是否有关系呢?我们可以分以下4种方式进行探索:
- 类和分类都实现
load方法。 - 类实现
load,分类不实现load方法。 - 类不实现
load方法,分类实现load。 - 类和分类都不实现
load方法。 同时,我们在源码里面添加LGPerson和 这个类的分类LGPerson+LGA,在通过断点调试.
类和分类都有load方法
主类和分类的load方法都实现,然后再断点调试,如下图:
通过打印的方法信息和左侧堆栈的调用情况,我们可以梳理出一个程序执行的线路出来:
_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass --> load_categories_nolock --> attachCategories
类有load,分类没有load方法。
主类的load方法实现,而分类 load 方法注释掉,然后再断点调试,如下图:
通过打印的方法信息情况,我们可以梳理出一个程序执行的线路出来:
_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass
注意哦,此时,是没有执行 attachCategories 方法的。
类没有load,分类有load方法。
主类的load方法注释掉,而分类 load 方法打开,然后再断点调试,如下图:
通过打印的方法信息情况,我们可以梳理出一个程序执行的线路出来(和 类有load,分类没有load方法 这一种是一样的):
_read_images(非懒加载类) --> realizeClassWithoutSwift --> methodizeClass -> attachToClass
也是没有执行 attachCategories 方法的。
类和分类都不实现load方法。
主类和分类的load方法都注释掉,然后再断点调试,如下图:
发现直接就到了 main 函数里面,上面说到的这些方法都没实现。
那么他们这几种情况,具体发什么了什么,我们就需要进行详细的数据跟踪了。
分类加载流程跟踪(单个分类)
分别在主类 LGPerson 和分类 LGPerson (LGA) 里面添加多个方法:
-
主类:
-
分类:
-
条件是:类和分类都有load方法 首先,我们得确定在实现类的时候,分类是否已经道到ro里面了。如果已经有了数据,那么就说明分类加载是在类的实现之前就已经完成了。那么我们可以在realizeClassWithoutSwift函数里面,打印ro的信息,如下图:
我们可以查看到类里面的方法数组里面,目前只有 10 个方法,我们再把这个 10 方法打印出来,看里面有没有分类的方法存在:
可以看到 10 个方法全部是主类的方法,那么说明此时分类的方法还没有加载过来。
接着再根据上文探寻到的路径执行,到 attachCategories 函数,通过 lldb 调试,获取里面的内存信息,如下图:
mlist 是一个地址,而 mlists 里面存的是地址数组,共 64 个元素。
-
ATTACH_BUFSIZ的值是64,mcount初始值为0,mlists共有64个位置。 -
这里将
mlist赋值到mlists中64 - 1的位置,并对mcount进行+1操作。
接着往下走:
到了这里,就印证了文章开始所讲的 attachLists 函数。其传入进去的参数是 ** 类型。
-
通过
prepareMethodLists对mlists中的方法进行排序,这里mcount是1,取的是mlists的第64 - 1的位置的值。 -
接下来进入
attachLists函数,传入mlists + ATTACH_BUFSIZ - mcount是method_list_t **类型,传入mcount是1。
进入到attachLists 函数里面:
这个list的count是3,存的是我们主类的3个方法。再接着往下分析:
到了这里,就验证了我们对attachLists 函数的源码分析,开辟出的新数组,把旧数组作为指针存入到新数组里面,而这个指针是指向一个数组。传入进来的分类(load_categories_nolock 里面的cat)是以元素循环存入新数组。
多个分类加载
对load_categories_nolock函数的跟踪
在for循环中:
循环添加分类:
此时,i = 1, count = 4,分类是LGA_Two,接下来就进入attachCategories函数。
对attachCategories函数的跟踪
- 根据
lldb调试,传入的mlists + ATTACH_BUFSIZ - mcount依然是method_list_t **类型,也就是method_list_t指针地址类型。
对attachLists函数的跟踪
-
oldCount = 2,根据之前的分析我们可以知道,它们是主类的method_list_t *和第一个分类的method_list_t *。 -
newCount = 3,用这个大小开辟了一个新的newArray。 -
for循环,取array()->lists的值,也就是old的值,从后向前存储到newArray。
刚刚把主类和分类都有 load 方法的情况给分析完了,那么现在就还剩下他的几种情况,接下来,继续往下探究。
五种情况 ro-rw 数据的加载
目前程序里面,只有 Person (LGA) 一个分类。
分类有 load 方法,主类没有
运行程序,断点在 _read_images 函数里面断住了,在主类里面,没有设置 load 方法,但是一样在这里断住了:
说明了,虽然在主类里面没有实现load方法,但是同样是非懒加载。因为分类是主类的,既然分类都执行了load方法,那么主类,也会 被迫 执行。
然后再进入到 realizeClassWithoutSwift 函数里面,打印 ro 的信息:
我们可以看到数组里面是有 13 个方法的,根据前面打印主类的方法数组,可以知道在主类里面有 10 个方法,但是现在有 13 个方法,那就说是把分类的方法,也加进去了。而我们所有的数据,都是从 dyld 过程里面获取的,这就说明,在整个加载过程中,已经把分类加载进去了,和主类杂合在一起。那么分类的数据,就从 data() 里面获取。
主类有 load 方法 ,分类没有
和上面步骤一样,经过 lldb 调试,在realizeClassWithoutSwift中打印的结果,和上面的这个情况是一样的。方法已经存在了,说明是通过data()加载的,已经被编译器处理了。
分类和主类都没有 load 方法
此时主类和分类都没有实现 load 方法。运行工程:
根据运行的结果,就会发现,直接就到了类的初始化上面来了,也就是说,根本就没有走我们之前几次情况时,所调用的路线。我们再跟着断点往下走:
通过这个工程左边的堆栈信息,就很明确了,此时是懒加载执行的,进行了消息发送。再看此时的 lldb 打印 ro 的信息,和非懒加载的数据是一样的。
这就说明了,当主类和分类都没有 load 方法时,就会推迟到第一次消息的时候,进行初始化,这个时候,无论是主类,还是分类,他们加载的数据都是从 data() 里面进行获取的。
多个分类不全有 load 方法 + 主类有 load 方法
再增加 3 个分类,分别是 LGPerson+LGA_One 、 LGPerson+LGA_Two 、 LGPerson+LGA_Three,加上原先的分类,就 4 个分类。那么再运行代码,来到 load_categories_nolock 方法,就可以打印出分类加载的数量 :
得到的分类数,就是 4 。也就是说,经过非懒加载进行实现的,也是从 machO 里面加载的数据。
- 就和我们前面分析的几种情况一样,数据是从
data()里面获取,但是data()的数据,却是从machO里面来的。这也就是我们为什么说load方法的调用,是一个耗时的操作。结合我们上面的分析,调用load方法,就会执行非懒加载,在load_categories_nolock和attachCategories两个函数里面,执行加多的算法,这样就会出现大的耗时操作。所以,load 方法的使用,要慎重。
methodList -> 数据结构
在attachCategories函数里面,由于分类是由 rwe 存储的, rwe->methods.attachLists(mlists, mcount)这种加载分类的方式,attachLists函数中是方法列表加载的核心代码。而mlists是method_list_t类型,通过 lldb 打印 :
-
通过源码和打印分析,由源码,从分类中获取的方法列表是
method_list_t,通过打印,由get方法获取,取出来的是method_t类型。我们来分别看下method_list_t和method_t的源码: -
method_list_t源码:
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return i;
}
bool isSmallList() const {
return flags() & method_t::smallMethodListFlag;
}
bool isExpectedSize() const {
if (isSmallList())
return entsize() == method_t::smallSize;
else
return entsize() == method_t::bigSize;
}
method_list_t *duplicate() const {
method_list_t *dup;
if (isSmallList()) {
dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
dup->entsizeAndFlags = method_t::bigSize;
} else {
dup = (method_list_t *)calloc(this->byteSize(), 1);
dup->entsizeAndFlags = this->entsizeAndFlags;
}
dup->count = this->count;
std::copy(begin(), end(), dup->begin());
return dup;
}
};
method_t源码:
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
。。。。。 代码省略。。。。。
根据源码:
method_list_t是继承于entsize_list_tt,在entsize_list_tt的实现里面,就可以知道通过getOrEnd进行返回数据,对应着就是在打印里面的get;- 在
getOrEnd里面,
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
- 先是从结构体头部,平移到结构体尾部。
- 通过
sizeof(*this)和i * entsize()计算出方法位置的大小,再平移,就获取到相应的方法了。 - 最后通过
(Element *)强转出来。
在进行验证下:
通过 lldb 打印,当一开始就进行读取时,method_list_t的地址是0x00000001000045c0,再看 method_list_t 的大小 8,然后再通过 get 拿到第 0 个位置的地址,是0x00000001000045c8,对比刚开始的位置,是平移了 8 ,那就完全对上了。
- 也间接验证了,在
method_list_t里面,存的都是指针地址。当然,如果分类里面,什么都没实现的话,也不会被加载进去。
多个分类加载,需不需要排序
通过methods.attachLists进行加载的类,在调用时是否进行排序了呢,在 iOS底层探究-----慢速查找流程 中,方法的查找会通过----二分查找,我们要讲的多个分类加载,需不需要排序问题,就得从这里开始。
此时,我们在源码中设置两个分类,分别是feLGPerson+LGA和LGPerson+LGA_One。其实现 saySomething方法。然后在 main 里面调用,如下:
当进行非懒加载之后,调用 saySomething方法,进入方法查找流程,就会执行到 getMethodNoSuper_nolock 方法。如下图:
根据上面的 attachLists 数据追踪,我们当时只有一个分类,所以得到的 count = 3,现在我们有两个分类,所以得到的 count = 4。所以情形是一样的。也就说明了,方法列表并没有进行排序。
然而执行 getMethodNoSuper_nolock --> search_method_list_inline --> findMethodInSortedMethodList,在findMethodInSortedMethodList中
看源码,当keyValue == probeValue时,会有一个while的死循环向前查找,这明明又是说明对方法列表进行了组合排序,这是为什么呢。
结合前面的分析,因为分类的加载,除了attachCategories的方式,还有一种data()加载的方式,data()加载的方法列表是一个把本类和分类方法杂合一起的列表,并且是排好序的。
class_ro_t 数据加载
根据前面的分析,ro的数据加载,是编译阶段就完成了,所以我们用llvm源码来进行分析。
对比objc中的class_ro_t的实现,可以看出是和llvm中的class_ro_t是一样的,只是llvm中多了m_的前缀。
编译器的读取,是在 dyld 链接的时候,read_images 和 load_images的时候,此时的整个 main 函数是还没运行起来的,ro 已经获得了数据,所以是在编译时完成的。
我们注意在结构体的结尾,是函数Read,这个函数中应该就是从MachO中读取了数据,接下来继续分析,进入到Read里面去。
我们可以索引class_ro_t::Read来定位,如下图:
看这个源码,和我们在开发中,进行模型赋值是差不多的,相当于序列化。
我们对Read函数的实现基本了解了,再看看调用的情况,全局搜索ro->Read(:
总结
- 懒加载类+懒加载分类:消息第⼀次调⽤(加载数据)
- ⾮懒加载类+懒加载分类:
read_image就加载数据
这两种情况,在编译时期,就完成了 data()