本文由快学吧个人写作,以任何形式转载请表明原文出处
一、资料准备
对应mac的版本是11.1。可根据自己的系统版本挑选可以进行调试的源码。
二、思路
- 类的加载(上)和类的加载(下)中知道了非懒加载类和懒加载类是如何从镜像加载到内存中并实现的。
- 在
methodizeClass
源码中有关分类的处理还没有看过。那么分类的本质是什么? - 分类是如何和主类联系上的,分类又是怎么实现的?
三、分类的本质
设置main.m中的代码,创建一个JDMan的分类 :
通过clang编译main.m文件,生成编译文件查看分类的本质。一定要进入到main.m所在文件夹下,再执行clang命令。
clang -rewrite-objc main.m -o main.cpp
1. 本质是结构体category_t
在main.cpp中可以看到分类的本质是结构体category_t
。
在818源码中可以搜索一下:struct category_t
name : 分类的名字
cls : 哪个类的分类
instanceMethods : 分类的实例方法列表,类型为
method_list_t
。classMethods : 分类的类方法列表,类型为
method_list_t
。protocols : 分类的协议列表,类型为
protocol_list_t
。instanceProperties : 分类中的属性,类型为
property_list_t
。官方注释 :下面的不常见,不说了。
2. 分类的特点
- 在分类中通过通过
@property
添加的属性是没有setter和getter方法的,和类不一样。- 分类的属性添加setter和getter方法可以通过关联对象来设置。后面再说。
在上面编译的main.cpp文件中也可以看出来,分类中的方法列表中不自带setter和getter方法 :
四、探索分类加载的准备
1. 分类加载的总体思路
分类的加载就要分成4种情况了,因为分类是依附于类的。
删除掉main.m中创建的JDPerson分类,以文件的形式来创建一下。用来测试分类的加载。创建结果如下图 :
main.m如下图 :
2. 分类的加载是从哪里进行的
- 思路 :
- 类的实现都是通过
realizeClassWithoutSwift
,无论是非懒加载类还是懒加载类。- 在
realizeClassWithoutSwift
中的methodizeClass
中发现了官方注释Attach categories.
- 在官方注释下面的
methodizeClass
源码中的attachToClass
源码中发现了attachCategories
,通过命名猜测是和分类相关。attachCategories
源码中有对rwe的操作。而rwe是运行时动态修改类才会出现的结构。
-
全局搜索
attachCategories
,找到load_categories_nolock
出现了对它调用。 -
全局搜索
load_categories_nolock
,找到了loadAllCategories
。
- 全局搜索
loadAllCategories
,找到了load_images
-
load_images
是在之前的objc_init中注册回调dyld,使其加载镜像的函数。 -
在map_images一章中见过
load_images
。是通过dyld的注册回调,用来加载镜像。 -
从搜索的流程看,是符合map_images这章中的
read_images
中对分类的处理的官方注释的。 -
那么实现类的
realizeClassWithoutSwift
中的methodizeClass
,它的attachToClass
是没有对类的分类进行操作吗?
3. 创建分类进行测试
1. JDMan和main.m中的代码
main.m :
这里删除掉了上面探索分类本质的时候在main.m中创建的分类代码。
JDMan :
2. 创建分类
3. 测试用的自己写的代码段 :
代码段1 :
arduino
const char *qudaodemingzi = cls->mangledName();
const char *JDManName = "JDMan";
if (strcmp(qudaodemingzi, JDManName) == 0) {
printf("找到自己定义的类了 : %s 从函数 : %s 拿到的\n",qudaodemingzi,__func__);
}
代码段2 :
ini
const char *qudaodemingzi = cls->mangledName();
const char *JDManName = "JDMan";
if (strcmp(qudaodemingzi, JDManName) == 0) {
auto jd_ro = (const class_ro_t *)cls->data();
auto jd_isMeta = jd_ro->flags & RO_META;
if (!jd_isMeta) {
printf("找到自己定义的类了:%s 从函数 : %s 拿到的\n",qudaodemingzi,__func__);
}
}
代码段3 :
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__);
}
}
4. 将代码段贴到源码中
用到的代码段不一样,也是根据类的实现情况来确定的。可以自行调整。
read_images
中,找到非懒加载类的实现,用代码段1 :
realizeClassWithoutSwift
中,用代码段2 :
methodizeClass
中,用代码段3 :
attachToClass
中,在if判断中分别加了代码段3 :
(1). attachToClass
的if外部 :
(2). attachToClass
的if内部 :
- 进入
load_categories_nolock
,加入代码段3 ,稍作修改,将printf
里面直接改成load_categories_nolock
,因为这是个迭代器,所以__func__
只会显示operator()
,改成如下图 :
- 进入
attachCategories
,加入代码段3 :
五、只有一个分类,分类的加载情况
就如上面创建的,JDMan只有一个分类JDMan(LA)。
1. 类非懒加载+分类非懒加载
在read_images
、realizeClassWithoutSwift
、load_categories_nolock
、attachCategories
中自定义的代码段中加入断点,:
运行项目,结果如下 :
- 先进入了
read_images
的断点 :
- 再进入了
realizeClassWithoutSwift
的断点 :
- 然后进入了
load_categories_nolock
的断点 :
- 最后进入了
attachCategories
的断点 :
- 再运行,就会完成程序的运行,得到了流程如下 :
- 可以发现,是没有进入
attachToClass
的if
判断的。
小结 :
非懒加载类 + 非懒加载分类的加载 :
(1). 非懒加载类通过:
read_images
--->realizeClassWithoutSwift
--->methodizeClass
--->attachToClass
先进行加载和实现。(2). 非懒加载分类通过 :
load_images
--->loadAllCategories
--->load_categories_nolock
--->attachCategories
再进行加载和实现。并没有进入attachToClass
的if
判断的流程。
2. 类非懒加载+分类懒加载
首先注释掉JDMan(LA).m
这个分类中的+(void)load
方法。分类变成懒加载。
运行项目,结果如下 :
- 先进入了
read_images
的断点 :
- 然后进入了
realizeClassWithoutSwift
的断点 :
- 项目直接运行结束,看自定义代码段打印的结果 :
-
并没有进入
load_images
的流程。那么分类是否是和类一起加载的? -
重新运行项目,将断点走到
realizeClassWithoutSwift
里面。然后向下执行断点,到auto isMeta = ro->flags & RO_META;
这一行 :
- 利用lldb查看ro的数据 :
小结 :
非懒加载类 + 懒加载分类 :
懒加载分类的数据会在编译的时候就和非懒加载类一起被存入了非懒加载类的ro中。在非懒加载类加载的时候,一起加载到内存。不需要经过
attachCategories
等步骤再进行加载。
3. 类懒加载+分类非懒加载
注释掉JDMan
类中的+(void)load
。打开JDMan(LA)
分类中的+(void)load
。
运行项目,结果如下 :
- 先进入了
read_images
的断点 :
2.然后进入了realizeClassWithoutSwift
的断点 :
- 项目直接运行结束,看自定义代码段打印的结果 :
- 并没有进入
load_images
的流程。那么分类非懒加载是否会使类也成为非懒加载?在read_images
非懒加载类的循环那里,加入如下测试代码,如图 :
- 再次运行818.2项目。
JDMan
出现在了非懒加载类中,证明非懒加载的分类使类也变成了非懒加载的。那么分类的数据是否也已经存储到了类中?重新运行818.2,然后使断点执行到realizeClassWithoutSwift
中,移动断点到ro下面,查看ro中的数据。
小结 :
懒加载类 + 非懒加载分类 :
非懒加载的分类会使懒加载的类变成非懒加载,分类数据和类的数据在编译期一起被写入ro。在类加载的时候,一起加载到内存。不需要经过
attachCategories
等步骤再进行加载。
4. 类懒加载+分类懒加载
打开类和分类的+(void)load
方法。在main.m
中让类被调用一次,例如在main.m
加入JDMan *aMan = [[JDMan alloc] init];
。
运行项目,结果如下 :
- 进入了
realizeClassWithoutSwift
的断点,只不过是通过lookUpImpOrForward
进入的 :
-
然后就直接运行完成,不会走其他的断点。
-
那么分类是否也和上面一样,直接和类一起完成了加载?重新运行项目,看ro的数据 :
小结 :
懒加载类+懒加载分类 : 类会在第一次被调用的时候完成实现和加载,分类的实现和加载与类一起完成。分类的数据存储在ro中。
六、总结
- 分类本质是结构体
category_t
。 - 分类中添加的属性不会自动生成setter和getter方法,需要用到关联对象。
- 如果只有一个分类的情况下,分类的实现和加载情况如下 :
(1). 非懒加载类 + 非懒加载分类 :
非懒加载类的加载 :
map_images
--->read_images
--->realizeClassWithoutSwift
--->methodizeClass
--->attachToClass
非懒加载分类的加载 :
load_images
--->loadAllCategories
--->load_categories_nolock
--->attachCategories
只有一个分类,非懒加载分类不会进入类的
methodizeClass
--->attachToClass
--->attachCategories
流程中进行加载。(2). 非懒加载类 + 懒加载分类 :
分类的数据会在编译期就被写入类的ro中。伴随着类的加载完成加载。不进入
attachCategories
。(3). 懒加载类 + 非懒加载分类 :
非懒加载分类会使懒加载的类变成非懒加载的类,分类的数据也会在编译期就被写入到类的ro中。分类会伴随着类的加载完成加载。不进入
attachCategories
。(4). 懒加载类 + 懒加载分类 :
分类的数据也会在编译期就被写入类的ro中。当类第一次被调用,通过
lookUpImpOrForward
进行实现和加载的时候,分类伴随着一起完成。不进入attachCategories
。