Objective-C中的load方法执行的来龙去脉

引子

我们都知道: Objective-C中类Class+load方法会在类第一次加载到内存时, 并且APP的整个生命周期只会执行一次. 但是知其然最好知其所以然, 今天来分析一下+load方法执行的来龙去脉.

准备工作, 本文涉及到的Apple 开源源码如下:

  • dyld-635.2
  • objc4-750

上一篇文章<<iOS APP启动前后发生了什么?>>开篇, 有如下的调用栈:

0 +[AppDelegate load]
1 call_load_methods
2 load_images
// 这里是一个断层
3 dyld::notifySingle(dyld_image_states, ImageLoader const*)
4 ImageLoader::recursiveInitialization(...)
5 ImageLoader::processInitializers(...)
6 ImageLoader::runInitializers(...)
7 dyld::_main(...)
8 dyldbootstrap::start(...)
9 _dyld_start

我们能看到实际最后会调用+[Class load]类方法, 我们发现从调用栈那里有一个断层, 3 dyld::notifySingle(dyld_image_states, ImageLoader const*) -> +[AppDelegate load]的过程, 明显不是在dyld, ImageLoader库中, 而是在runtime中的方法, 重要的原因就是dyld::notifySingle是对外发送两个一个通知, 而loadImage是针对通知注册的handler.

而前文在讲到, 当运行到后面会在runtime初始化时调用_objc_init, 这个方法最后会调用dyld::_dyld_objc_notify_register方法注册三个hanlder, 其中有一个方法就是runtimeload_images, 因此dyld::notifySingle实际是发出了某个通知, 触发load_images.

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();
    lock_init();
    exception_init();

    // 这里是在dyld中加入一个监听器, 一旦dyld监听到有新的镜像加载到runtime时, 就调用 load_images 方法, 并传入最新镜像的信息类别 infoList
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

dyld_image的state的监听与通知

为了证明我们前面的内容, 我们需要在源码中去找到线索.

我们打开dyld的源码dyld_priv.h, 中的关于dyld_image_states的定义:

// DEPRECATED 
// dyld_image 整个生命周期中会经历的状态
enum dyld_image_states {
    dyld_image_state_mapped                 = 10,       // No batch notification for this - 是否已经映射
    dyld_image_state_dependents_mapped      = 20,       // Only batch notification for this - 依赖是否映射
    dyld_image_state_rebased                = 30,       // rebase
    dyld_image_state_bound                  = 40,       // 已经bound
    dyld_image_state_dependents_initialized = 45,       // Only single notification for this
    dyld_image_state_initialized            = 50,       // -- 已经初始化!!!!! 重要的状态
    dyld_image_state_terminated             = 60        // Only single notification for this
};

而且dyld_image.state的状态切换都是在dyld::_main(...)方法中进行的, 我们将该方法简写如下:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[], uintptr_t* startGlue){
    
    ...

    // dyld::instantiateFromLoadedImage ->  ImageLoaderMachOClassic::instantiateMainExecutable(create image for main executable) -> setMapped -> dyld_state = dyld_image_state_mapped-> 发出notification
    // 初始化完成以后, sMainExecutable被push到 sAllImages, 并且将它的关键信息插入到MappedRanges链表(这个链表中的内容已经mapped完毕)
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); // dyld_image_state_mapped 并通知

    ...

    /*
    注意, 这里执行 link(...) 时, linkingMainExecutable = true!!!
    1. recursiveLoadLibraries -> dyld_image_state_dependents_mapped 并通知
    2. recursiveRebase -> dyld_image_state_rebased 并通知
    (不会执行: 3. recursiveBindWithAccounting -> recursiveBind -> dyld_image_state_bound)
    (不会执行: 4. weakBind -> 通知 dyld_image_state_bound)
    */
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

    ...

    gLinkContext.linkingMainExecutable = false;

    // 从这里开始 linkingMainExecutable = false, 也就是 MainImageLoader完成link操作!!! 切换 MainImageLoader.fstate = dyld_image_state_bound, 并发送 dyld_image_state_bound_notify 通知
    // Bind and notify for the main executable now that interposing has been registered
    uint64_t bindMainExecutableStartTime = mach_absolute_time();
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    uint64_t bindMainExecutableEndTime = mach_absolute_time();
    ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
    gLinkContext.notifyBatch(dyld_image_state_bound, false);

    ...

    /*
    这里开始执行各个dyld_image的initializers方法, 当初始化完成以后, 就MainImageLoader.fstate = dyld_image_state_initialized, 并发送 dyld_image_state_initialized_notify 通知
    1. initializeMainExecutable
    2. sMainExecutable->runInitializers
    3. sMainExecutable->processInitializers
    4. context.notifyBatch(dyld_image_state_initialized, false);
    */
    initializeMainExecutable(); 

    ...

    return Main函数的入口
}

在梳理之前, 我们需要有一个简单的概念, 关于link(..)过程中的rebasebind.

