本文由快学吧个人写作,以任何形式转载请表明原文出处
一、资料准备
对应mac的版本是11.1。可根据自己的系统版本挑选可以进行调试的源码。
二、思路
既然说到了类的加载,那么必然要知道类的结构,在类的本质这里说过,类是个结构体,叫objc_class,是继承于objc_object的,objc_class结构体内有4个成员,也就是类有四个成员 : isa、superClass、cache、bits。
- 上一章map_images映射镜像中,在读取镜像的函数
_read_images
里面找到了类从镜像被加载到内存的步骤,也就是上一章总结里面的2和8。 - 那么类是如何加载到内存中的?类的结构中的数据又是如何加载到内存中的?
- 核心函数是 :
readClass
(加载了类的名称,并和地址关联上)、realizeClassWithoutSwift
(加载了类的其他内容)。
三、readClass
1. 找到readClass源码
- 在objc4-818.2 ---> 搜索_objc_init(void) ---> map_images ---> map_images_nolock ---> _read_images。
- 找到类处理的for循环。找到
readClass
如下 :
2. 进入readClass源码
- 解析都在图中的注释中。
- readClass对正常的类做的事情是 : 将类添加到
gdb_objc_realized_classes
表和allocatedClasses
表中,如果类已经在allocatedClasses
表中存在,则只添加元类。 - 添加到
gdb_objc_realized_classes
表中是通过addNamedClass
函数。 - 添加到
allocatedClasses
表中是通过addClassTableEntry
函数。 gdb_objc_realized_classes
表的创建是在上一章的四--->1,也就是doneOnce里面创建的。存储的是没有在dyld的共享缓存中存在的类,无论这个类是否实现过。allocatedClasses
表的创建是在上上章objc_init中的runtime_init()创建的。存储的是已经分配过内存的类。gdb_objc_realized_classes
可以看作是allocatedClasses
的超集,是包含了allocatedClasses
表中的内容的。- 为什么要创建两张表?答 : 方便或者说优化查找。不用每次查找都找大的表(
gdb_objc_realized_classes
)。已经分配过内存的类可以直接到小表allocatedClasses
中查找。 addNamedClass
源码 :
addClassTableEntry
源码 :
3. readClass源码总结
- 将类添加进
gdb_objc_realized_classes
表中,也就是添加到 :dyld共享缓存中没有的类的表中。allocatedClasses
表中如果没有类,也添加到allocatedClasses
表中,这个表是所有分配过内存的类的表。将元类也添加进来。- readClass之后,类有名称和地址,但是类的内容,包括isa、superclass、cache、bits都还没有数据。也就是都还没有加载到内存中。
四、realizeClassWithoutSwift
1. 找到realizeClassWithoutSwift源码
-
在objc4-818.2 ---> 搜索_objc_init(void) ---> map_images ---> map_images_nolock ---> _read_images。
-
找到实现类的for循环中 :
2. 进入realizeClassWithoutSwift源码
1. 判断类是否存在,判断类是否实现过
2. 将类的rw和ro数据加载进去。
3. 递归实现父类和元类的rw和ro的数据加载
4. 设置类的isa,看是否需要设置成纯净的isa
5. 更新类的父类和元类的数据,修正内存偏移,设置类的实例的大小,设置rw的flags,将子类添加到父类的子类列表里面
6. 修复类的方法、属性、协议的列表的存储。处理分类相关
五、源码调试realizeClassWithoutSwift
源码调试的目的是为了直观的看到,rw和ro到底是在哪一步就加到了cls中的。rwe是不是也在这里加到了cls中。
有关ro,rw,rwe的内容在有关ro和rw这里。
1. 在objc-818.2的KCObjcBuild文件夹下创建自己的类,并在类的.m文件中实现+load
之所以要实现+load是因为realizeClassWithoutSwift
的官方注释里面写了,实现的是非懒加载的类。正常的JDMan是懒加载的,添加上+load方法,就会让它的加载时机提前,不必再等到调用时再加载。
2. 需要用的代码,自己写的
这两段代码是为了在调试realizeClassWithoutSwift
方法时直接定位到自定义的类,过滤掉系统的类。
代码段1 :
arduino
//mangledName是在objc_class的结构体中找到的,在上面的readClass里面有一
//个相似的nonlazyMangledName,所以找到了它
const char *qudaodemingzi = cls->mangledName();
const char *JDManName = "JDMan";
if (strcmp(qudaodemingzi, JDManName) == 0) {
printf("找到自己定义的类了");
}
代码段2 :
scss
//仅仅比上面的代码多了一个从类的ro中取出flags,用来保证探索的不是元类
//毕竟元类我只能找到类方法,但是类可以通过lldb找到rw和ro中的属性和实例方法
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("找到自己定义的类了");
}
}
3. 将两个代码段放到如下位置
- read_images源码里面,找到实现懒加载类的源码,将代码段1加入如下位置 :
- realizeClassWithoutSwift源码里面,将代码段2加入如下位置 :
4. 断点怎么打
一定要一个断点,一个断点的使用,先断到了自己创建的类,再给别的地方打断点。因为read_images是循环遍历mach-o静态段去加载类,每一个非懒加载类都会走到realizeClassWithoutSwift
。直接打上所有的断点,走到断点上的时候,cls就不一定是不是自己创建的类了。
所以先断点如下 :
5. 断点调试
- 运行项目,断到如上面的图中3824行的位置。找到自定义的JDMan类
- 再添加断点,添加到
realizeClassWithoutSwift(cls, nil)
位置。让断点跳到这里。
- 进入
realizeClassWithoutSwift
函数,在自己写的代码段2的位置打上断点。然后让断点跳到这里。
- 然后点stepOver,跳断点,看能不能执行到如下位置,如果可以,证明现在的cls就是自定义的JDMan类,不是JDMan元类。
- 向下走断点,可以看下ro是什么,这个ro是mach-o中的ro,还没有加载到内存中,也没有放入到类的rw中。断点走到如下位置,lldb看ro。
- 向下走断点,会跳过下面的if,进入到else。先走到如下图位置。具体内容看图。
- 再向下走断点,会发现rw的内存空间已经分配好了,rw是一个崭新的rw,没有任何的数据存在里面。
不确定的话,可以用lldb看看。lldb调试如下 :
- 继续向下走断点。过了
rw->set_ro(ro)
之后,rw的ro就被设置好了。
此时的rw的ro有数据了。自己内部的函数,例如methods()
也可以从ro中获取到数据了。可以lldb试一下从rw拿到数据,如下图 :
为什么说methods()
函数是从ro中获取到数据?还能从哪里获取?通过上面的源码rw->set_ro(ro);
,进入到set_ro()
的源码。
看那个判断if(v.is)
这里的,类型是rwe的话,才会走这里。现在的rwe还没有值呢。可以验证rwe此时是没有值的,如下图操作 :
- 断点还是在这个位置,不要移动。看cls中的rw和ro有值吗?
cls的ro和rw是没有值的。因为rw还没有被赋值给cls的rw。赋值是在下面的一步 :cls->setData(rw)
- 移动断点到如下图所示,再看cls的rw和ro,已经有了数据,说明
cls->setData(rw)
这一步就已经给cls的rw和ro赋值 :
看cls的rw中的数据 :这里我只看了methods,属性其实也已经出来了,也可以看,没有截图。
看cls的ro中的数据 :
6. 总结
由上面的代码调试可以总结出,在
cls->setData(rw);
之后,类的rw和ro就已经有了数据了。
六、有关methodizeClass
放在下一章
methodizeClass
内容有关分类,放在分类之后。