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

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

一、启动时间的定义

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

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

因为启动动画时长为400ms,因此通常状况下app的启动最佳时间是400ms内
复制代码

下面直接进入正题,其余概念方面的东西很少作赘述。缓存

二、二进制重排

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

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

咱们不墨迹直接开始操做!函数

一、systemTrace工具使用

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

instruments.png

  • 二、选择真机->all process 所有进程,为了不手机内应用的内存缓存,先把应用删除,并clean项目,再run systemTrace,而后再运行Xcode的app项目。等项目运行起来启动页结束出现应用的第一个页面的时候,把systemTrace中止运行。

systemTrace.png

  • 三、找到运行的项目,点下一级,再找到MainThread,选择summary:Virtual Memory,就能够看到下方的File Backed Page In就是缺页中断的次数。

启动.png

二、查看工程的符号顺序

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

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

首先咱们先经过Xcode的配置去获取这个符号列表。学习

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

writeLinkMap.png

  • 二、Clean项目,而后再run。Success以后咱们找到项目目录中以下位置。

WeChat6b82286cfbce6afd371345330660f855.png

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

WeChat10e02efa08404aeddb14e60dc31c19da.png

  • 四、找到# Symbols:

TEXT.png

  • 五、而后咱们对照Xcode,Target中的Build Phases点开Compile Sources

CompileSources.png

  • 对比4点的图和5点的图,咱们发现符号列表中函数的排列顺序

就是按照CompileSources类的顺序来排列的。优化

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

三、Clang静态插桩

咱们直接上代码,原理能够看以前提到的原文戳戳戳大佬原文👉🏻

  • 一、开始咱们仍是先配置XCode,这个文件放的就是咱们须要重排列的符号列表,XCode会自动将文件内的内容从新排列在以前咱们生成的符号列表txt文件的前方顺序排列。

lborder.png

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

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

-fsanitize-coverage=func,trace-pc-guard
复制代码
  • 四、添加如下代码,并在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
复制代码
  • 五、成功生成lb.order以后咱们去找到这个文件

divice.png 选中项目下载包

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

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

  • 六、验证结果

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

TEXT.png result.png

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

启动.png result2.jpg

四、总结

因为个人项目工程并不大,而且该二进制重排方式仅仅只能优化本体项目的分页内容,因此其实优化效果并不明显,咱们的二进制重排其实并不完全。

经过配置工程,DYLD_PRINT_STATISTICS 能够看到pre-maind的启动时间

premainSet.png premain.png

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

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

相关文章
相关标签/搜索