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

背景

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容