美文网首页 iOS进阶之面试题
Objective-C中的load方法执行的来龙去脉

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

作者: brownfeng | 来源:发表于2019-02-15 18:35 被阅读31次

    引子

    我们都知道: 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

    相关文章

      网友评论

        本文标题:Objective-C中的load方法执行的来龙去脉

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