前言
我们这里主要还是接类的加载原理(中)讲解剩下的遗留问题,我们已经知道rwe是在调用extAllocIfNeeded之后进行赋值的,在涉及调用extAllocIfNeeded的地方,我们重点关注了attachCategories(分类相关)方法,因为我们现在也不知道分类是在什么时候加载的,所以就先找到调用attachCategories的地方然后进行反向推导,所以objc源码中全局搜索找到了调用attachCategories的两个方法attachToClass和load_categories_nolock,我们就接着这里继续分析。
分类加载分析
attachCategories反向推导
我们上面已经知道调用attachCategories的两个地方,我们下面就先看attachToClass,看看它被调用的地方,全局搜索attachToClass找到它就只在methodizeClass中被调用:
但是在第一处调用的地方有个条件判断previousely,我们需要这个参数是否有值且在哪里传递过来的,目前是从methodizeClass中过来的,所以根据之前分析类的加载调用流程:realizeClassWithoutSwift ——> methodizeClass ——> attachToClass,找下realizeClassWithoutSwift被调用的时候previousely参数的值,这里我们主要看两个地方的调用就可以知道previousely大多数时候都是nil,lazy和non-lazy类加载的时候调用realizeClassWithoutSwift的地方:
所以我们只关注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中包含saySomething和cate_insMethod两个方法。我们在realizeClassWithoutSwift方法中,打上断点,这里我们已经可以读取到ro的数据,我们看下methodlist中是否有分类方法,下面我们进行断点调试:
我们看到此时分类方法并没有被加载进来,我们接着往下走,就来到了load_categories_nolock,内部就调用了attachCategories进行分类的处理:
下面我们主要看attachCategories是如何进行分类方法加载的:
我们可以看到,此处cats_count就是分类方法的个数,通过循环遍历把mlist依次从后往前存入数组mlists中,最终会调用methods.attachLists(mlists+ATTACH_BUFSIZ-mcount,mcount)进行下一步处理,此时mlists+ATTACH_BUFSIZ-mcount是个二维指针,我们接着看attachLists的实现:
我们此时调试进入attachLists中,执行直接会来到else中,因为之前进行主类加载list已存在,此时list存入的是一个指针method_list_t*,那么此时oldCount此时值为1,array的count此时等于oldCount+addedCount,将oldList放在array()->lists最后一个元素处,并按顺序将addedList存入array()->lists:
我们可以看到,此时分类CTPerson(CT)的方法放在array->list[0]处,处于第一个元素位置,至于hasArray()下,新创建了一个数组newArray,newArray的count等于oldCount+addedCount,将之前的lists倒序存入newArray->lists中,当然前部空出了addedCount个位置,用于存放addLists,最后把老的array()释放,赋值为newArray。那么什么时候会进入hasArray()这里呢?
多个分类的加载
我们在项目中创建多个分类并且都实现load方法进行调试,如下图:
这里打印的count是当前分类的个数,目前我们创建了3个分类。当我们调试进入attachLists中时,它直接进入hasArray()这个条件中:
我们此时可以看到newArray->lists的前两个元素是分类的方法,最后一个元素是主类方法。
分类load和主类load是否实现其他情况
分类有实现load方法,主类没有实现load,我们调试下:
我们可以看到它跟主类实现load方法一样,也会来到非懒加载类的地方,我们接着往下走:
在realizeClassWithoutSwift方法里我们读取ro(cls->data()),发现分类的方法已经存在,说明分类方法这时候已经被加载进去。分类不实现load,主类实现load,流程跟此情况一致,方法都来自data中。分类和主类都没有实现load方法时,会推迟到第一次消息发送的时候初始化,也就是懒加载类那中情况,方法等相关的数据也都放在data中。
总结
主类和分类加载方式受load方法影响,无论其中哪一方实现load方法,都会走非懒加载流程,从而影响应用程序的启动速度,所以非必要情况下,我们不要在类或者分类中实现load方法,若真的需要,优先在主类中实现。
类的相关拓展
class_ro_t数据结构
我们在编译时就读取了ro相关的数据,所以确定它应该在llvm源码中:
在Read方法中进行相关的变量赋值:
分类加载补充
我们之前在讨论关于主类和分类有没有实现load方法讨论的4种情况,针对主类没有实现load,分类实现load方法,这种情况做相关补充,我们之前给出的结论是:_read_images(非懒加载类)——>realizeClassWithoutSwift——>methodizeClass——>attachToClass,不调用attachCategories方法,但其实在多个分类情况下,超过一个分类实现了load方法,是会调用attachCategories的,若只有一个分类实现了load方法,则不会调用它,我们在工程里验证下:
- 多个分类实现了
load方法:
- 只有一个分类实现
load方法:
我们看下多个分类实现load方法,是如何调用到attachCategories方法的:
我们看到当超过一个分类实现了load方法时,load_images方法中调用的是prepare_load_methods()方法,而其他情况(例如主类实现load,分类未实现load)则调用的是loadAllCategories()方法。我们在prepare_load_methods内部可以看到相关调用:
这里可以看到它迫使我们主类进行了加载,相当于非懒加载类那种情况的加载,这里最终调用流程prepare_load_methods——>realizeClassWithoutSwift——>methodizeClass——>attachToClass——>attachCategories。
分类加载是否需要排序
这里我们只需要研究主类和分类都实现load方法的那种情况,因为其他情况都在data数据段里。这里根据我们之前研究的methodList存储结构,若同时存在两个分类LGA和LGB,那么此时methodList存储的结构应该是这样(array_t(struct)){LGA数组指针(prepareMethodLists排序),LGB数组指针(prepareMethodLists排序),主类数组指针(realizeClassWithoutSwift排序)},我们在lookUpImpOrForward慢速查找流程里,看下getMethodNoSuper_nolock方法,lldb调试打印methods——>beginList():
我们可以看到这里遍历的其实是对array_t进行的遍历,取出对应的数组指针,然后再进行的二分查找,LGA和LGB两个分类的顺序是无序的,只是因为LGA和LGB排在主类的前面,所以分类加载是不需要进行排序的,他们都是直接遍历查找进行相应的调用。那么我们之前讲到的二分查找那里关于分类的处理,是因为其他情况(例如主类没有load,分类有load方法;或者分类没实现load,主类实现load方法)时候,分类和主类方法都来自data,他们是一起加载的,这时候是通过二分查找找到最前面的那个方法。
类扩展分析
我知道分类加载方式跟是否实现load方法有很大关系,那么类扩展呢,它是什么时候加载的呢,我们实现个类扩展研究下:
我们通过clang生成.cpp文件看下:
我们在.cpp文件中看到此时它已经被加载methodlist里,说明它是同主类一同被加载的,我们可以在realizeClassWithoutSwift方法里读取下ro,看下methodlist中此时是否有类扩展的方法:
我们看到此时ro中已存在类扩展的方法,说明类扩展数据是伴随主类一起加载的。下一篇章我们讲解分类添加属性相关的分析及关联对象相关的讲解。