本文由快学吧个人写作,以任何形式转载请表明原文出处
一、资料准备
对应mac的版本是11.1。可根据自己的系统版本挑选可以进行调试的源码。
二、思路
-
map_images,通过名字知道也知道,它是映射镜像的函数,那么它做了什么,需要映射什么内容到内存上给app使用?我们写的代码是否是在这里被映射到了内存上?
三、寻找map_images源码的核心
1. 找到map_images
在送给_objc_init()
也就是objc库的初始化函数中,是由dyld的回调函数来真正执行的。
2. 为什么map_images有&
取地址符?
因为map_images内容很多,也很重要,执行代码也耗时。
如果dyld在执行的过程中,由于某些原因,导致map_images函数的地址发生了变化,那么使用&
进行指针传递,就可以保证dyld可以正确调用到map_images,而不会受到地址变化的影响。
3. map_images源码
1. 官方注释 : map_images函数是用来处理dyld正在映射的镜像。
2. map_images_nolock
源码很长,我的主要目的是找被映射的objc镜像都做了什么,所以找到了最后的,读取镜像:_read_images
。
另外看一点,是下面2. 修复@selector的相关内容
中会用到的namedSelectors
这个哈希表怎么来的,源码也在这里。
3. _read_images
读取镜像是映射镜像的重点。源码很长,下面先进行大的思路的整理。
四、_read_images
注 : 怎么看_read_images
看官方注释,有注释的一般会比较重要。然后找我们了解的点,开发经常接触的东西来看。
由于源码太长,有的适合粘贴源码的,就粘贴源码在下面,可以用图片的,直接截图源码的图片,图片里有注释。
1. 变量定义,遍历头文件,是否第一次进入_read_images
ini
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
header_info *hi;
uint32_t hIndex;
size_t count;
size_t i;
Class *resolvedFutureClasses = nil;
size_t resolvedFutureClassCount = 0;
static bool doneOnce;
bool launchTime = NO;
TimeLogger ts(PrintImageTimes);
runtimeLock.assertLocked();
// 遍历每一个头文件,都要执行下面的源码
#define EACH_HEADER \
hIndex = 0; \
hIndex < hCount && (hi = hList[hIndex]); \
hIndex++
// doneOnce是表示if里的代码只执行一次
// 也就是说,不是每一个头文件执行下面代码的时候都要进if里面,只有第一个头文件才会进
// 因为doneOnce在上面定义的时候是static,是局部静态变量
if (!doneOnce) {
doneOnce = YES;
launchTime = YES;
...
忽略的源码
...
// namedClasses
// Preoptimized classes don't go in this table.
// 预先优化的类不在这个表中
// 4/3 is NXMapTable's load factor
// 4/3是NXMapTable的负载系数,也就是说,创建表的时候,创建的大小是实际需求容量的4/3
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
//这是一个NXMapTable类型的变量,是个表。
//点进去有官方注释说 : 命名出现了失误,实际上这个表是用来存储没有在dyld共享缓存里面缓存的类,无论这个类是否实现过
gdb_objc_realized_classes =
NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
ts.log("IMAGE TIMES: first time tasks");
} //注释 : if(doneOnce)的判断结束。
#define EACH_HEADER
是遍历头文件,从_read_images
源码的开始到结束才有#undef EACH_HEADER
。这说明读取镜像文件,是遍历读取的。- doneOnce的判断语句中的代码,只有读取第一个头文件的镜像的时候,才会执行。以后都不会执行if中的代码。
- 唯一一次执行if,创建了一个哈希表,用来存储dyld的共享缓存中没有进行缓存的类,无论这个类是否实现过。
2. 修复@selector的相关内容
- 为什么
_getObjc2SelectorRefs
是获取mach-o静态段中的__objc_selrefs
内容?为什么sels是一个SEL *
这样的指针类型?
答 : 进入_getObjc2SelectorRefs
源码 :
所以这个函数是取mach-o文件中的_getObjc2SelectorRefs
的所有字段内容,不单单是一个sel,而是很多。所以需要用SEL *
来获取mach-o中的整个__objc_selrefs
的地址。
- 插入到namedSelectors哈希表是怎么回事?
答 : 进入sel_registerNameNoLock
源码,只封装了一行代码,再进入封装的这个__sel_registerName
:
- 举例 : 在
sels[i] = sel;
这行源码打上断点,运行objc818.2 :
3. 类的一些处理
- 类的处理是一个重点,下一章单独开一章了解
readClass
的源码。 - 举例 : 在
Class cls = (Class)classlist[i];
这句代码处挂上断点。运行程序
可以看到此时的cls只有地址。如果断点换到了readClass
执行之后,类则有了名称和地址 :
4. 修复,重新映射一些没有加载到内存的类和父类
不是重点。一般都进不来if判断。
5. 老版本的objc_msgSend修复(兼容老版本)
6. 如果类里面有协议,读取协议
7. 修复,重新映射一些没有加载到内存的协议
8. 分类的处理
9. 非懒加载类的实现,或者说非懒加载类的内容加载
10. 处理未被处理的类
五、总结
映射镜像也就是map_images的核心是读取镜像(_read_images)。
读取镜像源码的主思路流程 :
- 修复sel的地址。让sel的地址变成真正的内存中的地址。
- 类的基本处理。在内存中保存了类的名称和地址,但是没有类的其他内容。
- 重新映射那些没有被成功映射的类和父类。
- 兼容老版本的objc_msgSend。
- 如果有协议,则读取协议。
- 重新映射那些没有被成功映射的协议。
- 分类的处理。分类的处理要推迟到_dyld_objc_notify_register完成之后,第一次进行load_images(加载镜像)时再发现分类
- 非懒加载类的实现,也就是对非懒加载类的内容(不止是2中的名称和地址)进行加载。
- 处理未被处理的类。