dyld之类的加载

476 阅读4分钟

前言

上一篇博客我们了解到dyld->libsystem->libdispatch->_objc_init这样一个流程。那么下面我们来了解一下objc_init里面究竟做了些啥。

objc_init分析

  • environ_init环境变量初始化 在源码里面通过如下代码来查看环境变量:
    for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
     const option_t *opt = &Settings[i];
     _objc_inform("%s: %s", opt->env, opt->help);
     _objc_inform("%s is set", opt->env);
}

还可以通过在终端里面输入export OBJC_HELP=1,然后再输入lldb:打印信息如下

环境变量.png 我们可以在xcode里面设置一下OBJC_PRINT_IMAGES设置一下这个环境变量,就会打印出。通过Edit Scheme ->Run就可以设置。

  • tls_init:关于线程key的绑定,比如每个现场的析构函数
  • static_init:运⾏C ++静态构造函数。在dyld调⽤之前自己先调用
  • runtime_init:主要是初始化unattachedCategoriesallocatedClasses两个表。
  • exception_init:异常初始化,我们常用的objc_setUncaughtExceptionHandler处理崩溃后异常,或奔溃日志上传服务器。
  • cache_t::init()缓存条件初始化
  • _imp_implementationWithBlock_init:启动回调机制。通常这不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,我们会迫不及待地加载trampolines dylib.
  • _dyld_objc_notify_register,将map_images load_images unmap_image方法赋值给dyld去触发调用

map_images分析

  • map_images_nolock

这个方法主要是找到macho所有的image,然后调用_read_images

_read_images分析

这个里面,代码有很多,当时苹果工程师做了很多的日志打印。我们可以通过这个来分成小模块去分析。

  • 条件控制进行第一次加载 创建NXMapTable表,大小为4/3 *totalClasses。这个表是一张class总表;与上面的allocatedClasses表是有区别的,后面的是存储已经alloc的。
  • 修复预编译阶段的 @selector 的混乱问题. 为什么会需要修复呢?这是因为,我们从image里面获取的方法的地址是没有加上ASLR也就是基地址,需要通过dyld加上基地址后重新修复。
  • 读取类readClass、处理没有被清理的类
  • 修复重映射一些没有被镜像文件加载进来的类remapClassRef
  • 修复一些消息fixupMessageRef
  • 读取类的协议readProtocol
  • 修复没有被加载的协议remapProtocolRef
  • 加载分类load_categories_nolock
  • 加载类realizeClassWithoutSwift
  • 实现新解析的未来类

下面我们主要分析一下readClass

readClass分析

readClass方法中添加如下代码,这样就可以研究我们自定义的类是如何读取的。

testClass.png 断点继续执行:

截屏2021-07-15 下午10.43.28.png 发现并没有执行ro,rw相关的赋值。 断点继续下一步,会调用addNamedClass

总表.png 这时会把class插入到总表中

  • 继续执行会递归调用addClassTableEntry
addClassTableEntry(Class cls, bool addMeta = true)
{
    runtimeLock.assertLocked();

    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic table already.
    auto &set = objc::allocatedClasses.get();

    ASSERT(set.find(cls) == set.end());

    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        addClassTableEntry(cls->ISA(), false);
}

class插入到allocatedClasses表中,如果有元类,把元类也插入进去。

类的实现realizeClassWithoutSwift

  • 新建一个QHPerson类,然后在里面添加load方法,在代码里面添加如下代码,这样就可以研究我们自定义的类

自定义的类.png 继续执行进入到realizeClassWithoutSwift方法。

  • ro 赋值给rw

截屏2021-07-17 上午8.58.30.png

  • 设置父类和元类关系

截屏2021-07-17 上午9.02.40.png

  • 条理化class

截屏2021-07-17 上午9.04.29.png

methodizeClass分析

下面我们继续进入methodizeClass方法,添加如下代码,这里多了一个元类的判断,因为元类和本身类的名字一样:

截屏2021-07-17 上午9.47.04.png 打印rwe:发现为nil

截屏2021-07-17 上午9.48.55.png

截屏2021-07-17 上午9.49.53.png

  • 证明rwe->properties.attachLists(&proplist, 1);rwe->protocols.attachLists(&protolist, 1);
  • 如果有方法就会调用prepareMethodLists,这个方法实际上是通过地址对方法进行排序,下面的代码可以验证

截屏2021-07-17 上午10.00.33.png

  • 将分类添加到主类attachToClass,这个方法下篇博客再分析
  • 所以methodizeClass主要是方法排序和将分类添加到主类中。

懒加载和非懒加载

上面我们在自定义的类中添加了load方法,现在我们去掉.然后再运行。发现一个奇怪的现象:

截屏2021-07-17 上午10.10.17.png 这是为什么???下面我们在main里面调用QHPerson的方法。继续运行,这个时候进入了,只不过不是走的之前的流程:

截屏2021-07-17 上午10.19.13.png 通过堆栈,可以看出是在main函数之后走的消息发送的流程。

总结

  • 如果自定义的类实现了load方法,(非懒加载),会通过dyld->objc_init->(实例化主程序)->read_image->read_class去加载类。
  • 如果没有实现load方法,(懒加载),会在第一次调用对象方法的时候去,走消息转发流程去实现
  • 懒加载这样的处理方式会大大优化app的启动时间