重学OC第二十四篇:启动优化

前言

启动分为冷启动和热启动,主要区别是内存是否有APP加载的数据,如果所有的数据需从硬盘读取后加载到内存,那就为冷启动。下面主要是关于冷启动方面的优化。

一、冷启动

1.1 性能检测

APP启动分两个阶段来测试:

  • main函数前(pre-main)
    主要是dyld流程部分,包括动态库加载、类的加载、C++静态对象处理等。通过在Xcode中添加环境变量DYLD_PRINT_STATISTICS为YES
    在这里插入图片描述
//模拟器数据不真实,仅参考
Total pre-main time: 126687488.8 seconds (0.0%)   //总耗时
	dylib loading time: 258.93 milliseconds (0.0%) //dyld加载耗时,官方建议自定义动态库不超过6个
	rebase/binding time: 126687487.6 seconds (0.0%)//重定址和符号绑定耗时
   	ObjC setup time: 632.00 milliseconds (0.0%)//OC类注册耗时
   	initializer time: 366.95 milliseconds (0.0%)//+load、c++构造初始化耗时
   	slowest intializers :  //最慢的初始化器
  • main函数后
    从main()函数开始至applicationWillFinishLaunching结束,我们统一称为main()函数之后的部分。利用锚点分析applicationWillFinishLaunching的耗时。

1.2 优化

官方建议的启动时间要求:

  • 应该在400ms内完成main()函数之前的加载
  • 整体过程耗时不能超过20秒,否则系统会kill掉进程,App启动失败

对于main函数前的优化

  • 减少OC类,移除不需要的类
  • 减少动态库,移除不需要的动态库。可通过动态库合并来让动态库数量不超过6个
  • 尽量不要写__attribute__((constructor))的C函数,也尽量不要用到C++的静态对象
  • 尽量不要使用+load
  • 压缩图片大小,减少I/O操作量

对于main函数后的优化

  • 延迟不必要的页面、配置等加载
  • 耗时操作考虑多线程异步操作
  • 使用二进制重排来减少启动时硬盘到内存的操作次数。

二、二进制重排

二进制重排指的是重排编译阶段的代码文件和函数的顺序。要了解二进制重排需了解虚拟内存,相关知识可参考《深入理解计算机系统》第9章虚拟内存中的内容。

2.1 原理

编译器生成二进制代码时,默认按照链接的.o顺序写文件,按照.o内部顺序写函数,如果函数跨页了,就会触发多次Page Fault,所以把函数排到一个Page里,只需要一次Page Fault。
iOS中页的大小可通过下面代码打印

extern vm_size_t vm_page_size;
NSLog(@"%lx", vm_page_size);

在真机中每页为16KB,模拟器中为4KB。

2.1.1 PageFault检测

通过Instruments中的Systme Trace,选择项目,选中main thread,选择Virtual Memory,查看File Backed Page In(PageFault)和其他的相关次数、时间等。

2.2 重排

要进行重排要看到二进制数据的顺序,如何进行重排。

2.2.1 二进制符号顺序查看

Build Settings中搜索link Map,设置Write Link Map File为YES,然后编译,在文件中向上两级找到Intermediates.noindex,进入xxx.build最后找到xxx-LinkMap-normal-x86_64.txt文件,打开找到Symbol可看到按编译时文件和文件内部函数书写顺序排列。

2.2.2 通过.order文件重排

创建xxx.order文件,在Build Settings中搜索order file,加入./xxx.order,然后编译。
那xxx.order文件中的内容怎么来,可通过clang插桩hook函数然后保存到xxx.order中。

2.2.3 Clang插桩

官方介绍: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs

SanitizerCoverage 是 Clang 内置的一个代码覆盖工具,它把一系列以 _sanitizer_cov_trace_pc 为前缀的函数调用插入到用户定义的函数里来实现全局 AOP。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。

使用步骤:

  1. Build Settings 搜索 Other C Flags,添加-fsanitize-coverage=func, trace-pc-guard参数(OC); Other Swift Flags添加-sanitize-coverage=func 和 -sanitize=undefined(swift)
  2. 在__sanitizer_cov_trace_pc_guard_init和__sanitizer_cov_trace_pc_guard_init方法中进行相关代码编写或直接使用第三方AppOrderFiles
  3. 在didFinishLaunchingWithOptions中调用相应的方法生成xxx.order文件并进行调用顺序重排
  4. 把重排后的xxx.order文件拷贝到项目的根目录下,在Build Settings中搜索order file并加入./xxx.order或者${SRCROOT}/xxx.order

AppOrderFiles源码

#import "AppOrderFiles.h"
#import <dlfcn.h>  //提供了加载和处理动态连接库的系统调用
#import <libkern/OSAtomicQueue.h>
#import <pthread.h>

static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;

static BOOL collectFinished = NO;

typedef struct {
    void *pc;
    void *next;
} PCNode;

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint32_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.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (collectFinished || !*guard) {  
        return;
    }
    // If you set *guard to 0 this code will not be called again for this edge.
    // Now you can get the PC and do whatever you want:
    //   store it somewhere or symbolize it and print right away.
    // The values of `*guard` are as you set them in
    // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
    // and use them to dereference an array or a bit vector.
    *guard = 0;
    void *PC = __builtin_return_address(0);
    PCNode *node = malloc(sizeof(PCNode));
    *node = (PCNode){PC, NULL};
    OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}

extern void AppOrderFiles(void(^completion)(NSString *orderFilePath)) {
    collectFinished = YES;
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSMutableArray <NSString *> *functions = [NSMutableArray array];
        while (YES) {
            PCNode *node = OSAtomicDequeue(&queue, offsetof(PCNode, next));  //出列
            if (node == NULL) {
                break;
            }
            Dl_info info = {0};
            dladdr(node->pc, &info);  //把信息读入info中
            if (info.dli_sname) {
                NSString *name = @(info.dli_sname);  
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];   //OC方法
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name]; //非OC方法加_前缀
                [functions addObject:symbolName];
            }
        }
        if (functions.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        NSMutableArray<NSString *> *calls = [NSMutableArray arrayWithCapacity:functions.count];
        NSEnumerator *enumerator = [functions reverseObjectEnumerator];  //取反
        NSString *obj;
        while (obj = [enumerator nextObject]) {
            if (![calls containsObject:obj]) { //去重
                [calls addObject:obj];
            }
        }
        [calls removeObject:functionExclude];  //移除当前方法
        NSString *result = [calls componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", result);
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"app.order"];
        NSData *fileContents = [result dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] 	
        				createFileAtPath:filePath 
        				contents:fileContents
                       	attributes:nil];   //写入文件
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}

总结

  • 对冷启动性能的检测,在Xcode中添加环境变量DYLD_PRINT_STATISTICS为YES来打印pre-main阶段的耗时
  • 建议优化应该在400ms内完成main()函数之前的加载,整体过程耗时不能超过20秒
  • 通过Instruments中的Systme Trace查看PageFault
  • Build Settings中搜索link Map,设置Write Link Map File为YES,然后编译,在文件中向上两级找到Intermediates.noindex,进入xxx.build最后找到xxx-LinkMap-normal-x86_64.txt文件查看二进制符号
  • 通过Clang插桩最后生成.order文件,把重排后的xxx.order文件拷贝到项目的根目录下,在Build Settings中搜索order file并加入${SRCROOT}/xxx.order。

参考文章:
App 二进制文件重排已经被玩坏了
Improving Locality of Reference

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页