二十三、app加载流程(六)分类的实现和加载(下)

1,540 阅读14分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处

一、资料准备

上一章相同。本章主要看多个分类的加载和实现,以及一些源码的解析。

二、项目准备

更改JDMan中的代码,删除没必要的属性和实例变量。只保留两个方法。

更改JDMan(LA)分类中的代码,也只保留两个方法。

更改后的代码如下 :

1. main.m :

图片.png

2. JDMan :

图片.png

3. JDMan(LA) :

图片.png

三、重点函数的解析

1. prepareMethodLists

出现的位置 :methodizeClassattachCategories

作用 : 对方法进行排序

核心代码 : fixupMethodList

fixupMethodList的源码 :

图片.png

小结 :

方法的排序是通过SEL的地址来完成的。方法的SEL会加入到SEL列表中。

2. attachCategories

出现的位置 : attachToClassload_categories_nolock

其中load_categories_nolockloadAllCategories中是循环遍历头文件信息,循环调用load_categories_nolock

作用 : 初始化rwe,将类的ro中的数据、分类的数据放入rwe中。

attachCategories的源码 :

只贴了重要的,部分打印不太重要的没有加进来。

图片.png

图片.png

图片.png

图片.png

图片.png

attachCategories的源码调试

  1. JDMan类和JDMan(LA)分类全部都实现+(void)load方法,因为通过上一章可知,非懒加载类和非懒加载分类会通过load_images进入到attachCategories
  2. attachCategories加入自定义代码段,为了定义到正确的JDMan类的加载流程中。
    const char *qudaodemingzi = cls->mangledName();
    const char *JDManName = "JDMan";
    if (strcmp(qudaodemingzi, JDManName) == 0) {
        auto jd_isMeta = cls->isMetaClass();
        if (!jd_isMeta) {
            printf("找到自己定义的类了:%s 从函数 : %s 拿到的\n",qudaodemingzi,__func__);
        }
    }
  1. 在自定义代码的printf上打断点。运行程序

图片.png

  1. 可以看一下mlists,这个总表是什么,lldb看mlists :

图片.png

中间省略,直接到最后 :

图片.png

最初的mlists全部都是内存的地址,但是没有内容可以读取。

  1. 断点一直向下,移动到rwe的初始化。先看rwe未进行初始化的时候,cls的rwe是什么样的 :

图片.png

图片.png

  1. 断点继续向下,等rwe初始化完成后,看rwe和类的rwe :

rwe :

图片.png

类的rwe :

图片.png

因为extAllocIfNeeded就是在类的内部开辟了rwe的内存,然后将ro的数据贴进去。这个函数的返回值就是class_rw_t类型,也就是rwe。

  1. load_categories_nolock进入到attachCategoriescats_count全都是1,参数是写定的。因为load_categories_nolock和外部的loadAllCategories都会循环遍历头文件,找到分类相关的数据。因为是循环,所以每个循环内,都是只添加一个分类数据,下一个分类自然会再外面的循环后再进入这里,完成添加。

图片.png

  1. 移动断点,看entry :

图片.png

  1. 移动断点,看mlist,这里注意,mlist只是一个分类的方法列表。上面的mlists是一个类的所有分类的方法列表的合集。mlists是二维的结构。

图片.png

  1. 再移动断点是不会进入if (mcount == ATTACH_BUFSIZ)的,官方注释说了,因为一般正常的类不会有64个分类。所以直接跳到下面,看mlists,和上面的4中是一样的 :

图片.png

  1. 再移动断点,mlists就不一样了,因为mlists的最后一位插入了mlist,也就是当前分类的方法列表 :

图片.png

  1. 跳过属性和协议的添加,和方法的添加是一样的,只以方法举例。直接到if (mcount > 0) ,只要分类添加了方法,那么就会进入11到中,mcount一定是自增的,所以只要分类添加了方法,mcount至少为1。

图片.png

  1. 对分类的方法进行排序前 :

图片.png

  1. 对分类的方法进行排序后 :

图片.png

  1. 将分类方法贴入类的rwe中,先看贴进去之前的rwe的数据,会是类的ro的数据 :

图片.png

