07--应用加载02--应用加载流程[_objc_init][read_images]

TOC

_objc_init:初始化流程

  • _objc_init 源码
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

我们先不要着急分析流程,看到最后一行代码:_dyld_objc_notify_register。这个很明显是 _dyld 里面的一个方法,上一篇文章中分类了 _dyld 的加载流程。

App加载分析

所以这里从 _dyld 中将这个方法的源码拿了过来,下面是 _dyld_objc_notify_register 的源码

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    sNotifyObjCMapped   = mapped;
    sNotifyObjCInit     = init;
    sNotifyObjCUnmapped = unmapped;
    ……
}

源码很简单,仅仅是记录了这三个方法的地址,在需要的时候进行一个调用。在我们平时开发中,需要用到地址传递的时候,一般是需要修改这个变量的值,那么可以猜测这个方法的作用是将 _dyld 中的 mappedImage 回传给 objc,这里的对内容放在后面分析,下面对这些初始化方法进行一个简单的介绍。

environ_init();

读取影响运行时的环境变量。如果需要,可以打印环境变量帮助。

1. 在终端中输入 export OBJC_HELP=1 可以输出所有环境变量

OBJC_HELP

如果没有输出,尝试再执行一个 open 命令

2. 在Xcode 源码中输出环境变量

在这里手动修改:PrintHelp=true;

image

3. OBJC_DISABLE_NONPOINTER_ISA 环境变量

  • 设置 OBJC_DISABLE_NONPOINTER_ISAYES

    image

    设置之后的打印出来的isa结果

    image

    设置之前的isa结果

    image

    因为系统做了优化,对isa联合体做了平移操作,所以会不一样
    OBJC_PRINT_LOAD_METHODS

4. 环境变量 OBJC_PRINT_LOAD_METHODS

  • 设置 OBJC_PRINT_LOAD_METHODS 为 YES

    image
  • 打印所有的 +load 方法

    image

作用:可以对这些方法做方法优化,避免全局搜索找到的load方法(可能没有用到)

tls_init();

关于线程的绑定,比如每个线程数的析构函数

static_init();

  • 运行C++ 的静态构造函数。
  • 在 _dyld 调用我们自己的静态构造函数之前调用
  • libc 会调用 _objc_init(),所以我们必须自己做。

runtime_init();

是空实现!就是说 objc 的锁是完成采用C++那一套的, oc中不需要做任何处理。

exception_init();

初始化 libobjc 的异常处理系统。比如监控下一行注册异常的回调的代码。

cache_init();

_imp_implementationWithBlock_init()

_dyld_objc_notify_register()

仅供 objc 运行时使用
注册处理程序,以便在映射、取消映射和初始化 objc-image 时调用
Dyld将使用包含 objc-image-info 的景象文件的数组,回调 mapped 函数

  • 研究对象
    • map_images(重点)
    • load_images(重点)
    • unmap_image:主要做卸载相关的操作,不做重点研究

_objc_init:map_images 流程

read_images 注释

/***********************************************************************
* map_images
* Process the given images which are being mapped in by dyld.
* Calls ABI-agnostic code after taking ABI-specific locks.
*
* Locking: write-locks runtimeLock
**********************************************************************/
void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

先进入 map_images,这个函数的注释说“处理给定的镜像文件(在dyld中映射进去的镜像)”,这句注释正好验证了我们上面的猜测——registerObjCNotifiers 方法的作用是将 _dyld 中的 mappedImage 回传给 objc

下面来分析 map_images_nolock 方法的流程

read_images 要读懂的问题

  1. 我们的dyld主题的思维是加载库-镜像文件,但是镜像文件怎么读取的?
  2. 我们的macho里面的数据怎么到我们的内存?
  3. 有没有不在macho里面的数据同样也可以在内存找到?
  4. sel方法编号的加载?
  5. 什么是懒加载类和非懒加载类?
  6. 类是如何加载实现的 -ro-rw 的关系?
  7. 协议&分类里面的数据是如何加载的?

