类的加载原理——(下)

809 阅读8分钟

前言

我们这里主要还是接类的加载原理(中)讲解剩下的遗留问题,我们已经知道rwe是在调用extAllocIfNeeded之后进行赋值的,在涉及调用extAllocIfNeeded的地方,我们重点关注了attachCategories(分类相关)方法,因为我们现在也不知道分类是在什么时候加载的,所以就先找到调用attachCategories的地方然后进行反向推导,所以objc源码中全局搜索找到了调用attachCategories的两个方法attachToClassload_categories_nolock,我们就接着这里继续分析。

分类加载分析

attachCategories反向推导

我们上面已经知道调用attachCategories的两个地方,我们下面就先看attachToClass,看看它被调用的地方,全局搜索attachToClass找到它就只在methodizeClass中被调用:

Xnip2022-05-11_17-47-19.jpg

但是在第一处调用的地方有个条件判断previousely,我们需要这个参数是否有值且在哪里传递过来的,目前是从methodizeClass中过来的,所以根据之前分析类的加载调用流程:realizeClassWithoutSwift ——> methodizeClass ——> attachToClass,找下realizeClassWithoutSwift被调用的时候previousely参数的值,这里我们主要看两个地方的调用就可以知道previousely大多数时候都是nillazynon-lazy类加载的时候调用realizeClassWithoutSwift的地方:

截屏2022-05-11 下午5.58.09.png 截屏2022-05-11 下午5.57.00.png

所以我们只关注attachToClass第二处调用unattachedCategories.attachToClass。现在问题是到这里产生了分支,我们不知道什么时候调用到attachToClass,什么时候调用load_categories_nolock,这里我们要根据主类和分类加载有没有load方法调试4种情况:

  • 主类有load,分类也也有load方法实现; 调用流程:_read_images(非懒加载类)——>realizeClassWithoutSwift——>load_categories_nolock——>attachCategories
  • 分类有load,主类没有load方法; 调用流程:_read_images(非懒加载类)——>realizeClassWithoutSwift——>methodizeClass——>attachToClass,这里没有调用attachCategories方法。
  • 主类有实现load方法,分类没有; _read_images(非懒加载类)——>realizeClassWithoutSwift——>methodizeClass——>attachToClass,这里没有调用attachCategories方法。 调用流程:
  • 主类和分类都没有load,上面的方法都没调用。

分类加载流程调试分析

我们上面分析了四种可能的情况,首先我们分析第一种主类和分类都有实现load方法,看看分类方法在什么时候被加载进去的,首先我们看下目前主类和分类中我们实现的方法:
主类CTPerson

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)saySomething;
- (void)run;
- (void)run2;

分类CTPerson+CT中包含saySomethingcate_insMethod两个方法。我们在realizeClassWithoutSwift方法中,打上断点,这里我们已经可以读取到ro的数据,我们看下methodlist中是否有分类方法,下面我们进行断点调试:

Xnip2022-05-16_11-31-54.jpg

我们看到此时分类方法并没有被加载进来,我们接着往下走,就来到了load_categories_nolock,内部就调用了attachCategories进行分类的处理:

Xnip2022-05-16_11-48-47.jpg

下面我们主要看attachCategories是如何进行分类方法加载的:

Xnip2022-05-16_11-56-56.jpg

我们可以看到,此处cats_count就是分类方法的个数,通过循环遍历把mlist依次从后往前存入数组mlists中,最终会调用methods.attachLists(mlists+ATTACH_BUFSIZ-mcount,mcount)进行下一步处理,此时mlists+ATTACH_BUFSIZ-mcount是个二维指针,我们接着看attachLists的实现:

Xnip2022-05-16_12-38-08.jpg

我们此时调试进入attachLists中,执行直接会来到else中,因为之前进行主类加载list已存在,此时list存入的是一个指针method_list_t*,那么此时oldCount此时值为1arraycount此时等于oldCount+addedCount,将oldList放在array()->lists最后一个元素处,并按顺序将addedList存入array()->lists

截屏2022-05-16 下午1.38.16.png

我们可以看到,此时分类CTPerson(CT)的方法放在array->list[0]处,处于第一个元素位置,至于hasArray()下,新创建了一个数组newArraynewArraycount等于oldCount+addedCount,将之前的lists倒序存入newArray->lists中,当然前部空出了addedCount个位置,用于存放addLists,最后把老的array()释放,赋值为newArray。那么什么时候会进入hasArray()这里呢?

多个分类的加载

我们在项目中创建多个分类并且都实现load方法进行调试,如下图:

截屏2022-05-17 上午11.48.11.png

Xnip2022-05-17_12-03-00.jpg

这里打印的count是当前分类的个数,目前我们创建了3个分类。当我们调试进入attachLists中时,它直接进入hasArray()这个条件中:

