美文网首页iOS性能调优iOS开发技术
iOS冷启动优化之模块启动项自注册实现

iOS冷启动优化之模块启动项自注册实现

作者: tom555cat | 来源:发表于2019-06-26 00:22 被阅读84次

    背景

    方案来自美团外卖冷启动治理:https://www.jianshu.com/p/8e0b38719278

    1. 在App启动的时候,如果将启动项都写在didFinishLaunch中,当启动项非常多时,这一块内容会非常臃肿;
    2. 并不是所有的模块启动项都应该放在didFinishLaunch中,比如一个启动项非常耗时,尽管可以写在didFinishLaunch最后,但还是会影响首页的渲染;而直接写在首页的viewDidAppear中,这些与首页不相关的启动项代码会耦合在一起。
    3. 如果通过启动阶段发布通知,模块注册响应通知来管理启动项;那么模块注册通知的代码需要写在+load()函数中,这必然会影响冷启动main()函数执行之前阶段。

    美团外卖[1]给出的思路就是在编译时,将模块的启动函数指针保存在可执行文件的__DATA段中,在需要的执行的时候从_DATA段中将函数指针取出来再执行。
    先看一下实现效果,通过如下方式将模块的启动项注册到STAGE_A阶段启动:

    #import "XCDynamicLoader.h"
    
    XC_FUNCTION_EXPORT(STAGE_A)(){
        // 启动项代码
    }
    

    加入STAGE_A步骤的启动项需要在application:didFinishLaunchingWithOptions:中执行,可以通过如下方式来实现:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        // 执行STAGE_A阶段注册的启动项函数
        [XCDynamicLoader executeFunctionsForKey:@"STAGE_A"];
        return YES;
    }
    

    实现原理

    实现原理就是在编译时将数据(启动项函数指针)保存进__DATA段,在需要数据(启动项函数指针)的时候从__DATA段中读出来。如下图[1]所示:

    __DATA段数据读写
    1. 将数据写入__DATA段
    XC_FUNCTION_EXPORT(LEVEL_A)(){
        NSLog(@"level A, ViewController");
    }
    

    上述在模块内定义的启动函数,经过预处理之后,展开结果如下所示:

    // 启动函数封装在XC_Function结构体中
    struct XC_Function {
        char *key;
        void (*function)(void);
    };
    
    // 声明启动函数
    static void _xcSTAGE_C(void);
    
    // 将包含启动函数的结构体XC_Function保存在__DATA段的__STAGE_Cxc_func节中
     __attribute__((used, section("__DATA" ",__""STAGE_C" "xc_func"))) 
    static const struct XC_Function __FSTAGE_C = (struct XC_Function){(char *)(&"STAGE_C"), (void *)(&_xcSTAGE_C)}; 
    
    // 定义启动函数
    static void _xcSTAGE_C(){
        NSLog(@"STAGE C, TLMStageC, execute in viewDidAppear");
    }
    
    

    我们首先定义了启动项函数void _xcSTAGE_C(),然后将启动项函数指针存储在struct XC_Function中,struct XC_Function还可以保存其他字段,然后将这个struct XC_Function写入静态变量__FSTAGE_C中。
    最关键的地方是用于修饰静态变量的“attribute((used, section("DATA" ",""STAGE_C" "xc_func"))) ”这一段代码,通过clang提供的section函数,将struct XC_Function数据放置与__DATA段的"__STAGE_Cxcfunc"节中,如下图所示:

    __DATA段中自定义的__STAGE_Axc_func节
    1. 将数据从__DATA段中读取出来
      从__DATA中读取出来主要是通过“+[XCDynamicLoader executeFunctionsForKey:]”来指定具体的阶段来读取__DATA中相应的Section(节)中保存的struct XC_Function,然后取出其中的函数指针进行执行。
      从MachO文件的Segment中读取Section的具体方式如下所示:
    NSArray<NSValue *>* XCReadSection(char *sectionName, const struct mach_header *mhp) {
        NSMutableArray *funcArray = [NSMutableArray array];
        
        const XCExportValue mach_header = (XCExportValue)mhp;
        const XCExportSection *section = XCGetSectByNameFromHeader((void *)mach_header, XCDYML_SEGMENTNAME, sectionName);
        if (section == NULL) return @[];
        
        int addrOffset = sizeof(struct XC_Function);
        for (XCExportValue addr = section->offset;
             addr < section->offset + section->size;
             addr += addrOffset) {
            
            struct XC_Function entry = *(struct XC_Function *)(mach_header + addr);
            [funcArray addObject:[NSValue valueWithPointer:entry.function]];
        }
        
        return funcArray;
    }
    

    XCReadSection函数的第一个参数是Section名字,即处于那一节,第二个参数是MachO文件的mach_header,读取数据的段默认为__DATA。
    在app中,可执行文件是一个MachO文件,动态库也是一个MachO文件,这些MachO文件中都有可能注册了启动项,所以需要在app加载每一个MachO文件的时候都要读取其中注册的启动项。我们使用_dyld_register_func_for_add_image函数,该函数是用来注册dyld加载镜像时的回调函数,在dyld加载镜像时,会执行注册过的回调函数。

    *_dyld_register_func_for_add_image()
    registers the specified function to be called when a new image is added (a bundle or a dynamic shared library) to the program. When this function is first registered it is called for once for each image that is currently part of the process.

    代码如下所示:

    __attribute__((constructor))
    void initXCProphet() {
        _dyld_register_func_for_add_image(dyld_callback);
    }
    

    代码中通过"attribute((constructor))"修饰了函数initXCProphet(),initXCProphet()会在可执行文件(或动态库)load的时候被调用,可以理解为在main()函数调用之前执行。

    我们在回调函数中,读取了每一个MachO文件中的注册的各个阶段的启动函数,通过一个单例XCModuleManager保存起来:

    static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
        for (NSString *stage in [XCModuleManager sharedManager].stageArray) {
            NSString *fKey = [NSString stringWithFormat:@"__%@%s", stage?:@"", XCDYML_SECTION_SUFFIX];
            NSArray *funcArray = XCReadSection((char *)[fKey UTF8String], mhp);
            [[XCModuleManager sharedManager] addModuleInitFuncs:funcArray forStage:stage];
        }
    }
    

    模块启动阶段定义在了XCModuleManager中的stageArray中,模块启动项需要指定为其中一项来在指定阶段来启动:

    - (instancetype)init {
        self = [super init];
        if (self) {        
            self.stageArray = @[
                                @"STAGE_A",
                                @"STAGE_B",
                                @"STAGE_C",
                                @"STAGE_D"
                                ];
            self.modInitFuncPtrArrayStageDic = [NSMutableDictionary dictionary];
            for (NSString *stage in self.stageArray) {
                self.modInitFuncPtrArrayStageDic[stage] = [NSMutableArray array];
            }
        }
        return self;
    }
    

    Next

    上述功能是在__DATA中注册模块启动函数,同理__DATA中可以注册字符串等其他数据,而美团外卖冷启动中的例子"KLN_STRINGS_EXPORT("Key", "Value")"就是一个向__DATA中注册字符串的案例,可以探索编译时通过__DATA保存自定义数据的更多用途。

    这是源码地址:项目代码

    参考文献

    [1]:美团外卖冷启动治理

    相关文章

      网友评论

        本文标题:iOS冷启动优化之模块启动项自注册实现

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