iOS之启动速度优化二进制重排和clang插装

iOS启动流程


1、点击APP图标后,内核创建APP进程

2、将APP的Mach-O可执行文件mmap进虚拟内存,加载dyld程序,接下来调用_dyld_start函数开始程序的初始化

3、重启手机/更新APP会先创建启动闭包,然后根据启动闭包进行相关的初始化

4、将动态库mmap进虚拟内存,动态库数量太多则这里耗时会增加

5、对动态库和APP的Mach-O可执行文件做bind&rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据

6、初始化 objc 的 runtime,如果有了闭包,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category

7、+load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In

8、初始化 UIApplication,启动 Main Runloop

9、执行 will/didFinishLaunch,这里主要是业务代码耗时

10、Layout,viewDidLoad 和 Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间

11、Display,drawRect 会调用

12、Prepare,图片解码发生在这一步

13、Commit,首帧渲染数据打包发给 RenderServer,启动结束。

启动速度优化思路:

1、控制APP的可执行文件大小

2、控制动态库数量

3、控制Page In 次数

4、控制首帧渲染前业务逻辑相关耗时

5、控制首帧视图渲染耗时,即上面流程中的步骤10-12

优化启动速度


1、无用资源的优化

1、无用的类

2、无用的方法

3、无用的图片资源

清理方式参见:iOS之安装包优化以及瘦身_风雨「83」的博客-CSDN博客

2、二进制重排

二进制重排为什么会加快启动速度?

当APP进程访问一页虚拟内存page,而对应的物理内存不存在时,先触发缺页中断(Page Fault)阻塞当前进程,然后加载数据到对应物理内存(Release版本还要对加载的数据进行签名),所以缺页中断还是比较耗时的。假设APP启动时调用100个函数,这100个函数如果分布在100个不同的内存页,那会产生100次缺页中断。如通过二进制重排将这100函数分布到50个或者更少的内存页中,缺页中断的次数减半,启动速度就提升了 。

获取启动阶段Page Fault的次数

打开Instruments,选择System Trace工具

重启手机(热启动情况下系统已经做了加载缓存,产生缺页中断大幅减少,所以最好重启手机),然后点击启动,待首屏出现后停止,如下图: 

解决方案

获取启动阶段调用的函数符号然后编写order_file编译顺序文件然后在Build Settings -> Order File中配置一个后缀为order的文件路径是实现二进制重排的核心思路。目前业界获取启动阶段调用的函数符号主要有三种:

1、抖音通过静态扫描和运行时 Trace 等方法确定 order_file。该方案实现难度大(需要汇编、反汇编等知识),且只能覆盖部分符号(无法覆盖纯Swift、initialize、部分block 和 C++ 通过寄存器的间接函数调用)

2、手淘通过修改 .o 目标文件实现静态插桩,需要对目标代码较为熟悉,通用性不高

3、clang编译器静态插桩,目前业界已有成熟的库和方案

静态插桩:在 build settings->"Other C Flags"中添加"-fsanitize-coverage=func,trace-pc-guard"。如过项目中有 Swift 代码,还需要在 "Other Swift Flags" 中加入"-sanitize-coverage=fun"和"-sanitize=undefined",如下:

设置完参数-fsanitize-coverage=func,trace-pc-guard 再次运行会报错。报错原因是未到找两个方法,这两个回调方法设置完参数就需要手动去添加。

首先需要引入头文件然后实现缺失的两个方法// 参考文档:SanitizerCoverage — Clang 15.0.0git documentation

#include <stdio.h>
#include <stdint.h>
#include <sanitizer/coverage_interface.h>


 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.
}

// 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 (!*guard) return;  // Duplicate the guard check.
  // 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.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

 再次编译运行,继续报错 未定义的符号 __sanitizer_symbolize_pc

 

 删除以后再次运行编译通过。再次运行。

打印函数数量

打印应用启动所需要的符号表

获取启动需要的所有符号并排重

# pragma mark clang插桩
//https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards
 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;
 
  NSLog(@"函数个数%llu",N);
     
 }

// 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);
//全局容器存放符号,原子队列,线程安全,先进后出
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct{
    void *pc;
    void *next;
}SYNode;

 void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  
//  NSLog(@"%s",__func__);
//  if (!*guard) return;  // Duplicate the guard check.
  //*PC指的上一个函数的地址
  void *PC = __builtin_return_address(0);
//  Dl_info info;
//  dladdr(PC, &info);
//     NSLog(@"-----%s",info.dli_fname);
//  NSLog(@"dli_sname-----%s",info.dli_sname);
     SYNode *node = malloc(sizeof(SYNode));
     *node = (SYNode){PC,NULL};
     //添加数据函数符号
     OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
     

//  char PcDescr[1024];
//  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
+(void)getAllStartSymbolList{
    NSMutableArray *symbolNames = [NSMutableArray array];
    while(YES){
        SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, 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];
        [symbolNames addObject:symbolName];
        
//        if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {
//            [symbolNames addObject:name];
//            continue;
//        }
//        [symbolNames addObject:[@"_"stringByAppendingFormat:name]];
//        NSLog(@"dli_sname-----%s",info.dli_sname);

    }
    //对数据进行取反操作
//    symbolNames = (NSMutableArray *)[[symbolNames reverseObjectEnumerator] allObjects];
    //正向遍历
    //    NSEnumerator *en = [symbolNames objectEnumerator];
    NSEnumerator *en = [symbolNames reverseObjectEnumerator];
    NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString *name;
    while (name =[en nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    NSLog(@"%lu",(unsigned long)symbolNames.count);
    NSLog(@"funcs %lu",(unsigned long)funcs.count);
    //生成order文件
    NSString *funcStrs = [funcs componentsJoinedByString:@"\n"];
    //写入文件
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"cars.order"];
    NSData *file = [funcStrs dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
    
    NSLog(@"%@",NSHomeDirectory());
}

将导出的文件拖到工程主目录下并在build Setting中设置order file文件,

 设置好orderfile之后再次运行程序查看优化前和优化后的时间差

启动时间缩短了0.45秒。

启动link map 顺序查看

build setting 搜索link map 找到Write link map file 设置为YES 然后编译,编译完成后找Product->show build in folder in folder->Intermediates.noindex->Debug-iphoneos找到xx-LinkMap-normal-arm64.txt

 编译顺序根据

设置cars.order编译后link map

设置完成cars.order后再次编译查看link map 

 验证发现link map编译顺序和order文件中的一致,由此可见编译顺序优先安装order编译并启动加载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风雨「83」

你的鼓励将是我创作最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值