OC runtime 中的 load 和 initialize

我们的程序编译成二进制后,在启动时需要初始化运行环境,包括类环境的初始化,涉及到类相关方法的加载,其中很重要的两个方法有 +load+initialize,这两个方法都会在初始化的时候被调用,相信大家都不陌生,但是涉及到底层细节时可能就不太熟悉了,比如它们是如何被调用的?为什么会是这样的调用?它们在写业务代码时能用来做什么?本文将通过 runtimedyld 源码来回答这些问题

官方文档

遇事不决,官方文档!网上的博客以讹传讹的信息太多,最可靠的资料来源还是得看官方文档。
根据文档上的描述,+load 是在 class 或 Category 被添加到 runtime 时调用的,而 +initialize 是在类第一次收到消息时被调用。继续看详情,我们能得到以下印象:

  • +load
    • 静态库或动态库中的 load 方法都会被调用,前提是它们实现了 load 方法
    • 在继承链上依次按照本类、子类、孙子类的顺序调用
    • 本类上的 load 方法会先于所有分类上的 load 方法调用
  • +initialize
    • +initialize 方法会在调用第一次该类的方法之前被调用
    • +initialize 的调用阻塞式的,在 +initialize 方法执行完毕之前,该类的其他任何方法调用都会被阻断(block)
    • 在继承链上依次按照子类、孙子类的顺序调用
    • 当继承链上某个类没有 +initialize 的实现,那么其父类的 +initialize 可能会被多次执行
    • +initialize 在每个类上只会被调用一次

调用时机

基于 objc4-818.2 可调试源码[1]创建一个四世同堂工程,添加符号断点 +[KFCRootObject initialize] [2],选中Scheme: KCObjcBuild,KCObjcBuild target 的 main.m 文件中的 main 函数代码为:

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
NSObject *objc = [KFCRootObject alloc];

NSObject *objc1 = [KFCRootObject alloc];
}
return 0;
}

运行后[3]方法调用栈:

下面按照入栈顺序分析

