类的加载

892 阅读6分钟

上篇文章讲到 dyld 加载动态库时,会调用 notifyObjCInit 函数,去通知 objc 调用 +load 方法,通过 _dyld_objc_notify_registerlibobjc 中的 _objc_init 进行通信,其中有三个个参数:map_images 、load_imagesunmap_image, 其中 unmap_image 是在镜像消失时才调用,本篇文章就来探索一下 map_images 和load_images

1. _objc_init

去 OC 源码里面查看一下该函数:

image.png

  • environ_init(): 环境变量初始化,可以在 Edit Scheme -> Arguments 添加一些环境变量
  • tls_init(): 创建线程的析构函数,处理线程 key 的绑定
  • static_init(): 运行 c++ 静态构造函数
  • runtime_init(): 初始化两张表:分类的表和类的表
  • exception_init(): 异常处理的初始化
  • didCallDyldNotifyRegister: 标识对 _dyld_objc_register 的调用已完成

2. map_images

现在来探索一下 map_images:

image.png 由于 map_images_nolock 函数代码比较多,我们分段看下:

1. preopt_init 初始化环境

image.png

这一段最主要的函数就是当一次调用该函数时,会调用 preopt_init 来准备初始化环境

image.png image.png 可以发现 preopt_init 作用主要就是初始化一些共享缓存,包括选择器的缓存、头的缓存、类的缓存、协议的缓存等

2. 获取并添加 imageheader 指针

image.png

image.png

image.png

image.png

image.png 大致步骤如下:

  • 首先获取 imageheader 指针,将 mach_header 的指针转换成 headerType 类型的指针
  • 通过 addHeader 获取 header_info, 并将 header_info 插入到链表里
  • addHeader 方法先从共享缓存中拿,如果有,就插入链表并返回
  • addHeader 方法如果没有从共享缓存中拿到,就会封装一个 header_info,然后再插入链表并返回。

3. sel_initarr_init

可以看到在第一次执行 map_images,会调用 sel_initarr_init:

image.png

sel_init 主要是初始化 selector 表,该表定义如下:

image.png

image.png

namedSelectors 是个全局变量,存储所有的方法名 SEL,内部结构是 hashDenseMap

image.png

可以看出 arr_init 主要做了以下几件事:

  • 初始化自动释放池 AutoreleasePool
  • SideTablesMap 初始化
  • AssociationsManager 的初始化,即为全局使用的关联对象表开辟空间,关于关联对象,可以看下篇文章

4. _read_images

发现里面有很多代码,这也是 map_images 的核心所在

1. 初始化类表

image.png image.png

通过注释有以下结论:

  • doneOnce 保证了 _read_images 只执行一次
  • gcd_objc_realized_classes 是一个全局的类表,只要 class 没有在共享缓存中,那么不管其有没有实现都会存在这个类表里,其本质是个 hash

2. rebase

image.png

_getObjc2SelectorRefs 就是拿到 Mach-O 的静态段 __obj_selrefs, 后面所有通过 _getObjc2 开头的 Mach-O 静态段获取,都对应不同的 section name, 如下:

image.png

这段代码主要的作用就是将所有的 SEL 注册到 namedSelectors 表中,且当 _getObjc2SelectorRefs 中得到的 SELsel_registerNameNoLock 中的 SEL 不同时,就会把前者的 SEL 修正修复成后者,这一步就是 rebase, 修复镜像内部的资源指针。验证一下:

image.png

造成这两个函数地址不同的原因是 ALSR 偏移

3. 读取类

image.png

这一步的主要作用就是发现并读取类,readClass 是关键函数,在未调用该方法前,cls 只是一个地址,执行该方法后,cls 存入表中,是一个类的名称

image.png

可以看到有很多条件判断,那么我们自己加段代码,进行单步调试:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    
    const char *userClassName = "User";
    auto user_ro = (const class_ro_t *)cls->data();
    // 判断是否是元类
    auto user_isMeta = user_ro->flags & RO_META;
    if (strcmp(mangledName, userClassName) == 0 && !user_isMeta) { 
        // do something
    }
    
    ...
    
}

打上断点,进行调试,发现 readClass 主要做了以下几件事:

  • addNamedClass, 该函数把 namecls 添加到命名为 gdb_objc_realized_classes 的表中去,在 addNamedClass 方法中,调用 NXMapInsertclsname 插入到 NXMapTable 中,NXMapTablehash
  • addClassTableEntry 将类和元类加入到 allocatedClasses 表中