read_images 初体验

在dyld的源码中去查找_dyld_objc_notify_register方法实现

image

1. 如何开展dyld研究

  • 看注释 “// record functions to call”——记录被调用的函数
  • 可以看出来,如果要研究这三个参数,和明显要研究这三个函数被调用的地方
  • 以第一个参数为例,找到第一个函数被调用的地方
image

2. 在objc源码中分析,定位到_read_images方法

  • _objc_init->map_images

    image
  • map_images->map_images_nolock

    image
  • map_images_nolock->_read_images

    image

3. 怎么分析 _read_images方法

这个方法有400多行,逐行读肯定不行

  1. 我们先将所有有注释的代码块折叠起来

  2. 方法的准备条件

    image
  3. 方法的流程

    image

    我们可以看到每个代码块都有很标准的注释和日志输出,整个流程一目了然:

    1. 加载所有类到类的 gdb_objc_realized_classes 表中
    2. 对所有类做重映射
    3. 将所有SEL都注册到 namedSelecotors 表中
    4. 修复函数指针遗留
    5. 将所有 Protocol 都添加到 protocol_map 表中
    6. 对所有 Protocol 做重映射
    7. 初始化所有非懒加载的类,进行 rw、ro等操作
    8. 遍历已标记的懒加载的类,并做初始化操作
    9. 处理所有 Category,包括 ClassMeta Class
    10. 初始化所有未初始的类。

4. 表的介绍

  • gdb_objc_realized_classes:无论是否实现,只要不在 dyld 共享缓存中的已命名类的表
  • allocatedClasses:通过 objc_allocateClassPair 已分配的所有类(和元类)的表
  • namedSelecotors:方法编号的表
  • protocol_map:协议的表

题外话

  1. 为什么类的开头是 NS
    NextStep的简称
    乔帮主曾经被苹果踢出去了,然后又回来了
  2. NX开头的又代表什么含义
    NX表示一种CPU的计数,“禁止执行的意思”

read_images 流程分析

第一流程:查找类。修复未解决的future类。标记 bundle类。

流程代码
  1. 从编译后的类列表中取出所有类, 获取到的是一个 classref_t 类型指针
    classref_t *classlist = _getObjc2ClassList(hi, &count);

  2. 遍历数组中会去除 OS_dispatch_queue_concurrent OS_xpc_object NSRunloop 等系统类, 例如 CFFoundation libdispatch 中的类, 以及自己创建的类.

    通过readClass函数获取处理后的新类, 内部主要操作 ro 和 rw 结构体

  3. 初始化所有懒加载的类需要的内存空间,现在数据没有加载

    image

第二流程:修复类的重映射(一般不会走进来)

image

注释

  1. 类列表和非懒加载类类表保持未重映射
  2. 重映射类和super类,用于消息分发
  3. 将未映射class和super class重映射,被remap的类都是非懒加载类

第三流程:修复SEL引用

image

将所有 SEL 都注册到 namedSelecotors 表中

第四流程:修复旧的objc_msgSend_fixup调用站点,有条件才进来,不做分析

image

第五流程:查找协议protocols。修复协议protocols引用

image

_getObjc2ProtocolList 读协议存到 protocol_map

第六流程:修复协议的引用

image

预先优化的图像可能已经在正确的位置了,但并不能确定。所以需要重新映射协议的引用

第七流程:实现非懒加载类(重点流程)

  • 实现了load方法的类
  • 静态实例变量的类(常见的是单例)
image

第八流程:实现未来类。(条件依赖第一个流程)

image

第九流程:查找分类。(默认找的是懒加载分类-实现了load)

  • 注册分类并关联目标类
  • 重新构建类的方法列表(attachLists流程)

因为分类的方法也是放到了类的方法列表里面,而且是通过 attachLists 流程,插入了方法列表的前面,所以会造成分类方法“覆盖”主类方法的现象。

image

第十流程:实现所有未实现的类

image

你可能感兴趣的:(07--应用加载02--应用加载流程[_objc_init][read_images])