图片.png

  1. 贴入后的rwe的数据,这里注意,lldb的命令不一样了 :

如果按照以前的lldb取rwe的methods数据 :

图片.png

图片.png

lldb在换成如下,就可以显示rwe中方法数据的,证明通过了rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);,分类的方法被贴入到了rwe中 :

图片.png

图片.png

小结 :

  1. 设置一个总的方法列表,用来装分类的方法列表,总的列表里面装的是分类的方法列表,也就是二维的。
  2. 总的列表的容量是64。因为苹果认为正常的情况是不会出现一个类创建了64个以上的分类的。
  3. 给类开辟rwe空间。如果rwe之前就已经存在,则是获取rwe。
  4. 根据要加入的分类数量进行遍历循环。将分类的方法列表放入总的列表里面。这里的mcount自增目的是将后加入的分类中的方法放到先加入的分类方法的前面。这也就解释了为什么如果后加入的分类重写了之前分类的方法,在调用的时候,调用到的是后加入的分类的方法。
  5. 对分类方法进行排序。(这里通过内存平移。传入排序函数的,会是以平移到的位置为开始,到整个列表最后,这一段中的所有分类方法列表。举例 :相当于一个数组是1,2,3,4,5,假设所有添加的分类方法,不论几个分类的,是所有分类的方法,最终占据了3,4,5三个位置,这个内存平移,就会把3,4,5全部传入,最终排序的是3中的内容排序,然后4中的内容排序,最后5中的内容排序,都是根据sel地址进行排序)。
  6. 将分类的方法贴入到rwe中。

3. extAllocIfNeeded

出现的位置 :attachCategories

作用 :

(1). 如果类没有rwe,就让类创建rwe,并将类的ro数据复制给rwe。

(2). 如果类已经有了rwe,就获取rwe。

核心代码 : extAlloc,在类没有rwe的时候会调用extAlloc创建rwe。

extAlloc源码 :

图片.png

图片.png

小结 :

  1. 分配一块内存给rwe
  2. 给rwe的version赋值,通过ro的flags,让rwe也能判断当前类是否是元类。
  3. 从ro中获取数据方法列表
  4. 如果方法列表存在,就把ro中的方法列表赋值给rwe。属性和协议同理。
  5. 设置rwe的ro为类的ro,让rwe也可以读取到ro的信息。

4. attachLists

出现的位置 : extAllocattachCategoriesmethodizeClass

作用 : 将数据添加到指定的数组列表中。

attachLists源码 :

主要就是三种不同情况的方法插入 :

  1. 多对多 :

列表数组中本身就有多个方法列表,然后又插入了多个方法列表。也就是所谓的多对多。

图片.png

  1. 0对1 :

本来就没有列表数组,也没有列表。插入要插入的列表。

图片.png

  1. 1对多 :

图片.png

小结 :

  1. 多对多 : 本身就存在了列表数组,又插入了新的列表,则设置新的列表数组,容量是旧的数量+新列表的数量。然后根据这个容量进行开辟内存。循环遍历,将旧的列表按照原有顺序,放到新列表数组的后面。新插入的列表则按照顺序,循环遍历放入新列表的前面。
  2. 0对1 : 本身没有列表,也没有列表数组,将新进入的列表赋值给list
  3. 1对多 : 原来只有一个列表,新插入了多个列表,则分配一块内存,分配内存的大小根据1(或者是0)+新插入列表的数量。内存中存放的是数组指针,也就是开辟的是一个数组,也就是列表数组。将原有的1个列表放到列表数组的最后。新插入的列表,根据顺序,循环遍历从数组的头部加入

四、多个分类的加载

上一章探索了只有1个分类的情况。

这里探索下1个类有多个分类的情况,以2个分类来举例。

1. 项目变动

JDMan类新添加一个分类JDMan(LB),其内部实现如下 :

图片.png

2. 思路

  1. 非懒加载类 + 两个分类非懒加载
  2. 非懒加载类 + 一个分类非懒加载 + 一个分类懒加载
  3. 非懒加载类 + 两个分类懒加载
  4. 懒加载类 + 两个分类非懒加载
  5. 懒加载类 + 一个分类非懒加载 + 一个分类懒加载
  6. 懒加载类 + 两个分类懒加载