initialize

  1. main


    0x0000000100008468 是指向类对象的指针,调用 alloc 时,首先被转换成对 objc_alloc 的调用

  2. objc_alloc -> callAlloc(objc_class*, bool, bool) [inlined]

    objc_alloccallAlloc(objc_class*, bool, bool) [inlined] 中,对形参、调用环境做了一番校验后,最终调用了 ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));,接着进入 objc_msgSend 的汇编实现

  3. _objc_msgSend_uncached -> lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked(objc_object*, objc_class*, bool)
    • @selector(alloc) 对应的 IMP 在方法缓存中不存在时,会调用 MethodTableLookup 继续查找,这部分的汇编代码又将流程导向 lookUpImpOrForward,后者预期返回该 IMP。
    • lookUpImpOrForward 中调用 realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE) 时,behavior 形参值为 0x1011,包含标记位 LOOKUP_INITIALIZE,即形参 initialize 为 true
    • (slowpath(initialize && !cls->isInitialized())) 判断当前类是否被初始化过,slowpath(x) 宏定义标记暗示编译器的优化方向,表明 x 较大概率为 false,重点看看!cls->isInitialized()
    • 从字面意思可以看出当前类是否被初始化的信息保存在元类的 class_rw_t 结构的 flags 标记位中,由于 #define RW_INITIALIZED (1<<29),所以是否被初始化的信息保存在 flags 的右起第 29 位中
  4. initializeAndLeaveLocked(objc_class*, objc_object*, mutex_tt<true>&) -> initializeAndMaybeRelock(objc_class*, objc_object*, mutex_tt<true>&, bool) -> initializeNonMetaClass

    initializeNonMetaClass 中:

    • 如果发现父类没有调用过初始化方法,将递归调用父类的初始化方法:

      1
      2
      3
      4
      supercls = cls->getSuperclass();
      if (supercls && !supercls->isInitialized()) {
      initializeNonMetaClass(supercls);
      }
    • 通过加锁来设置 CLS_INITIALIZING

      1
      2
      3
      4
      5
      6
      7
      8
      monitor_locker_t lock(classInitLock);
      if (!cls->isInitialized() && !cls->isInitializing()) {
      cls->setInitializing();
      reallyInitialize = YES;

      // Grab a copy of the will-initialize funcs with the lock held.
      localWillInitializeFuncs.initFrom(willInitializeFuncs);
      }
      1
      2
      3
      4
      void setInitializing() {
      ASSERT(!isMetaClass());
      ISA()->setInfo(RW_INITIALIZING);
      }
    • 接下来标记当前类被当前所在线程独占(当被某一线程独占时,只能在当前线程向类发送消息,其他线程在独占结束之前只能等待),然后向当前类发送 +initialize 消息

      1
      _setThisThreadIsInitializingClass(cls);
      1
      2
      3
      4
      5
      6
      7
      8
      void callInitialize(Class cls)
      {
      // 通过 objc_msgSend 走消息转发流程。
      // 意味着子类没有对应的方法实现时
      // 会沿着继承链去尝试调用父类上的 initialize 方法
      ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
      asm("");
      }
    • 完成初始化:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      static void lockAndFinishInitializing(Class cls, Class supercls)
      {
      monitor_locker_t lock(classInitLock);
      if (!supercls || supercls->isInitialized()) {
      _finishInitializing(cls, supercls);
      } else {
      // 如果父类未被初始化,会在父类初始化完成后再修改当前 cls 初始化状态标记位
      _finishInitializingAfter(cls, supercls);
      }
      }
    • 数据结构 PendingInitializeMap:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      typedef struct PendingInitialize {
      Class subclass;
      struct PendingInitialize *next;

      PendingInitialize(Class cls) : subclass(cls), next(nullptr) { }
      } PendingInitialize;

      typedef objc::DenseMap<Class, PendingInitialize *> PendingInitializeMap;
      static PendingInitializeMap *pendingInitializeMap;

      pendingInitializeMap 是一个全局的字典结构,它负责维护 +initialize 调用的依赖,会在两个地方访问:

      1. 父类未完成初始化时。父类的初始化会优先于当前类,这一设定是通过 _finishInitializingAfter 中如下的关键代码保证的:

        1
        2
        3
        4
        5
        6
        7
        PendingInitialize *pending = new PendingInitialize{cls};
        auto result = pendingInitializeMap->try_emplace(supercls, \
        pending);
        if (!result.second) {
        pending->next = result.first->second;
        result.first->second = pending;
        }
      2. 父类已经初始化完成时,调用 _finishInitializing

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        static void _finishInitializing(Class cls, Class supercls)
        {
        PendingInitialize *pending;

        classInitLock.assertLocked();
        ASSERT(!supercls || supercls->isInitialized());

        if (PrintInitializing) {
        _objc_inform("INITIALIZE: thread %p: %s is \
        fully +initialized",
        objc_thread_self(), cls->nameForLogging());
        }

        // mark this class as fully +initialized
        cls->setInitialized();
        classInitLock.notifyAll();
        _setThisThreadIsNotInitializingClass(cls);

        if (!pendingInitializeMap) return;

        auto it = pendingInitializeMap->find(cls);
        if (it == pendingInitializeMap->end()) return;

        pending = it->second;
        pendingInitializeMap->erase(it);

        if (pendingInitializeMap->size() == 0) {
        delete pendingInitializeMap;
        pendingInitializeMap = nil;
        }

        while (pending) {
        PendingInitialize *next = pending->next;
        if (pending->subclass)
        _finishInitializing(pending->subclass, cls);
        delete pending;
        pending = next;
        }
        }

        _finishInitializing 函数中,设置 RW_INITIALIZED 标记并清除之前设置的 RW_INITIALIZING,设置当前类不再被当前线程独占,然后递归地将先前被阻塞的子类设置为初始化完成状态,由于初始化工作已完成,这里还清理了不再需要的内存占用

    • 在类初始化未完成之前(RW_INITIALIZING),后续在该线程上其他的 +initialize 调用都会被直接 return;在类已经完成初始化时(RW_INITIALIZED)直接 return,官方的注释也很详细:

      查看官方注释
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      if (...) {...}
      else if (cls->isInitializing()) {
      // We couldn't set INITIALIZING because INITIALIZING was already set.
      // If this thread set it earlier, continue normally.
      // If some other thread set it, block until initialize is done.
      // It's ok if INITIALIZING changes to INITIALIZED while we're here,
      // because we safely check for INITIALIZED inside the lock
      // before blocking.
      if (_thisThreadIsInitializingClass(cls)) {
      return;
      } else if (!MultithreadedForkChild) {
      waitForInitializeToComplete(cls);
      return;
      } else {
      // We're on the child side of fork(), facing a class that
      // was initializing by some other thread when fork() was called.
      _setThisThreadIsInitializingClass(cls);
      performForkChildInitialize(cls, supercls);
      }
      }

      else if (cls->isInitialized()) {
      // Set CLS_INITIALIZING failed because someone else already
      // initialized the class. Continue normally.
      // NOTE this check must come AFTER the ISINITIALIZING case.
      // Otherwise: Another thread is initializing this class. ISINITIALIZED
      // is false. Skip this clause. Then the other thread finishes
      // initialization and sets INITIALIZING=no and INITIALIZED=yes.
      // Skip the ISINITIALIZING clause. Die horribly.
      return;
      }
  5. 到这里已经梳理完了类的 +initialize 调用流程,验证了苹果 API 文档中关于 +initialize 的特性

