美文网首页
iOS类加载流程(一):类加载流程的触发

iOS类加载流程(一):类加载流程的触发

作者: 康小曹 | 来源:发表于2022-06-16 10:48 被阅读0次

    首先,大家应该都知道 _objc_init 函数是 OC 中类加载比较关键的一个函数,这个函数的调用栈如下:

    objc_init

    那么,objc_init 这个函数是如何被调用的呢?又和 OC 中的类加载有什么关系?类又是如何被加载并以什么形式存在于运行时呢?OC 中的成员变量、方法、协议、分类,这些都是如何实现的?

    1. objc_init 的调用流程

    从调用栈可以看到,_objc_init 起始于 doModinitFunctions 这个方法。这个方法在 dyld 中,因为 dyld3 都已经在 iOS12 被全面使用了,dyld-433 仍然是 dyld2 的版本,dyld-655 已经是 dyld3 的版本了,所以这里以 dyld-655 的源码来探索 _objc_init 的调用流程。

    首先,doModinitFunctions 这个函数属于 dyld 流程的“初始化方法调用”阶段。这一阶段是整个流程的倒数第二步,也就是执行 main 函数之前的阶段。

    dyld 详细流程见dyld:启动流程解析

    doModinitFunctions 函数在 ImageLoader::recursiveInitialization 中被调用,关键代码如下:

    recursiveInitialization

    先看看 doInitialization 方法的逻辑:

    bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
        CRSetCrashLogMessage2(this->getPath());
    
        // mach-o has -init and static initializers
        doImageInit(context);
        doModInitFunctions(context);
        
        CRSetCrashLogMessage2(NULL);
        
        return (fHasDashInit || fHasInitializers);
    }
    

    很明显,关键逻辑在于 doImageInitdoModInitFunctions 这两个函数。

    doImageInit 内部经过逻辑主要是找出该 Image 对应的 mach-O 文件中 LC_ROUTINES 表内的函数进行调用:

    doImageInit

    LC_ROUTINES 的定义可以直接在 mach-o 库的 loader.h 中看到,如果 dyld 源码中无法跳转,可以在自己的项目中 import <mach-o/loader.h> 来看到具体的内容:

    /*
     * The routines command contains the address of the dynamic shared library 
     * initialization routine and an index into the module table for the module
     * that defines the routine.  Before any modules are used from the library the
     * dynamic linker fully binds the module that defines the initialization routine
     * and then calls it.  This gets called before any module initialization
     * routines (used for C++ static constructors) in the library.
     */
    struct routines_command { /* for 32-bit architectures */
        uint32_t    cmd;        /* LC_ROUTINES */
        uint32_t    cmdsize;    /* total size of this command */
        uint32_t    init_address;   /* address of initialization routine */
        uint32_t    init_module;    /* index into the module table that */
                            /*  the init routine is defined in */
        uint32_t    reserved1;
        uint32_t    reserved2;
        uint32_t    reserved3;
        uint32_t    reserved4;
        uint32_t    reserved5;
        uint32_t    reserved6;
    };
    

    根据注释来看,LC_ROUTINES 大概就是动态库在调用初始化函数之前需要被调用的函数。找了几个动态库,也没有找到包含 LC_ROUTINES 这个 load command 的动态库,暂时不深究吧~~~

    紧接着,就来到了调用栈上最初的 doModInitFunctions 函数了,这个函数做了这么几件事:

    1. 递归寻找 Load Command,找到 S_MOD_INIT_FUNC_POINTERS 这个 section 对应的 Load Command;
    2. 根据 slide 计算 S_MOD_INIT_FUNC_POINTERS 的具体位置,并且取出这个表中的函数指针;
    3. 进行一系列判断之后调用这些函数;
    4. 在函数调用前后进行判断,如果函数调用使得 dyld::gLibSystemHelpers 有值了,证明 libSystem 初始化完成,此时将 dyld::gProcessInfo->libSystemInitialized 标志置为 true;

    关键代码:

    doModInitFunctions

    简而言之:

    1. dyld 在动态链接完成之后会执行所有动态库的初始化函数,最后执行主工程的初始化函数;
    2. 初始化函数需要使用 __attribute__修饰,编译器识别之后会存储在 Mach-O 文件的 __mod_init_func 中;
    3. 因为 libSystem 是一系列系统库的集合,被很多动态库依赖,优先级更高,libSystem 的初始化函数会在比较靠前的顺序开始执行(不是第一)。而 objc 就被包含在这个库中。objc 库的初始化方法 objc_init 就是在 libSystem 的初始化函数中被调用;
    4. objc_init 方法中包含了 OC 类的加载逻辑;

    至此,可以做个阶段性总结了:

    1. dyld 初始化函数调用阶段会去递归调用 image 的初始化函数;
    2. libSystem 库在比较靠前的位置被调用,进而触发了 _objc_init 函数的调用;

    2. _objc_init 方法做了什么

    来看下 objc_init 方法里面的代码吧:

    void _objc_init(void) {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
        
        // 环境初始化相关
        environ_init();
        // 线程相关
        tls_init();
        // objc库初始化方法调用,即objc库中被__attribute__修饰的方法
        static_init();
        // 暂无任何逻辑
        lock_init();
        // NSSetUncaughtExceptionHandler()的基础
        exception_init();
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    }
    

    如上,两个点可以稍微关注下:

    1. static_init(); 方法调用了 objc 库内部的初始化方法。一般而言 image 的初始化方法在 dyld 的第八步中被调用,而 objc 则主动调用了自己的初始化函数,有兴趣的可以见后文;
    2. exception_init(); 方法内部实现是 iOS 中使用 NSSetUncaughtExceptionHandler() 的基础。该方法可以设置 crash 后的处理逻辑,也是早起友盟、bugly 等三方 crash 监控 SDK 获取 crash 堆栈信息的基础:
    NSSetUncaughtExceptionHandler

    从上面代码来看,貌似 objc 并没有进行类的加载?此时就需要关注 _dyld_objc_notify_register 以及对应的三个回调了,这个方法是怎么个逻辑?

    3. dyld 和 objc 的联系

    _dyld_objc_notify_register 这个方法由 dyld 提供,定义如下:

    //
    // Note: only for use by objc runtime
    // Register handlers to be called when objc images are mapped, unmapped, and initialized.
    // Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
    // Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
    // call dlopen() on them to keep them from being unloaded.  During the call to _dyld_objc_notify_register(),
    // dyld will call the "mapped" function with already loaded objc images.  During any later dlopen() call,
    // dyld will also call the "mapped" function.  Dyld will call the "init" function when dyld would be called
    // initializers in that image.  This is when objc calls any +load methods in that image.
    //
    void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                    _dyld_objc_notify_init      init,
                                    _dyld_objc_notify_unmapped  unmapped);
    

    注释的大意是:

    1. 该方法专门为 objc-runtime 提供;
    2. 三个回调会在 image 被 mapped、unmapped、initialized 时分别被触发;
    3. dyld 在调用 mapped 这个回调时,会传递一个 image 数组,这些 image 都是 objc 相关的 image;
    4. objc 不需要再调用 dlopen() 方法来加载或者维持这些 image。后续有新的 image 被载入时,仍然会调用 mapped 相关的回调;
    5. dyld 会在调用 image 初始化函数阶段触发 init 回调,而这个回调就是 objc 调用 +load 方法的时机;

    紧接着,一一验证上述的注释。首先在 dyld 中找到这个函数:

    _dyld_objc_notify_register

    _dyld_objc_notify_register 只是一个对外包装接口,关键方法在 registerObjCNotifiers

    registerObjCNotifiers

    根据注释,dyld 会通过 notifyBatchPartial 函数触发 mapped 回调。因为 mapped 的回调被绑定到了 sNotifyObjCMapped 这个指针,所以我们看代码时只需要关注 sNotifyObjCMapped 的调用逻辑即可,来看看这个函数的关键代码

    notifyBatchPartial

    打个断点来验证:

    iOS15

    咦?有点不一样?别慌,这个是用的 iOS15 的模拟器,很明显,dyld4 已经都被用上了。用 iOS12 的 iPhone7 看看:

    notifyBatchPartial

    完美,结论被完美验证~~~

    即:

    map_images() 是 objc 中类初始化的主要函数。该函数在 _objc_init() 调用 dyld 进行回调绑定时就会通过 notifyBatchPartial 被触发,进而 objc 会对当前所有 objc 相关的 image 进行类的初始化操作。

    4. load函数的调用逻辑

    load 函数调用栈:

    load

    感觉核心在 notifySingle 这个函数,首先回到初始化函数的调用逻辑上,在recursiveInitialization 函数中对 notifySingle 调用如下:

    recursiveInitialization

    上图可看出:

    1. 在初始化操作之前调用了一次 notify,根据注释可以看出,应该是即将初始化对应 image 的一个通知;
    2. 初始化操作之后之后,发送了初始化完成的通知;

    这里的重点在第一次 notify 的 dyld_image_state_dependents_initialized,来看看 notifySingle 函数中的关键代码:

    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();
    
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    
        uint64_t t1 = mach_absolute_time();
        uint64_t t2 = mach_absolute_time();
        uint64_t timeInObjC = t1-t0;
        uint64_t emptyTime = (t2-t1)*100;
        if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
            timingInfo->addTime(image->getShortName(), timeInObjC);
        }
    }
    

    不看 time 相关的代码,关键代码逻辑就是:

    1. 判断 sNotifyObjCInit 是否存在;
    2. 存在则执行 sNotifyObjCInit,传递 image 的 path 和 mh_header 的地址;

    那么 sNotifyObjCInit 是个啥?全局搜一下找到:

    registerObjCNotifiers

    registerObjCNotifiers 又是啥呢?继续全局搜索:

    _dyld_objc_notify_register

    很明显,又是 _dyld_objc_notify_register 函数,这个函数注册了三个回调,load_image 就是在 image 的初始化函数即将被调用之前会被触发的回调。

    其实 fishhook 也是用到了该文件下的 Api,只不过是 _dyld_register_func_for_add_image函数,该方法是添加 image 相关的回调,大概逻辑有点类似,具体就不赘述了;

    总结下逻辑:

    1. objc 在初始化函数 _objc_init 调用 dyld 的 Api 设置了依赖库被加载时的回调;
    2. 依赖库即将被调用初始化方法时,通过通知触发回调;
    3. 回调执行预先设置的函数,也就是 objc 中的 load_images 函数;
    4. load_images 函数执行 objc 的类加载的逻辑,触发 +load 方法的调用;

    其实这里挺重要的,必须在所有依赖库的初始化函数执行 之前 (也就是两个通知的前者)进行 objc 的 load 逻辑。因为该 image 初始化函数可能使用了自身定义的类,而 load 函数就是将该 image 的 objc 类加载进入 runtime,如果不优先执行 load 操作,那么执行初始化方法时可能因为找不到对应的类而出错;

    5. 补充:objc 自己调用初始化函数

    比较好玩的一点是:objc 库中的初始化函数是 objc 自己调用的,而不是 dyld。

    这里首先要从我们经常涉及到的 objc_init 来说起:

    objc_init

    该方法通过 static_init 方法完成了 objc 库中初始化方法的调用:

    static_init

    看看 getLibobjcInitializers 是个啥?

    mach-O

    本质上是常见的 GETSECT 方法,但是这里最关键的是 __objc_init_func 。objc 用这个标记来在 mach-O 文件中来标识 objc 独有的初始化函数。

    但是,初始化方法不是一般都存放在 __mod_init 这个 section 中吗? dyld 内部也是这个逻辑:

    dyld初始化函数调用

    这个 S_MOD_INIT_FUNC_POINTERS 在 mach-o 相关的源码中:

    image.png

    实际测试结果:

    初始化函数

    结论:dyld 通过 mach-O 文件中的 __mod_init 这个 section 来记录并调用初始化函数;

    看到这里会想当疑惑,难道 objc 的初始化方法不是 dyld 加载的?继续查找 objc 源码,看看 objc 对这个 __objc_init_func 做了什么? 在 markgc 的 main 函数中做了这么一个操作:

    imarkgc

    markgc 是个啥?猜测是个脚本之类的东西?markgc 的 main 函数最终会触发这个 dosect 方法。也就是说 markgc 这个程序在 objc 的 mach-O 文件生成之后(可以理解成被编译成了动态库之后),手动修改了初始化方法对应的 sectionName 和 type。

    而 dyld 调用初始化方法是通过 mach-O 文件中的 __mod_init 这个 section 来完成调用的。objc 做了这么个骚操作之后, dyld 就不会(没办法,因为找不到对应的 section)在初始化阶段(倒数第二步,即调用 main 函数之前)去调用这些初始化函数了。

    按照 Apple 给出的理由是,dyld 调用 objc 的初始化函数的时机太晚,主要是晚于 libc 的调用:

    libc calls _objc_init() before dyld would call our static constructors, so we have to do it ourselves

    总结:

    1. libc 可能被包装在了 libSystem 中,而 libc 需要调用 objc,且这个调用发生在 dyld 调用 objc 初始化函数之前,所以 objc 需要自己来调用初始化函数;
    2. objc 通过 markgc 程序将 __mod_init 修改为 __objc_init_function,从而适配自己的 static_init 逻辑,同时也避免了 dyld 对 objc 初始化函数的重复调用;

    6. 一个疑问

    image list

    如上图,断点打在 ImageLoaderMachO::doModInitFunctions 时,libSystem 显然不是第一个 image,此时就有个疑问:

    1. 为什么自己嵌入的动态库 UPBase 那么靠前?
    2. 如果按照这个 image list 顺序进行初始化调用,那么 UPBase 被初始化时肯定 libSystem 还没有初始化。虽然 map_images() 在后续被调用时会遍历所有 image,但是如果是涉及到 UPBase 中有初始化函数调用,那么此时 objc 仍然没有初始化的,这样会不会有问题?
    3. 如果没问题,那逻辑是怎样的呢? dyld 是进行了 image 顺序调整,类似于依赖层级调整?或者说 image list 指令打印的不是当前 dyld 中的 image list 中的顺序?

    7. 总结

    一张图做个总结吧:

    总结

    相关文章

      网友评论

          本文标题:iOS类加载流程(一):类加载流程的触发

          本文链接:https://www.haomeiwen.com/subject/lzafvrtx.html