上篇文章讲到 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.pngimage.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.pngimage.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