3. 代码段准备

上一章中的代码段准备相同。加入的地方也相同。

和之前不同的地方,在load_categories_nolockattachCategories的打印中,加入分类的信息 :

load_categories_nolock :

图片.png

attachCategories :

图片.png

JDMan类、JDMan(LA)分类、JDMan(LB)分类中加入同名的方法- (void)ceShi;

并实现。然后在main.m中调用。

JDMan :

图片.png

JDMan(LA) :

图片.png

JDMan(LB) :

图片.png

4. 非懒加载类 + 两个分类非懒加载

  1. JDManJDMan(LA)JDMan(LB)中的+(void)load方法不进行注释。

  2. 运行项目,结果如下 :

图片.png

图片.png

  1. attachToClassattachCategories里面的printf那里打上断点,看一下堆栈信息。

attachToClass:

图片.png

attachCategories : 图片.png

堆栈信息中显示的调用路径是和上一章相比,并没有新的函数出现。

小结 :

  1. 非懒加载类+两个非懒加载分类 :

类也是通过read_images中的realizeClassWithoutSwift实现了类的实现。

分类则是通过load_images--->loadAllCategories--->load_categories_nolock--->attachCategories完成了数据的加载,并且类在attachCategories中开辟了rwe。

  1. 从项目运行结果来看,类和分类中的同名方法,普通的调用,一定是会调用到分类的方法。并且会调用到后加载完成的分类的方法,和谁先创建无关。

5. 非懒加载类 + 一个分类非懒加载 + 一个分类懒加载

  1. JDManJDMan(LA)中的+(void)load方法不注释。JDMan(LB)+(void)load注释掉,变成懒加载类。

  2. 保持attachToClassattachCategories中的断点。

  3. 运行项目,看堆栈信息和控制台打印信息

attachToClass :

图片.png

attachCategories :

图片.png

打印信息 :

图片.png

图片.png

小结 :

非懒加载类 + 一个分类非懒加载 + 一个分类懒加载 :

类非懒加载的情况下,只要一个分类是非懒加载的,那么其他的分类也会变成非懒加载。

6. 非懒加载类 + 两个分类懒加载

  1. JDMan保留+(void)loadJDMan(LA)JDMan(LB)注释掉+(void)load

  2. 保持attachToClassattachCategories中的断点。

  3. 运行项目,结果如下 :

图片.png

  1. 继续执行断点,并不进入attachCategories的断点。项目运行完成。

图片.png

  1. 但是看打印的结果,分类的方法明显进入了类的ro中。也就是在编译期,两个非懒加载分类的方法都写入了类的ro。和上一章中的除非懒加载类+非懒加载分类之外的三种情况是一样的。

图片.png

小结 :

非懒加载类 + 两个懒加载分类:

非懒加载类的情况下,如果多个分类都是懒加载的,那么分类的数据会在编译期就被写入类的ro中。

7. 懒加载类 + 两个分类非懒加载

  1. 注释掉JDMan类中的+(void)load方法。保留JDMan(LA)分类和JDMan(LB)分类的+(void)load方法。

  2. 保持attachToClassattachCategories中的断点。

  3. 运行项目会发现报错 :

图片.png

图片.png

原因 :

(1). cls是从分类的结构体成员中获得的cls,但是类我们并没有实现。

(2). 看调用堆栈,是从load_images开始的。因为类是懒加载的,所以在map_images的时候,并没有实现JDMan这个类。那么JDMan的对象就是无法调用到isMetaClass函数的。

改正 :

auto jd_isMeta = cls->isMetaClass();注释掉,获取元类的情况换成如下,直接从类的ro中读取 :

                auto jd_ro = (const class_ro_t *)cls->data();
                auto jd_isMeta = jd_ro->flags & RO_META;
  1. 改正后,重新运行项目,发现终于进入了attachToClass

图片.png

  1. 发现是从prepare_load_methods进入的,再看控制台的打印信息,已经进过了load_categories_nolock函数。证明loadAllCategories也是执行过的。可以验证一下,给load_categories_nolock中的printf打上断点。重新执行程序