mach-odyld_image二进制文件被加载到内存中以后, 由于地址空间加载随机化(ASLR, Address Space Layout Randomization)的缘故, 二进制文件最终的加载地址与预期地址之间会存在偏移, 所以需要进行rebase操作, 对那些指向文件内部符号的指针进行修正, 在 link 函数中该项操作由 recursiveRebase 函数执行. rebase 完成之后, 就会进行 bind 操作, 修正那些指向其他二进制文件所包含的符号的指针, 由 recursiveBind函数执行。 当rebase以及bind结束时, link函数就完成了它的使命.

我们能看到在dyld::_main(...)函数中dyld_image会随着过程切换自己的state状态, 并且对外发出相关状态的通知.

同时我们在源码中有如下代码:

// DEPRECATED -- 当 dyld_image 的state状态变化以后, 调用的回调函数callback格式如下
typedef const char* (*dyld_image_state_change_handler)(enum dyld_image_states state, uint32_t infoCount, const struct dyld_image_info info[]);

// 注册的方法的 函数指针当  mapped/ init/ unmapped 状态时, 分别调用的callback格式如下
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);

// 
// Note: only for use by objc runtime 
// 这个方法只有在 runtime 的 _objc_init 方法中调用
// 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.  

// 1. During the call to _dyld_objc_notify_register(), dyld will call the "mapped" function with already loaded objc images.  
// 2. During any later dlopen() call, dyld will also call the "mapped" function.  (每一次调用dlopen(), 都会调用'mapped' function)
// 3. 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. - 当 image状态变化成 initializer 时候, 会调用`init` callback, 这个callback在实际代码中是调用的 objc4.750 的 `loadImages` 方法, 这个方法内部会调用这个`image`中的每个`Class`的`+load`方法

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);
}

注意上面这个方法_dyld_objc_notify_register是在dyld::_main方法中的mainImageLoaderlink(...)方法结束以后, 由于依赖的库中有libSystemlibCloure从而加载runtime_objc_init(...)方法结束时候才调用, 因此在执行_dyld_objc_notify_register以后, 相当于runtime就会监听所有在runtime之后被加载的dyld_image, 根据他们的的状态, 去调用注册的3个回调函数, 这里我们重点关注load_images方法.

load_imagesruntime_objc_init(...)被注册以后,一旦dyld中有新的image状态成为init(也就是dyld_image_state_initialized, 此时表示该image已经完成link), 就会调用load_images方法, 对这个完全初始化成功的image中的内容做一些处理.

objc中的load_images

load_images方法的源码如下:

/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock

 有新的镜像image被加载到 runtime 时,调用 load_images 方法,并传入最新镜像image的信息列表 infoList:

  images 是镜像的意思: 这里就会遇到一个问题:镜像到底是什么,我们用一个断点打印出所有加载的镜, 从控制台输出的结果大概就是这样的,我们可以看到镜像并不是一个 Objective-C 的代码文件,它应该是一个 target 的编译产物。这里面有很多的动态链接库,还有一些苹果为我们提供的框架,比如 Foundation、 CoreServices 等等,都是在这个 load_images 中加载进来的,而这些 imageFilePath 都是对应的二进制文件的地址。

 +load 的应用:

 +load 可以说我们在日常开发中可以接触到的调用时间最靠前的方法,在主函数运行之前,load 方法就会调用。

 由于它的调用不是惰性的,且其只会在程序调用期间调用一次,最最重要的是,如果在类与分类中都实现了 load 方法,它们都会被调用,不像其它的在分类中实现的方法会被覆盖,这就使 load 方法成为了方法调剂的绝佳时机。

 但是由于 load 方法的运行时间过早,所以这里可能不是一个理想的环境,因为某些类可能需要在在其它类之前加载,但是这是我们无法保证的。不过在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的。
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);

void load_images(const char *path __unused, const struct mach_header *mh) {
    // Return without taking locks if there are no +load methods here.
    // 如果 没有 +load 方法, 直接返回
    if (!hasLoadMethods((const headerType *)mh)) return;

    // 此时表示 mh中有 +load 方法

    // 上锁, 不能同时多个线程执行 loadMethod, 锁1
    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        // runtimeLock 两个锁, 锁2
        // 这里 write-locks 需要两个锁
        mutex_locker_t lock2(runtimeLock);
        //调用 prepare_load_methods 对 load 方法的调用进行准备, 主要工作就是将Class的所有方法都加载到一个叫loadable_classes的数组中
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    // 在将镜像加载到运行时, 对 load 方法的准备就绪之后,执行 call_load_methods,开始调用 load 方法
    call_load_methods();
}

load_images中的源码游走以后, 我们主要看到两个重要的步骤 -- 准备load和调用load:

  1. 当有新的镜像被dyld加载, runtime就会去该镜像中对所有的 class/category 进行准备操作.
  2. 准备操作是prepare_load_methods
  3. 调用操作是call_load_methods

objc中如何准备 -- prepare_load_methods解析

