背景
方案来自美团外卖冷启动治理:https://www.jianshu.com/p/8e0b38719278
- 在App启动的时候,如果将启动项都写在didFinishLaunch中,当启动项非常多时,这一块内容会非常臃肿;
- 并不是所有的模块启动项都应该放在didFinishLaunch中,比如一个启动项非常耗时,尽管可以写在didFinishLaunch最后,但还是会影响首页的渲染;而直接写在首页的viewDidAppear中,这些与首页不相关的启动项代码会耦合在一起。
- 如果通过启动阶段发布通知,模块注册响应通知来管理启动项;那么模块注册通知的代码需要写在+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段
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段中读取出来
从__DATA中读取出来主要是通过“+[XCDynamicLoader executeFunctionsForKey:]”来指定具体的阶段来读取__DATA中相应的Section(节)中保存的struct XC_Function,然后取出其中的函数指针进行执行。
从MachO文件的Segment中读取Section的具体方式如下所示:
NSArray* 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]:美团外卖冷启动治理