图片.png

  1. 上图证明了,两个非懒加载分类+懒加载类,分类会因为实现了load方法,先在load_images中被处理,而类因为是懒加载的,所以在map_images中并未被处理。

  2. 这时进入load_categories_nolock,就不会从这里进入attachCategories,因为cls并没有被实现。可以断点向下看一下,会进入如下流程 :

图片.png

  1. 经过load_categories_nolock。会继续load_images的流程,进入prepare_load_methods,然后在这里进入realizeClassWithoutSwift,然后进入attachToClass,通过attachToClass进入attachCategories完成。

小结 :

懒加载类 + 两个分类非懒加载 :

  1. 非懒加载类会先进入load_images,但是因为类是懒加载的,所以类在map_images的时候不会进行实现。进而分类的流程不会进入attachCategories去完成类的rwe的初始化和将方法放入类的rwe中。分类会被加入到和类的关联中。
  2. 非懒加载的分类会进入prepare_load_methods,在这里会进入realizeClassWithoutSwift完成类的实现,使类加载完成,不会等到类第一次被调用再完成类的加载。然后通过attachToClass进入attachCategories,完成类的rwe的初始化,和将分类的数据放入rwe中。

8. 懒加载类 + 一个分类非懒加载 + 一个分类懒加载

    1. 注释掉JDMan类和JDMan(LA)分类中的+(void)load方法。JDMan(LB)分类的+(void)load方法。
  1. 保留上面调试的时候的断点。
  2. 运行项目
  3. 控制台的打印信息 :

图片.png

图片.png

  1. 根据上面的经验,分类的方法是在编译期就放入了类的ro中。

小结 :

懒加载类 + 一个分类非懒加载 + 一个分类懒加载 :

一个分类非懒加载,一个分类懒加载,会使类变成非懒加载。在编译期就会将分类的数据放入类的ro中。

9. 懒加载类 + 两个分类懒加载

  1. JDMan类、JDMan(LA)分类、JDMan(LB)分类中的+(void)load全都注释掉。
  2. 保持断点。
  3. 运行项目
  4. 如果在main.m中不调用类,则什么都不会有。类和分类都是懒加载的。
  5. main.m中调用类,再运行项目 :

图片.png

  1. 根据上面的经验,分类的数据是在编译期就被放入了类的ro中。

小结 :

懒加载类 + 两个分类懒加载 :

  1. 如果类不被调用,则类和分类都不会加载
  2. 分类的数据会在编译期放入类的ro中
  3. 当类被调用,会通过lookUpImpOrForward调用到realizeClassWithoutSwift,完成类的加载。

五、总结

  1. rwe的创建是在attachCategories中。如果类已经有了rwe就是获取,如果没有就是创建。
  2. 分类如果实现了类的同名方法(类方法和实例方法分开,分类实现了类已有的分类方法,或者实现了类已有的类方法,不能是类中的方法是实例方法,分类实现的是同名的类方法,这种无效),按照类的加载顺序,最终调用同名方法,会调用到最后加载的分类中的同名方法的实现。
  3. 结合上一章的情况 :

(1). 如果类是非懒加载的 :

<1>. 分类无论有多少个,如果有1个分类是非懒加载的,其他分类都会提前加载。

数据加载流程如下 :

不会进入attachToClass,而是通过load_images--->loadAllCategories--->load_categories_nolock--->attachCategories,完成向rwe中放入分类数据。

<2>. 分类都是懒加载的

分类的数据会在编译期就写入类的ro中。

(2). 如果类是懒加载的 :

<1>. 分类至少2个是非懒加载的,所有分类都会提前加载实现,并且使主类也提前加载实现。

数据加载流程如下 :

load_images--->loadAllCategories--->addForClass--->prepare_load_methods--->realizeClassWithoutSwift--->attachToClass--->attachCategories

<2>. 分类只有1个是非懒加载的,其余是懒加载的

分类会使主类提前加载,但是所有分类的数据是在编译期被放入ro中。

<3>. 所有分类都是懒加载的

分类的数据都会在编译期放在类的ro中。