/**
 准备load methods
 */
void prepare_load_methods(const headerType *mhdr) {
    size_t count, i;

    // 调用 load_method 时, 必须是 runtimeLock已经上锁
    runtimeLock.assertLocked();

    //处理mach-o中的class:
    // 通过 _getObjc2NonlazyClassList 获取二进制文件中所有的类的列表之后,会通过 remapClass 获取类对应的指针,然后调用 schedule_class_load 递归地安排当前类的父类和当前类加入到一个 loadable_list中
    classref_t *classlist =
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 内部处理以后调用 add_class_to_loadable_list方法
        schedule_class_load(remapClass(classlist[i]));
    }

    //处理mach-o中的categorys:
    // 通过 _getObjc2NonlazyCategoryList 方法获取二进制文件中所有的category的列表, 然后递归处理每个单独的category.
    // 单独处理Category的过程如下: 首先获取每个category的Class, 然后先调用一个关键的方法`realizeClass`, 这个方法能够保证每个类已经被runtime进行了`realize`过, 这个过程很重要(后面有专门的文章来解释整个realize的过程), 然后调用`add_category_to_loadable_list`将category方法加入loadable_list
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        // realizeClass 做的工作就是Class第一次 initiail
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        // 获取 category中的+load方法, 然后按照一定顺序将+load方法加入到一个loadabel_list中
        add_category_to_loadable_list(cat);
    }
}

通过源码注释, 我们可以看出准备过程会处理两块内容, 分别是镜像中的class以及category.

如果处理镜像中的class, 过程是:

  1. _getObjc2NonlazyClassList获取二进制文件中所有的类, 放到一个链表中, 然后递归处理每个class
  2. 遍历这个链表, 取出每个节点, 先调用remapClass, 然后调用schedule_class_load
  3. schedule_class_load主要是将入参的class的继承链的每个+load方法都加入到loadable_classes链表中. 注意这里的添加+load方法到链表的顺序是, 先父类, 然后自己.

如果处理镜像中的category, 过程有点不一样:

  1. _getObjc2NonlazyCategoryList方法获取二进制中所有的category, 放到一个链表中, 然后递归处理每个category
  2. 单独category的过程是: 首先获取每个category对应的class, 先对class进行remapClass,调用一个关键的方法realizeClass(我们可以认为这个方法是Class类对象在内存中的初始化创建方法),最后调用add_category_to_loadable_list方法
  3. add_category_to_loadable_list是将category按照一定顺序将+load方法加入到一个叫做loadable_categories链表中.

realizeClass方法我们后面专门分析, 这里我们只简单了解一下. 我们直到Class在编译期间, 有很多方法是我们自己在代码里面定义的, 这些方法在编译器编译期间就搞定了, 当它加载到内存时候, 它的方法列表里面都是编译期间确定的方法, 我们称为只读方法, 但是还有一些方法例如在category中的方法, 也是与Class有关的, 但是并没有与这个Class关联, 通过realizeClass来调整类在内存中的结构, 例如将category中的方法都关联到class上去, 添加到class的方法列表中, 当然, 还有一些其他的作用, 后面再讲.

objc中正式调用每个类的+load方法 -- call_load_methods解析

void call_load_methods(void) {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads(); // 这里会调用  load 方法
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    // 用一个便利结构体, 内部持有Class对应方法的+load IMP
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    // 遍历所有的 loadable_classes 中的每个 loadable Class, 从中按照顺序取出+load方法, 按照 loadable_list 的顺序执行!!!
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        // 某个类的 +load 方法会执行!!!
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

简单来说, 就是将loadable_classes中之前存储的class+load调用, 然后清理, 最后调用loadable_categories缓存的category相关的+load方法.

小总结

  1. +load方法是如何被调用的?

runtime在初始化时, 会注册一个回调, 去监听镜像加载, 每次有新的镜像加载时, 就会调用注册的load_images回调, 这个方法会将镜像中所有类和分类的+load方法按照一定顺序, 放到loadable_classesloadable_categories两个链表中, 然后遍历执行两个链表中的每个节点的+load方法.

  1. +load方法的调用顺序如何?
    1. 父类的+load会先调用, 然后才调用子类+load
    2. 类的+load会先调用, 然后调用分类的+load
    3. 总得来说, 会先调用super类的+load方法, 然后调用自身的+load方法, 最后调用分类重写的+load方法.

并且结合前面文章我们知道: +load方法会先于app的启动方法main执行, 并且它在全局只会调用一次等特性, +load方法是让我们实现的method swizzling最佳位置!!!!

就算分类重写了+load方法, 通过上面分析, 仍然会按照父类, 本类, 分类的顺序执行+load方法. 需要注意这点比较特殊, 与其他方法的执行不一样!!!

参考

iOS程序启动->dyld加载->runtime初始化(初识)
你真的了解load方法么?
http://www.cocoachina.com/ios/20170716/19876.html

推荐阅读更多精彩内容