4. 修复类、消息、协议

image.png

这里的主要作用就是将未映射的 classsuperclass 进行重映射

  • _getObjc2ClassRefs 是获取 Mach-O 中的静态段 __objc_classrefs, 即类的引用
  • _getObjc2SuperRefs 是获取 Mach-O 中的静态段 __objc_superrefs, 即父类的引用
  • fixupMessageRef 修复一些消息的调用
  • 当类里面有协议时,通过 _getObjc2ProtocolList 获取到 Mach-O 中的静态段 __objc_protolist 协议列表,即从编译器读取并初始化 protocol
  • 修复没有被加载到的协议,通过 _getObjc2ProtocolRefs 获取 Mach-O 的静态段 __objc_protorefs, 然后遍历需要修复的协议,通过 remapProtocolRef 比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换

5. 分类的处理

下一篇文章处理

6. 非懒加载类的加载

非懒加载类和懒加载类的区别就是是否实现了 +load 方法

  • 实现了 +load 方法,就是非懒加载类
  • 没实现就是懒加载类,因为 load 会被提前加载,load 方法会在 load_images 中调用

image.png

  • hi->nlclslist(&count) 中通过 _getObjc2NonlazyClassList 获取 Mach-O 的静态段 __objc_nlclslist 非懒加载类表
  • addClassTableEntry 将非懒加载类插入类表,存储到内存,如果已经添加就不会再添加,需要确保整个结构都被添加
  • realizeClassWithoutSwift 实现当前类,因为之前 readClass 读取到内存的只有地址和名称,类的 data 数据并没有加载出来。realizeClassWithoutSwift 下一篇文章处理

7. 没有被处理的类

image.png

8. read_images 总结

  • 加载所有的类到类的 gdb_objc_realized_classes 表中
  • 对所有类做重映射
  • 将所有 SEL 都注册到 namedSeletors 表中
  • 修复函数指针
  • 将所有 Protocol 都添加到 protocol_map 表中
  • 对所有 Protocol 做重映射
  • 初始化所有非懒加载类,对 rw、ro 等进行操作(初始化)
  • 遍历已标记的懒加载类,并做初始化操作
  • 处理所有 Category, 包括 ClassMeta Class
  • 初始化所有未初始化的类

3. load_images

image.png

通过源码来看 load_images 主要做了以下几件事:

  • loadAllCategories 加载所有分类,分类的加载下一篇文章处理
  • prepare_load_methods 找到 load 方法
  • call_load_methods 调用 load 方法

1. prepare_load_methods

image.png

image.png

image.png

  • 这里的 schedule_class_load 会进行一个递归调用,在找 load 方法时,会优先找父类的方法,然后再把找到的方法添加到 loadable_classes 表里面去
  • add_class_to_loadable_list 会去找分类的 load 方法,找到之后同样会被添加到 loadable_classes 表里面去
  • load 方法不是走消息派发机制的,而是通过地址调用

2. call_load_methods

image.png

3. load 方法总结

  • 子类的 load 方法默认实现了父类的父类的 load 方法,所以不需要写 super load
  • load 方法执行顺序:父类 > 本类 > 分类
  • load 方法内部使用了锁,所以时线程安全的
  • 有多个类别实现了 load 方法时,load 方法都会执行,执行顺序与编译顺序有关(在 Build Phases -> Compile Sources 里面查看编译顺序),后编译的先执行

拓展:环境变量的配置

在 _objc_init 方法中,可以看到会对环境变量进行初始化,以环境变量 OBJC_DISABLE_NONPOINTER_ISA 为例,该环境变量为是否开启指针优化,YES 表示纯指针,NO 就表示使用 nonpointerisa:

  • 未设置 OBJC_DISABLE_NONPOINTER_ISA 时,对象的 isa 指针地址末尾为 1,默认开启了指针优化,表示 isa 不仅包含了类对象地址,还包含了类信息、对象的引用计数等

image.png

  • 设置 OBJC_DISABLE_NONPOINTER_ISA 为 YES 后,isa 地址末尾变成了 0,此时的 isa 就表示类的首地址

image.png

image.png

其他的一些环境变量及说明:

环境变量说明.png