App启动时间优化之二进制重排

579 阅读5分钟

App启动时间优化之二进制重排

1、启动时间的定义

  • 点击app图标到首页数据加载完毕
  • 点击app图标到launch界面完全消失的第一帧

我们这里按照第二种来去定义应用的启动时间。

由于启动动画时长为400ms,所以一般情况下app的启动最佳时间是400ms内

下面直接进入正题,其他概念方面的东西不多做赘述。

2、二进制重排

看了很多大佬的文章,抖音大佬主要讲了二进制重排的大概原理和一些实现的思路,主要采用的是静态扫描的方式去获取函数符号,文章比较粗略。戳我👉🏻

本记录主要是通过此文章进行的实践戳戳戳👉🏻这篇文章的作者包括概念性的东西都讲述的很详细。

我们不墨迹直接开始操作!

1、systemTrace工具使用

  • 1、先打开XCode的Instrument找到工具system trace

instruments.png

  • 2、选择真机->all process 全部进程,为了避免手机内应用的内存缓存,先把应用删除,并clean项目,再run systemTrace,然后再运行Xcode的app项目。等项目运行起来启动页结束出现应用的第一个页面的时候,把systemTrace停止运行。 systemTrace.png
  • 3、找到运行的项目,点下一级,再找到MainThread,选择summary:Virtual Memory,就可以看到下方的File Backed Page In就是缺页中断的次数。 启动.png

2、查看工程的符号顺序

程序在编译的时候,会有一个默认的符号顺序的列表,这个列表包含了项目中所有类的函数的逻辑地址,内存分页就是按照该内存地址进行排列。

二进制重排的最终目的其实就是改变这个列表的排序,让我们在程序启动的时候需要调用的函数的逻辑地址顺序排列起来。

首先我们先通过Xcode的配置去获取这个符号列表。

  • 1、build Setting 中搜索 Link m找到 Write Link Map File 默认为NO,此处设置为YES。 writeLinkMap.png

  • 2、Clean项目,然后再run。Success之后我们找到项目目录中如下位置。 WeChat6b82286cfbce6afd371345330660f855.png

  • 3、根据下图的目录中找到后缀为.txt的文件。我们打开它。 WeChat10e02efa08404aeddb14e60dc31c19da.png

  • 4、找到# Symbols: TEXT.png

  • 5、然后我们对照Xcode,Target中的Build Phases点开Compile Sources CompileSources.png

  • 对比4点的图和5点的图,我们发现符号列表中函数的排列顺序 就是按照CompileSources类的顺序来排列的。

好的,既然我们知道了Xcode编译产生的mapFile的原样了。我们接下来就是需要找到app在点击运行的时候到app第一个页面出来的时候调用了哪些函数,并将它重新排列在这个txt里面,使它的逻辑地址连续,从而让他们在同一个内存分页中连续。

3、Clang静态插桩

我们直接上代码,原理可以看之前提到的原文戳戳戳大佬原文👉🏻

  • 1、开始我们还是先配置XCode,这个文件放的就是我们需要重排列的符号列表,XCode会自动将文件内的内容重新排列在之前我们生成的符号列表txt文件的前方顺序排列。 lborder.png

  • 2、第一点只是声明,但是没有实体文件,我们需要手动创建一下,创建lb.order ,我们cd到项目根目录下命令好执行 touch lb.order 生成对应的lb.order文件。

  • 3、build Setting 中直接搜索 Other C Flags 找到Apple Clang - Custom Compiler Flags 添加以下代码配置

-fsanitize-coverage=func,trace-pc-guard
  • 4、添加以下代码,并在appDelegate最后调用。

.h

@interface ClangInsertStaticPile : NSObject


+ (void)startWriteToFileOfClangInsertStaticPile;

@end

.m

#import "ClangInsertStaticPile.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>

@implementation ClangInsertStaticPile

+ (void)load{
    
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint64_t N;  // Counter for the guards.
    if (start == stop || *start) return;  // Initialize only once.
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
        *x = ++N;  // Guards should start from 1.
}

+ (void)startWriteToFileOfClangInsertStaticPile{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (true) {
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        if (node == NULL) break;
        Dl_info info;
        dladdr(node->pc, &info);

        NSString * name = @(info.dli_sname);

        // 添加 _
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];

        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);

    //将结果写入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件写入出错");
    }
}

//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void * pc;
    void * next;
}SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    //if (!*guard) return;  // Duplicate the guard check.

    void *PC = __builtin_return_address(0);

    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};

    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}


@end
  • 5、成功生成lb.order之后我们去找到这个文件

divice.png 选中项目下载包

downloaddivice.png 下载下来的包右键显示包内容

tmp.png 把这个lb.order的内容拷贝到项目目录中的lb.order里面就完成了。

  • 6、验证结果

clean 一下再跑一下项目,我们再按最开始的方式去看一下 # Symbols的符号顺序,发现我们已经重排成功了!我们对比一下:

TEXT.png result.png

再用system Trace看一下对比一下:

启动.png result2.jpg

4、总结

由于我的项目工程并不大,并且该二进制重排方式仅仅只能优化本体项目的分页内容,所以其实优化效果并不明显,我们的二进制重排其实并不彻底。

通过配置工程,DYLD_PRINT_STATISTICS 可以看到pre-maind的启动时间

premainSet.png premain.png

其实优化的方式多种多样,我们也可以从动态库入手,对于私有动态库,可以才用合并动态库的方式进行优化等。对于类的初始化方法,我们可以少使用load方法,尽可能使用initializer在类使用到的时候再进行初始化等等方式。

如果进行彻底的二进制重排需要对第三方的framework也进行上述重排方式,一个一个framework进行操作会比较复杂且耗时,这次实践就没有做第三方的重排。静态插桩二进制重排仅是其中一种方式,本文是通过大佬的文章进行实操做的一个记录,也是一个学习过程的记录。