iOS性能调优iOS开发技术

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

2019-06-26  本文已影响84人  tom555cat

背景

方案来自美团外卖冷启动治理: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]:美团外卖冷启动治理

上一篇下一篇

猜你喜欢

热点阅读