load

load 方法相信都不陌生,用得最多的场景就是方法交换,而且大家也都知道 load 方法会先于 main 函数调用。接下来将对照源码来理解 load 方法具体的调用过程。

还是刚刚的四世同堂工程,添加符号断点 +[KFCRootObject load],运行后的调用栈为:

还是按照入栈顺序分析:

  1. _dyld_start->dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) -> load_images,前两个栈记录是操作系统通过 dyld 加载程序时,dyld 的内部函数调用过程,dyld 负责给程序创建一个和操作系统绑定的运行环境,包括链接程序所用到的动态库(包括系统动态库)、绑定外部调用符号、rebase 基址,做完了环境准备工作后,通过 load_images 回调 runtime。我们增加一个符号断点:load_images,重新 run 起来:

    定位到 _objc_init 的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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();
    #if __OBJC2__
    cache_t::init();
    #endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

    #if __OBJC2__
    didCallDyldNotifyRegister = true;
    #endif
    }

    链接库被初始化之前 libSystem 调用 _objc_init 进行初始化,在 _objc_init 中又通过 _dyld_objc_notify_register 注册了 dyld 的回调,在 dyld 源码[4] 中可以查看到 _dyld_objc_notify_register 的原型:

    1
    2
    3
    void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
    _dyld_objc_notify_init init,
    _dyld_objc_notify_unmapped unmapped);

    通过注释可知,当某个镜像将被 dyld 初始化时,dyld 会通过 init 这个函数指针形参将该镜像信息回调给 objc runtime。来看看 load_images:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void load_images(const char *path __unused, const struct mach_header *mh)
    {
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
    didInitialAttachCategories = true;
    loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
    mutex_locker_t lock2(runtimeLock);
    prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
    }
    1. 遍历所有链接进来的 Image 头信息链表,找到所有的 Category 方法,并附加到对应的类的方法列表上
    2. 通过查询 Image 的 Mach-O 结构,在 __DATA,__objc_nlclslist__DATA,__objc_nlcatlist 中分别检查是否存在包含 +load 方法的类和包含 +load 方法的分类, 如果没有,跳过 load_images 接下来的步骤
    3. 准备 load 方法。先通过 _getObjc2NonlazyClassList 获取到所有包含 +load 的类(话说这里分别进行了两次重复的 Mach-O 结构的查找,也许可以合并为一次🤔),然后将这些类添加到 loadable_classes 数组中。如果某个类有父类,父类的 load 方法(如果有)将会先添加到 loadable_classes 里面,这是通过 schedule_class_load 的递归调用保证的。接下来对包含 +load 的分类进行类似的操作,将结果保存在 loadable_categories 数组中。
    4. 调用 call_load_methods,执行 +load 方法。call_load_methods 可能会触发其他镜像的映射(mapping),其他的镜像映射时可能会有它自己的 +load 调用过程,所以 call_load_methods 可能会发生 Re-entrant。当 Re-entrant 发生时啥也不用做,因为按照我们刚刚的分析,其他镜像加载时执行到 call_load_methods 时,所包含 +load 方法的类和分类已经被添加到了全局的 loadable_classesloadable_categories 中。
    5. 先开启一个 autoreleasepool,接下来会先在一个循环中不断调用先前找到的类的 +load 方法,且保证在一个镜像中,本类的 +load 总是比分类的 +load 先调用
    6. call_class_loads 方法,涉及到一个比较有意思的任务控制。先前我们知道 loadable_classes 数组保存的是 struct loadable_class 结构体,它指向通过 realloc 申请到的内存,在 call_class_loads 中首先用一个临时指针指向该内存区间,然后重置 loadable_classes 相关的全局变量,后续如果其他的 Image 被加载导致 add_class_to_loadable_list 被调用时 loadable_classes 数组会指向重新申请的内存空间,+load 方法会被继续添加到这个数组里面,视 call_class_loads 消耗的速度,loadable_classes 可能是重新申请内存(loadable_classes == NULL 时),也可能是在原有内存区域扩大空间,这些堆空间最终都会在 call_class_loads 中被 free。回到 call_class_loads 函数,它顺序遍历上述临时指针指向的数组,取出 load_method_t 进行 +load 调用(注意是通过函数地址直接调用,没有走 objc_msgSend 流程),由于该数组中父类的 +load 在前面,所以父类的 +load 方法会被先调用
    7. 对分类的 +load 方法收集和本类的差不多,但是当分类的 Image 在本类的 Image 之前被加载运行时,存在额外的处理流程,所以分类的 +load 调用逻辑会有所不同。先遍历loadable_categories,如果类被首次加载过(Realized)就调用其 +load 方法,然后将该分类的从数组中移除,同时会将 Re-entrant 过程新增加的分类整理到一起,最后如果 loadable_categories_used 不为 0,返回 true,以便在 call_load_methods 中能够通过循环继续处理本次未处理完毕的分类 +load 方法

