本文由快学吧个人写作,以任何形式转载请表明原文出处
一、资料准备
和上一章相同。本章主要看多个分类的加载和实现,以及一些源码的解析。
二、项目准备
更改JDMan
中的代码,删除没必要的属性和实例变量。只保留两个方法。
更改JDMan(LA)
分类中的代码,也只保留两个方法。
更改后的代码如下 :
1. main.m :
2. JDMan :
3. JDMan(LA) :
三、重点函数的解析
1. prepareMethodLists
出现的位置 :methodizeClass
和attachCategories
。
作用 : 对方法进行排序
核心代码 : fixupMethodList
。
fixupMethodList的源码 :
小结 :
方法的排序是通过SEL的地址来完成的。方法的SEL会加入到SEL列表中。
2. attachCategories
出现的位置 : attachToClass
、load_categories_nolock
。
其中load_categories_nolock
在loadAllCategories
中是循环遍历头文件信息,循环调用load_categories_nolock
。
作用 : 初始化rwe,将类的ro中的数据、分类的数据放入rwe中。
attachCategories的源码 :
只贴了重要的,部分打印不太重要的没有加进来。
attachCategories的源码调试
- 让
JDMan
类和JDMan(LA)
分类全部都实现+(void)load
方法,因为通过上一章可知,非懒加载类和非懒加载分类会通过load_images
进入到attachCategories
。 - 在
attachCategories
加入自定义代码段,为了定义到正确的JDMan
类的加载流程中。
ini
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__);
}
}
- 在自定义代码的
printf
上打断点。运行程序
- 可以看一下
mlists
,这个总表是什么,lldb看mlists
:
中间省略,直接到最后 :
最初的mlists
全部都是内存的地址,但是没有内容可以读取。
- 断点一直向下,移动到rwe的初始化。先看rwe未进行初始化的时候,cls的rwe是什么样的 :
- 断点继续向下,等rwe初始化完成后,看rwe和类的rwe :
rwe :
类的rwe :
因为extAllocIfNeeded
就是在类的内部开辟了rwe的内存,然后将ro的数据贴进去。这个函数的返回值就是class_rw_t
类型,也就是rwe。
- 从
load_categories_nolock
进入到attachCategories
,cats_count
全都是1,参数是写定的。因为load_categories_nolock
和外部的loadAllCategories
都会循环遍历头文件,找到分类相关的数据。因为是循环,所以每个循环内,都是只添加一个分类数据,下一个分类自然会再外面的循环后再进入这里,完成添加。
- 移动断点,看
entry
:
- 移动断点,看
mlist
,这里注意,mlist
只是一个分类的方法列表。上面的mlists
是一个类的所有分类的方法列表的合集。mlists
是二维的结构。
- 再移动断点是不会进入
if (mcount == ATTACH_BUFSIZ)
的,官方注释说了,因为一般正常的类不会有64个分类。所以直接跳到下面,看mlists,和上面的4中是一样的 :
- 再移动断点,mlists就不一样了,因为mlists的最后一位插入了mlist,也就是当前分类的方法列表 :
- 跳过属性和协议的添加,和方法的添加是一样的,只以方法举例。直接到
if (mcount > 0)
,只要分类添加了方法,那么就会进入11到中,mcount
一定是自增的,所以只要分类添加了方法,mcount
至少为1。
- 对分类的方法进行排序前 :
- 对分类的方法进行排序后 :
- 将分类方法贴入类的rwe中,先看贴进去之前的rwe的数据,会是类的ro的数据 :
- 贴入后的rwe的数据,这里注意,lldb的命令不一样了 :
如果按照以前的lldb取rwe的methods数据 :
lldb在换成如下,就可以显示rwe中方法数据的,证明通过了rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
,分类的方法被贴入到了rwe中 :
小结 :
- 设置一个总的方法列表,用来装分类的方法列表,总的列表里面装的是分类的方法列表,也就是二维的。
- 总的列表的容量是64。因为苹果认为正常的情况是不会出现一个类创建了64个以上的分类的。
- 给类开辟rwe空间。如果rwe之前就已经存在,则是获取rwe。
- 根据要加入的分类数量进行遍历循环。将分类的方法列表放入总的列表里面。这里的
mcount
自增目的是将后加入的分类中的方法放到先加入的分类方法的前面。这也就解释了为什么如果后加入的分类重写了之前分类的方法,在调用的时候,调用到的是后加入的分类的方法。- 对分类方法进行排序。(这里通过内存平移。传入排序函数的,会是以平移到的位置为开始,到整个列表最后,这一段中的所有分类方法列表。举例 :相当于一个数组是1,2,3,4,5,假设所有添加的分类方法,不论几个分类的,是所有分类的方法,最终占据了3,4,5三个位置,这个内存平移,就会把3,4,5全部传入,最终排序的是3中的内容排序,然后4中的内容排序,最后5中的内容排序,都是根据sel地址进行排序)。
- 将分类的方法贴入到rwe中。
3. extAllocIfNeeded
出现的位置 :attachCategories
。
作用 :
(1). 如果类没有rwe,就让类创建rwe,并将类的ro数据复制给rwe。
(2). 如果类已经有了rwe,就获取rwe。
核心代码 : extAlloc
,在类没有rwe的时候会调用extAlloc
创建rwe。
extAlloc源码 :
小结 :
- 分配一块内存给rwe
- 给rwe的version赋值,通过ro的flags,让rwe也能判断当前类是否是元类。
- 从ro中获取数据方法列表
- 如果方法列表存在,就把ro中的方法列表赋值给rwe。属性和协议同理。
- 设置rwe的ro为类的ro,让rwe也可以读取到ro的信息。
4. attachLists
出现的位置 : extAlloc
、attachCategories
、methodizeClass
。
作用 : 将数据添加到指定的数组列表中。
attachLists源码 :
主要就是三种不同情况的方法插入 :
- 多对多 :
列表数组中本身就有多个方法列表,然后又插入了多个方法列表。也就是所谓的多对多。
- 0对1 :
本来就没有列表数组,也没有列表。插入要插入的列表。
- 1对多 :
小结 :
- 多对多 : 本身就存在了列表数组,又插入了新的列表,则设置新的列表数组,容量是旧的数量+新列表的数量。然后根据这个容量进行开辟内存。循环遍历,将旧的列表按照原有顺序,放到新列表数组的后面。新插入的列表则按照顺序,循环遍历放入新列表的前面。
- 0对1 : 本身没有列表,也没有列表数组,将新进入的列表赋值给
list
。- 1对多 : 原来只有一个列表,新插入了多个列表,则分配一块内存,分配内存的大小根据1(或者是0)+新插入列表的数量。内存中存放的是数组指针,也就是开辟的是一个数组,也就是列表数组。将原有的1个列表放到列表数组的最后。新插入的列表,根据顺序,循环遍历从数组的头部加入
四、多个分类的加载
上一章探索了只有1个分类的情况。
这里探索下1个类有多个分类的情况,以2个分类来举例。
1. 项目变动
给JDMan
类新添加一个分类JDMan(LB)
,其内部实现如下 :
2. 思路
- 非懒加载类 + 两个分类非懒加载
- 非懒加载类 + 一个分类非懒加载 + 一个分类懒加载
- 非懒加载类 + 两个分类懒加载
- 懒加载类 + 两个分类非懒加载
- 懒加载类 + 一个分类非懒加载 + 一个分类懒加载
- 懒加载类 + 两个分类懒加载
3. 代码段准备
和上一章中的代码段准备相同。加入的地方也相同。
和之前不同的地方,在load_categories_nolock
和attachCategories
的打印中,加入分类的信息 :
load_categories_nolock
:
attachCategories
:
在JDMan
类、JDMan(LA)
分类、JDMan(LB)
分类中加入同名的方法- (void)ceShi;
并实现。然后在main.m
中调用。
JDMan
:
JDMan(LA)
:
JDMan(LB)
:
4. 非懒加载类 + 两个分类非懒加载
-
JDMan
、JDMan(LA)
、JDMan(LB)
中的+(void)load
方法不进行注释。 -
运行项目,结果如下 :
- 在
attachToClass
和attachCategories
里面的printf
那里打上断点,看一下堆栈信息。
attachToClass
:
attachCategories
:
堆栈信息中显示的调用路径是和上一章相比,并没有新的函数出现。
小结 :
- 非懒加载类+两个非懒加载分类 :
类也是通过
read_images
中的realizeClassWithoutSwift
实现了类的实现。分类则是通过
load_images
--->loadAllCategories
--->load_categories_nolock
--->attachCategories
完成了数据的加载,并且类在attachCategories
中开辟了rwe。
- 从项目运行结果来看,类和分类中的同名方法,普通的调用,一定是会调用到分类的方法。并且会调用到后加载完成的分类的方法,和谁先创建无关。
5. 非懒加载类 + 一个分类非懒加载 + 一个分类懒加载
-
JDMan
和JDMan(LA)
中的+(void)load
方法不注释。JDMan(LB)
的+(void)load
注释掉,变成懒加载类。 -
保持
attachToClass
和attachCategories
中的断点。 -
运行项目,看堆栈信息和控制台打印信息
attachToClass
:
attachCategories
:
打印信息 :
小结 :
非懒加载类 + 一个分类非懒加载 + 一个分类懒加载 :
类非懒加载的情况下,只要一个分类是非懒加载的,那么其他的分类也会变成非懒加载。
6. 非懒加载类 + 两个分类懒加载
-
JDMan
保留+(void)load
。JDMan(LA)
和JDMan(LB)
注释掉+(void)load
。 -
保持
attachToClass
和attachCategories
中的断点。 -
运行项目,结果如下 :
- 继续执行断点,并不进入
attachCategories
的断点。项目运行完成。
- 但是看打印的结果,分类的方法明显进入了类的
ro
中。也就是在编译期,两个非懒加载分类的方法都写入了类的ro。和上一章中的除非懒加载类+非懒加载分类之外的三种情况是一样的。
小结 :
非懒加载类 + 两个懒加载分类:
非懒加载类的情况下,如果多个分类都是懒加载的,那么分类的数据会在编译期就被写入类的ro中。
7. 懒加载类 + 两个分类非懒加载
-
注释掉
JDMan
类中的+(void)load
方法。保留JDMan(LA)
分类和JDMan(LB)
分类的+(void)load
方法。 -
保持
attachToClass
和attachCategories
中的断点。 -
运行项目会发现报错 :
原因 :
(1). cls是从分类的结构体成员中获得的cls,但是类我们并没有实现。
(2). 看调用堆栈,是从load_images
开始的。因为类是懒加载的,所以在map_images
的时候,并没有实现JDMan
这个类。那么JDMan
的对象就是无法调用到isMetaClass
函数的。
改正 :
将auto jd_isMeta = cls->isMetaClass();
注释掉,获取元类的情况换成如下,直接从类的ro中读取 :
ini
auto jd_ro = (const class_ro_t *)cls->data();
auto jd_isMeta = jd_ro->flags & RO_META;
- 改正后,重新运行项目,发现终于进入了
attachToClass
。
- 发现是从
prepare_load_methods
进入的,再看控制台的打印信息,已经进过了load_categories_nolock
函数。证明loadAllCategories
也是执行过的。可以验证一下,给load_categories_nolock
中的printf
打上断点。重新执行程序
-
上图证明了,两个非懒加载分类+懒加载类,分类会因为实现了
load
方法,先在load_images
中被处理,而类因为是懒加载的,所以在map_images
中并未被处理。 -
这时进入
load_categories_nolock
,就不会从这里进入attachCategories
,因为cls并没有被实现。可以断点向下看一下,会进入如下流程 :
- 经过
load_categories_nolock
。会继续load_images
的流程,进入prepare_load_methods
,然后在这里进入realizeClassWithoutSwift
,然后进入attachToClass
,通过attachToClass
进入attachCategories
完成。
小结 :
懒加载类 + 两个分类非懒加载 :
- 非懒加载类会先进入
load_images
,但是因为类是懒加载的,所以类在map_images
的时候不会进行实现。进而分类的流程不会进入attachCategories
去完成类的rwe的初始化和将方法放入类的rwe中。分类会被加入到和类的关联中。- 非懒加载的分类会进入
prepare_load_methods
,在这里会进入realizeClassWithoutSwift
完成类的实现,使类加载完成,不会等到类第一次被调用再完成类的加载。然后通过attachToClass
进入attachCategories
,完成类的rwe的初始化,和将分类的数据放入rwe中。
8. 懒加载类 + 一个分类非懒加载 + 一个分类懒加载
-
- 注释掉
JDMan
类和JDMan(LA)
分类中的+(void)load
方法。JDMan(LB)
分类的+(void)load
方法。
- 注释掉
- 保留上面调试的时候的断点。
- 运行项目
- 控制台的打印信息 :
- 根据上面的经验,分类的方法是在编译期就放入了类的ro中。
小结 :
懒加载类 + 一个分类非懒加载 + 一个分类懒加载 :
一个分类非懒加载,一个分类懒加载,会使类变成非懒加载。在编译期就会将分类的数据放入类的ro中。
9. 懒加载类 + 两个分类懒加载
- 将
JDMan
类、JDMan(LA)
分类、JDMan(LB)
分类中的+(void)load
全都注释掉。 - 保持断点。
- 运行项目
- 如果在
main.m
中不调用类,则什么都不会有。类和分类都是懒加载的。 - 在
main.m
中调用类,再运行项目 :
- 根据上面的经验,分类的数据是在编译期就被放入了类的ro中。
小结 :
懒加载类 + 两个分类懒加载 :
- 如果类不被调用,则类和分类都不会加载
- 分类的数据会在编译期放入类的ro中
- 当类被调用,会通过
lookUpImpOrForward
调用到realizeClassWithoutSwift
,完成类的加载。
五、总结
- rwe的创建是在
attachCategories
中。如果类已经有了rwe就是获取,如果没有就是创建。- 分类如果实现了类的同名方法(类方法和实例方法分开,分类实现了类已有的分类方法,或者实现了类已有的类方法,不能是类中的方法是实例方法,分类实现的是同名的类方法,这种无效),按照类的加载顺序,最终调用同名方法,会调用到最后加载的分类中的同名方法的实现。
- 结合上一章的情况 :
(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中。