Xnip2022-05-17_12-43-32.jpg

我们此时可以看到newArray->lists的前两个元素是分类的方法,最后一个元素是主类方法。

分类load和主类load是否实现其他情况

分类有实现load方法,主类没有实现load,我们调试下:

截屏2022-05-17 下午1.05.21.png

我们可以看到它跟主类实现load方法一样,也会来到非懒加载类的地方,我们接着往下走:

Xnip2022-05-17_13-11-07.jpg

realizeClassWithoutSwift方法里我们读取ro(cls->data()),发现分类的方法已经存在,说明分类方法这时候已经被加载进去。分类不实现load,主类实现load,流程跟此情况一致,方法都来自data中。分类和主类都没有实现load方法时,会推迟到第一次消息发送的时候初始化,也就是懒加载类那中情况,方法等相关的数据也都放在data中。

总结

主类和分类加载方式受load方法影响,无论其中哪一方实现load方法,都会走非懒加载流程,从而影响应用程序的启动速度,所以非必要情况下,我们不要在类或者分类中实现load方法,若真的需要,优先在主类中实现。

类的相关拓展

class_ro_t数据结构

我们在编译时就读取了ro相关的数据,所以确定它应该在llvm源码中:

截屏2022-05-17 下午2.49.32.png

Read方法中进行相关的变量赋值:

Xnip2022-05-17_14-55-45.jpg

分类加载补充

我们之前在讨论关于主类和分类有没有实现load方法讨论的4种情况,针对主类没有实现load,分类实现load方法,这种情况做相关补充,我们之前给出的结论是:_read_images(非懒加载类)——>realizeClassWithoutSwift——>methodizeClass——>attachToClass,不调用attachCategories方法,但其实在多个分类情况下,超过一个分类实现了load方法,是会调用attachCategories的,若只有一个分类实现了load方法,则不会调用它,我们在工程里验证下:

  • 多个分类实现了load方法:

截屏2022-05-22 上午11.13.40.png

  • 只有一个分类实现load方法:

截屏2022-05-22 上午11.17.39.png

我们看下多个分类实现load方法,是如何调用到attachCategories方法的:

截屏2022-05-22 上午11.21.31.png

我们看到当超过一个分类实现了load方法时,load_images方法中调用的是prepare_load_methods()方法,而其他情况(例如主类实现load,分类未实现load)则调用的是loadAllCategories()方法。我们在prepare_load_methods内部可以看到相关调用:

截屏2022-05-22 上午11.43.57.png

这里可以看到它迫使我们主类进行了加载,相当于非懒加载类那种情况的加载,这里最终调用流程prepare_load_methods——>realizeClassWithoutSwift——>methodizeClass——>attachToClass——>attachCategories

分类加载是否需要排序

这里我们只需要研究主类和分类都实现load方法的那种情况,因为其他情况都在data数据段里。这里根据我们之前研究的methodList存储结构,若同时存在两个分类LGALGB,那么此时methodList存储的结构应该是这样(array_t(struct)){LGA数组指针(prepareMethodLists排序),LGB数组指针(prepareMethodLists排序),主类数组指针(realizeClassWithoutSwift排序)},我们在lookUpImpOrForward慢速查找流程里,看下getMethodNoSuper_nolock方法,lldb调试打印methods——>beginList()

Xnip2022-05-26_11-20-51.jpg 截屏2022-05-26 上午11.22.29.png

我们可以看到这里遍历的其实是对array_t进行的遍历,取出对应的数组指针,然后再进行的二分查找,LGALGB两个分类的顺序是无序的,只是因为LGALGB排在主类的前面,所以分类加载是不需要进行排序的,他们都是直接遍历查找进行相应的调用。那么我们之前讲到的二分查找那里关于分类的处理,是因为其他情况(例如主类没有load,分类有load方法;或者分类没实现load,主类实现load方法)时候,分类和主类方法都来自data,他们是一起加载的,这时候是通过二分查找找到最前面的那个方法。

类扩展分析

我知道分类加载方式跟是否实现load方法有很大关系,那么类扩展呢,它是什么时候加载的呢,我们实现个类扩展研究下:

截屏2022-05-26 上午11.54.41.png

我们通过clang生成.cpp文件看下:

截屏2022-05-26 上午11.57.04.png

我们在.cpp文件中看到此时它已经被加载methodlist里,说明它是同主类一同被加载的,我们可以在realizeClassWithoutSwift方法里读取下ro,看下methodlist中此时是否有类扩展的方法:

Xnip2022-05-26_12-08-16.jpg

我们看到此时ro中已存在类扩展的方法,说明类扩展数据是伴随主类一起加载的。下一篇章我们讲解分类添加属性相关的分析及关联对象相关的讲解。