iOS 底层探索篇 ——类的加载原理(上)
1. objc_init 做了什么
上文说到了objc_init调用了_dyld_objc_notify_register,初始化了dyld 里面的sNotifyObjCMapped,sNotifyObjCInit,sNotifyObjCUnmapped函数并对 sNotifyObjCMapped,sNotifyObjCInit直接进行了调用,那么objc_init还做了什么呢?
首先看到environ_init,这里面主要做了环境变量的初始化。那么环境变量有什么用呢?先把环境变量打印出来。
然后运行,发现有很多的环境变量,其中有OBJC_DISABLE_NONPOINTER_ISA和OBJC_PRINT_LOAD_METHODS。
NONPOINTER_ISA就是不纯净的isa,里面不仅包含了类的信息,还有其他的一些信息。把 OBJC_DISABLE_NONPOINTER_ISA设为YES,那么就不会有NONPOINTER_ISA,就会得到一个纯净的只包含类的信息的isa。
这个是没有将OBJC_DISABLE_NONPOINTER_ISA设为true之前的isa,可以看到里面包含有其他的信息,因为类信息在x86_64架构里是只有44位的,而这里不止44位有值。并且直接po isa 是得不到类的信息的。
点开editScheme,然后在Arguments里面的Environment variables 添加OBJC_DISABLE_NONPOINTER_ISA并设为YES。打勾后运行,可以看到这里是只占用中间一些,最后一位不为1。而且直接po isa 可以得到类的信息。
OBJC_PRINT_LOAD_METHODS就是会把调用load方法的地方打印出来,添加OBJC_PRINT_LOAD_METHODS环境变量并且设为YES。
运行一下。
环境变量也可以通过中在终端中输入export OBJC_HELP=1来查看。在终端中输入export OBJC_HELP=1 后在输入ls,可以看到环境变量在终端中被打印出来了。
environ_init看完,下一个就是tls_init, tls_initz做的就是设置objc的预定义的线程特定键和键的析构函数,来存储objc的私有数据。
再往下就是static_init,static_init的作用就是调用全局静态c++函数(objc库里面的)。因为这里已经开启了runtime的下层,这里自己调用c++函数是为了方便所有的环境能够准备充分。
接下来就是runtime_init。这里进行了两个表的初始化。
在往下就是exception_init,这里做的是初始化libobjc的异常处理系统。
往下是cache_init,这里做的是缓存条件初始化。继续往下就是_imp_implementationWithBlock_init,这里启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib。
在往下就是重点: _dyld_objc_notify_register(&map_images, load_images, unmap_image)。
为什么这里的map_images加了&符号,而其他的没有呢?加了&之后,map_images就会和内部同步发生变化,相当于进行了指针传递。为什么单独map_images加&呢,因为map_images太重要了,map_images是一个比较耗时的过程,如果不同步发生变化,那么就会发生错乱,所以需要一起变化。比如map_images原先的地址是0x100001,但是可能在某个地方发生变化成了0x100000,那么在dyld里面调用map_images的地方也就是registerObjCNotifiers会调用0x100000而不是0x100001了。
看一下map_images的实现。
在点击map_images_nolock查看。这里重要的是寻找image的读取和映射,这里找到了_read_images这个方法。
点击_read_images,然后看到代码行数太多。接下来就从整体来看这个方法,把代码块收起来。可以看到这里有log记录每个方法做了些什么。
read_images流程:
- 条件控制进行一次的加载
- 修复预编译阶段的
@selector
的混乱问题 - 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的 类
- 修复一些消息!
- 当我们类里面有协议的时候 : readProtocol
- 修复没有被加载的协议
- 分类处理
- 类的加载处理
- 没有被处理的类 优化那些被侵犯的类
首先看条件控制进行一次的加载,这里是对一些环境变量的处理。
这里报出一些异常。
接下来是taggedPointers的一些处理。
在往下就是第一次的重点,看到这里创建了一个表且大小 * 4 / 3, 这是为什么呢?假设现在的总容积为9, 那么要开辟的内存就是9 * 4 / 3 = 12。当存入的时候,就不能超过 12的 3/4 也就是9,所以当开辟内存的时候总容积要乘以 4 / 3。这里还有一个表gdb_objc_realized_classes,这个表是干什么的呢?
点击这个表进去,看到注释写着这是一个储存没有在dyld shared cache 里面的class 的表。
再来看修复预编译阶段的 @selector
的混乱问题,sel是 名字+地址,那么就有可能出现名字相同但是地址不同的情况,因为sel在每个镜像文件中的地址不同。打个断点运行一下,
输出sels[i] 和sel,发现其方法名字是一样的,那么为什么会进来呢?肯定是他们的地址不同。
输出地址证明一下,发现两者地址确实不一样。
这里的sels是从mach-o读取出来的,而sel是从dyld读取出来的,dyld 是链接整个程序的,所以以dyld为基准。
接下来看discover classes,这里会在mach-o读取class,然后也在readClass里面调用popFutureNamedClass判断newCls是不是future class,是的话就重新赋值,然后进入下面的if里面进行处理,不是的话就不处理。什么是future class呢?在类的加载中,有时候一些类读取完后就会被删除,但是没有删除干净,就会出现一些混乱,future class 就是没有被删除干净的类。
readClass 里面还对class进行了一些处理,从machO里面读取出来的是类的地址,而当运行了readClass之后,类的名字就被赋值了。
那么readClass 是怎么处理的呢?点击进去看一下。
这里研究自己创建的类,所以设置一个条件打下断点后运行进来,就是要研究的LGPerson类了。
接下来往下运行看看会进去哪里,发现这三个地方是不进去的。
往下走发现调用了addNamedClass,为类添加了名字
继续往下走,发现会调用addClassTableEntry把类以及他的元类添加到allocatedClasses表中
在往下走,就直接返回了。