背景
方案來自美團外賣冷啟動治理:http://www.reibang.com/p/8e0b38719278
- 在App啟動的時候功偿,如果將啟動項都寫在didFinishLaunch中媒吗,當啟動項非常多時,這一塊內(nèi)容會非常臃腫坝疼;
- 并不是所有的模塊啟動項都應該放在didFinishLaunch中搜贤,比如一個啟動項非常耗時,盡管可以寫在didFinishLaunch最后钝凶,但還是會影響首頁的渲染仪芒;而直接寫在首頁的viewDidAppear中,這些與首頁不相關的啟動項代碼會耦合在一起耕陷。
- 如果通過啟動階段發(fā)布通知掂名,模塊注冊響應通知來管理啟動項;那么模塊注冊通知的代碼需要寫在+load()函數(shù)中哟沫,這必然會影響冷啟動main()函數(shù)執(zhí)行之前階段饺蔑。
美團外賣[1]給出的思路就是在編譯時,將模塊的啟動函數(shù)指針保存在可執(zhí)行文件的__DATA段中嗜诀,在需要的執(zhí)行的時候從_DATA段中將函數(shù)指針取出來再執(zhí)行猾警。
先看一下實現(xiàn)效果,通過如下方式將模塊的啟動項注冊到STAGE_A階段啟動:
#import "XCDynamicLoader.h"
XC_FUNCTION_EXPORT(STAGE_A)(){
// 啟動項代碼
}
加入STAGE_A步驟的啟動項需要在application:didFinishLaunchingWithOptions:中執(zhí)行隆敢,可以通過如下方式來實現(xiàn):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// 執(zhí)行STAGE_A階段注冊的啟動項函數(shù)
[XCDynamicLoader executeFunctionsForKey:@"STAGE_A"];
return YES;
}
實現(xiàn)原理
實現(xiàn)原理就是在編譯時將數(shù)據(jù)(啟動項函數(shù)指針)保存進__DATA段发皿,在需要數(shù)據(jù)(啟動項函數(shù)指針)的時候從__DATA段中讀出來。如下圖[1]所示:
- 將數(shù)據(jù)寫入__DATA段
XC_FUNCTION_EXPORT(LEVEL_A)(){
NSLog(@"level A, ViewController");
}
上述在模塊內(nèi)定義的啟動函數(shù)拂蝎,經(jīng)過預處理之后雳窟,展開結(jié)果如下所示:
// 啟動函數(shù)封裝在XC_Function結(jié)構(gòu)體中
struct XC_Function {
char *key;
void (*function)(void);
};
// 聲明啟動函數(shù)
static void _xcSTAGE_C(void);
// 將包含啟動函數(shù)的結(jié)構(gòu)體XC_Function保存在__DATA段的__STAGE_Cxc_func節(jié)中
__attribute__((used, section("__DATA" ",__""STAGE_C" "xc_func")))
static const struct XC_Function __FSTAGE_C = (struct XC_Function){(char *)(&"STAGE_C"), (void *)(&_xcSTAGE_C)};
// 定義啟動函數(shù)
static void _xcSTAGE_C(){
NSLog(@"STAGE C, TLMStageC, execute in viewDidAppear");
}
我們首先定義了啟動項函數(shù)void _xcSTAGE_C(),然后將啟動項函數(shù)指針存儲在struct XC_Function中,struct XC_Function還可以保存其他字段封救,然后將這個struct XC_Function寫入靜態(tài)變量__FSTAGE_C中。
最關鍵的地方是用于修飾靜態(tài)變量的“attribute((used, section("DATA" ",""STAGE_C" "xc_func"))) ”這一段代碼捣作,通過clang提供的section函數(shù)誉结,將struct XC_Function數(shù)據(jù)放置與__DATA段的"__STAGE_Cxcfunc"節(jié)中,如下圖所示:
- 將數(shù)據(jù)從__DATA段中讀取出來
從__DATA中讀取出來主要是通過“+[XCDynamicLoader executeFunctionsForKey:]”來指定具體的階段來讀取__DATA中相應的Section(節(jié))中保存的struct XC_Function券躁,然后取出其中的函數(shù)指針進行執(zhí)行惩坑。
從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函數(shù)的第一個參數(shù)是Section名字,即處于那一節(jié)也拜,第二個參數(shù)是MachO文件的mach_header以舒,讀取數(shù)據(jù)的段默認為__DATA。
在app中慢哈,可執(zhí)行文件是一個MachO文件蔓钟,動態(tài)庫也是一個MachO文件,這些MachO文件中都有可能注冊了啟動項卵贱,所以需要在app加載每一個MachO文件的時候都要讀取其中注冊的啟動項滥沫。我們使用_dyld_register_func_for_add_image函數(shù),該函數(shù)是用來注冊dyld加載鏡像時的回調(diào)函數(shù)键俱,在dyld加載鏡像時兰绣,會執(zhí)行注冊過的回調(diào)函數(shù)。
*_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))"修飾了函數(shù)initXCProphet()编振,initXCProphet()會在可執(zhí)行文件(或動態(tài)庫)load的時候被調(diào)用缀辩,可以理解為在main()函數(shù)調(diào)用之前執(zhí)行。
我們在回調(diào)函數(shù)中踪央,讀取了每一個MachO文件中的注冊的各個階段的啟動函數(shù)臀玄,通過一個單例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中注冊模塊啟動函數(shù)杯瞻,同理__DATA中可以注冊字符串等其他數(shù)據(jù)镐牺,而美團外賣冷啟動中的例子"KLN_STRINGS_EXPORT("Key", "Value")"就是一個向__DATA中注冊字符串的案例,可以探索編譯時通過__DATA保存自定義數(shù)據(jù)的更多用途魁莉。
這是源碼地址:項目代碼