基于Mach-O的启动项管理

业务初期App的启动项并不多,随着业务的正常和版本的迭代,各种第三方库和自研的一些库的启动可能会都建议在App启动时期去初始化并启动。然而建议只是建议。
当领导们发现App启动过慢,遭殃的就是我们这样的码农。
如果平时不对启动项进行把控,这个启动优化任务将会是灾难性的。
App开发过程中启动项的管理是非常重要的一个环节,直接影响着用户体验。那么该如何去管理启动项。

基本

网上有很多例子,但是基本思路都差不多。
pre-main阶段:
少用动态库,动态库合并,类合并,扩展合并,避免使用+load方法,删除无用代码等等。
post-main阶段:
宗旨是do less stuff
能延时操作的尽量延时,不能延时的尽量优化。

这篇文章是针对post-main的启动项管理。

前两天阅读了美团外卖的冷启动治理,受到启发。
启动项的治理部分,主要是分段启动。
使用流程是这样的

KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行
    // 启动项代码A
}
KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行
    // 启动项代码B
}

通过KLN_FUNCTIONS_EXPORT进行方法的导出,导出到Mach-O文件的一个Segment中。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 其他逻辑
    [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A];  // 在此触发所有注册到STAGE_KEY_A时间节点的启动项
    // 其他逻辑
    return YES;
}

App启动时通过KLN_FUNCTIONS_EXPORT中使用的STAGE_KEY_A去Mach-O的内存映射中查找所有A阶段的任务进行调度。

Mach-O

在讲具体的实现之前,需要先了解一下Mach-O的知识。
Mach-O简单来讲就是Clang编译器,编译出的一个产物。当App启动时,会将Mach-O加载到内存中(其实内存中是对该Mach-O文件的映射)。

Mach-O 文件主要由Header,LoadCommand,Data组成,其中Data中包含多个Segment,Segment中包含多个Section
主要由Text,Data等Section组成。
以下是通过MachOViewer查看的文件结构。

首先看一下MachHeader

MachHeader中包含了CPU架构,文件类型,MagicNumber(中文术语叫魔数)等信息。
LoadCommands是将下面的多个Sections加载到内存的命令(我是这么理解的,如果不准确请大神没指点)。

每个Section的请参考以下表:

Section 用途
TEXT.text 主程序代码
TEXT.cstring C 语言字符串
TEXT.const const 关键字修饰的常量
TEXT.stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
TEXT.stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
TEXT.objc_methname Objective-C 方法名称
TEXT.objc_methtype Objective-C 方法类型
TEXT.objc_classname Objective-C 类名称
DATA.data 初始化过的可变数据
DATA.la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指
DATA.const 没有初始化过的常量
DATA.cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
DATA.bss BSS 存放为初始化的全局变量,即常说的静态内存分配
DATA.common 没有初始化过的符号声明
DATA.objc_classlist Objective-C 类列表
DATA.objc_protolist Objective-C 原型
DATA.objc_imginfo Objective-C 镜像信息
DATA.objc_selfrefs Objective-C self 引用
DATA.objc_protorefs Objective-C 原型引用
DATA.objc_superrefs Objective-C 超类引用

也可以自定义段
通过Clang的编译属性__attribute__((used, section("__DTC," "__test" ".function")))可将一个变量编译到__DTC段的__test.function中。

我们来试验以下,拷贝以下代码在工程中的随意位置。CMD+B进行Build。
并使用MachOViewer查看Mach-O文件。

__attribute__((used, section("__DTC," "__test" ".function"))) static const int testVar = 16;

将看到下面这样的Section

选中这个段看一下MachOViewer面板的右侧

能看到有一条数据.

开始

思路:用以上的Clang的编译属性,将函数的指针编译到制定的段,在启动阶段通过某种方法从该段中读取并找到函数指针再进行调用。

编译

首先想办法把函数指针编译到Mach-O的制定的段
首先引入两个头文件

#import <dlfcn.h>
#import <mach-o/getsect.h>
typedef struct {
    char * key;
    void( * function)(void);
} DTFuntion;


static void _DTTest(void);
__attribute__((used, section("__DTC," "__Test" ".function"))) static const DTFuntion __FuncTest = (DTFuntion){"test", (void *)(&_DTTest)};
static void _DTTest (){
    printf("invoked");
}

定义一个函数,在定义一个变量,将函数的指针进行赋值。
用__attribute__修饰这个变量。这样这个变量就会编译到Mach-O文件中。
cmd+b进行build,并查看Mach-O文件。

可见已经编译到Mach-O文件了。

读取

接下来要解决的就是如何从Mach-O读取这个函数指针了
如果想读取__DTC,__Test.function 那么首先要确定当前程序的Mach-O在内存中的位置。
首先定义一个C函数
void DTExcuteFunctionsForKey(const char * key) {
}
在内部通过Dyld获取Dl_info,Dl_info的dli_fbase就是mach-o文件的开始。

void DTExcuteFunctionsForKey(const char * key) {
    Dl_info info;//info.dli_fbase就是mach-o的起始地址。
    dladdr((const void *)&DTExcuteFunctionsForKey, &info);
    //mach_header_64就是machheader
    struct mach_header_64 * machOHeader = (struct mach_header_64 *)info.dli_fbase;
    //通过machheader可以找到对应的section,getsectbynamefromheader_64是mach-o/getsect.h中的一个API,其中的key就是"__Test.function"
    struct section_64 * section = (struct section_64 * )getsectbynamefromheader_64(machOHeader, "__DTC", key);
    读取出来的section中可能包含多个变量,我们要把所有的变量都进行读取,并通过函数指针进行对函数的调用。这里遍历时需要用到偏移量,section的offet是基于mach-o的偏移量,所以mach-o的起始位置加上section的offset就是一个section的开始,每次迭代读取DTFuntion的大小,直到该段读取完成,也就是mach_header + section->offset + section->size
    size_t size = sizeof(DTFuntion);
    for (uint64_t add = mach_header + section->offset; add < mach_header + section->offset + section->size ; add += size) {
        DTFuntion func = *(DTFuntion *)add;
        func.function();
    }
}

这就是大体的思路,完成实现可以查看我的GitHub DTLauchItemManager

参考文章美团外卖iOS App冷启动治理

You Might Also Like
发表评论