总结

我们通过源码分析 +load+initialize 的调用时机以及它们各自的调用特点,总结如下:

  • +load

    1. 类的 +load 方法一定会被调用,而且是在 +main 函数之前被调用
    2. 父类的 +load 方法一定会先于子类的 +load 方法调用,而且在子类的 +load 中不需要添加 [super load];
    3. 类的 +load 方法会先于分类的 +load 方法调用
    4. 由于动态链接库先于主程序二进制加载,所以动态链接库里面的 +load 方法会先于主程序的 +load 方法调用
  • +initialize
    1. +initialize 会在类首次收到消息之前调用
    2. 父类的 +initialize 会优先于子类的 +initialize 调用
    3. runtime 会自动处理对继承链上的 +initialize 调用,所以重写时无需调用 [super initialize];
    4. 相对于 +load+initialize 是普通方法,可以被交换;在多个分类中被实现时只会调用 Complie Sources 列表中最靠后的分类中的那个
  1. https://github.com/LGCooci/objc4_debug/tree/master/objc4-818.2
  2. 尝试过在 KFCRootObject.m+ (void)initialize 处添加断点,但运行时没有进来🤔,知道原因的大佬烦请留言赐教
  3. x86_64 架构运行
  4. 找到对应版本的 dyld 源码:输入 lldb 命令:image list dyld,得到dyld所在路径为:/usr/lib/dyld,使用 MachOView 打开,在 LoadCommand 的 LC_SOURCE_VERSION 中找到源码 Version ,我这里是 832.7.1