iOS启动时间优化-搞定二进制重排

什么是缺页中端

前面iOS启动时间优化-二进制重排的前世的文章提到当进程去映射表获取数据不存在时,系统会阻塞当前进程,完成数据获取写入物理内存并且和映射表进行映射,这个过程称为缺页中断 ,即[Page fault]。(baike.baidu.com/item/%E7%BC...%25EF%25BC%258C "https://baike.baidu.com/item/缺页中断/5029040?fr=ge_ala)%EF%BC%8C")

当发生缺页中断 ,在iOS系统中,对于生产环境的应用,当产生缺页中断进行重新加载时,iOS系统还会对其做一次签名验证 ,因此iOS生产环境的应用page fault所产生的耗时要更多

什么是二进制重排

抖音团队分享的一个 Page Fault,开销在 0.6 ~ 0.8ms,我们知道发生缺页中断系统会阻塞进程和签名验证,站在启动时间优化的角度上是可以通过减少缺页中断避免在首屏展示之前消耗过多时间

假设在启动时期我们需要调用两个函数 method1 与 method4 . 函数编译在 mach-o 中的位置是根据 ld ( Xcode 的链接器) 的编译顺序并非调用顺序来的 . 因此很可能这两个函数分布在不同的内存页上 .

那么启动时 , page1与page2 则都需要从无到有加载到物理内存中 , 从而触发两次 page fault .

如果将method1method4 放到一个内存页 中 , 那么启动时则只需要加载 page1 即可 , 也就是只触发一次page fault, 达到优化目的 .而这个做法就叫做二进制重排

二进制重排具体如何进行

Xcode用的链接器叫做ld,ld有一个参数叫order File,在启动时也是根据order file定义的文件顺序去加载对应的指令到物理内存.我们需要做的就是把启动相关的函数符号存放在这个order file里面。

配置order file

Xcode提供ld需要的order file的路径配置,如下:

  • 在这个 order文件中,将你需要的符号按顺序写在里面
  • 当工程build的时候,Xcode会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O

Tips:

1️⃣ : order文件里符号写错了或者这个符号不存在会不会有问题 ?

  • 答:ld 会忽略这些符号 , 实际上如果提供了 link 选项 -order_file_statistics,会以 warning 的形式把这些没找到的符号打印在日志里。 .

2️⃣ : 有部分同学可能会考虑这种方式会不会影响上架 ?

  • 答:不影响,二进制重排只是重新排列了所生成的 macho 中函数表与符号表的顺序 .

知道配置ld依赖order file路径,我们只需要把启动相关的函数符号写入order file即可,那么启动相关的函数又怎么获取呢,别慌,继续往下看😄

获取APP启动相关的函数符号

业界方案:

  • hook objc_MsgSend(只能拿到 oc 以及 swift @objc dynamic 后的方法,并且由于可变参数个数,需要用汇编来获取参数)
  • 静态扫描mach-o特定段和节里面所存储的符号以及函数数据(静态扫描 , 主要用来获取 load 方法,c++ 构造)
  • clang插桩(完美版本,完全拿到 swift、 oc 、 c 、 block 全部函数 )

本文主要介绍clang插桩方式在编译时期获取所有启动时需要函数符号

clang插桩原理

插桩实际上是在编译期就在每一个函数内部二进制源数据添加 hook 代码 ( 我们添加的 __sanitizer_cov_trace_pc_guard 函数 ) 来实现全局的方法hook 的效果.

由于本篇注重实践,这里的插桩原理后续会单独拿一篇文章详细介绍原理

demo编译设置1

直接搜索 Other C Flags 来到 Apple Clang - Custom Compiler Flags 中 , 添加

shell 复制代码
-fsanitize-coverage=trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard #只扫描函数

demo编译配置2(swift或者混编项目) 可选

直接搜索 Other Swift Flags

js 复制代码
-sanitize-coverage=func
-sanitize=undefined

获取启动相关的函数符号,并且直接写入.order文件

swift 复制代码
@interface ViewController ()

@property (nonatomic, strong) Login *login;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.login = [[Login alloc] init];
    [self.login login];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(writefile)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];
}

- (void)writefile {
    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_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 __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));
}

因为这里是demo,我是直接在ViewController测试的,进入后台就会把启动相关函数写入设置tmp目录下文件里

运行项目拿到app启动相关函数符号

运行项目进入后台,xcode-> windows -> device and simulators 里下载对应的.order文件,例如我的demo如下: 确实和启动调用的方法一致

设置ld链接器依赖的order file

准备工作

如何测量我们对order file的修改生效了

这里介绍下link map ,它是编译期间的产物,记录符号加载的顺序,默认情况下和 xcode-> build phases -> compile source 的类的顺序一致

配置输出link map步骤

设置link map输出路径和允许输出link map

新建一个demo,这里以我的为例 运行项目后拿到设置路径下link map文件(下方是我截取了关键信息):

js 复制代码
# Symbols:
# Address	Size    	File  Name
0x100004AE0	0x00000028	[  1] -[Login login]
0x100004B08	0x00000024	[  1] _sancov.module_ctor_trace_pc_guard
0x100004B2C	0x0000008C	[  2] -[ViewController viewDidLoad]
0x100004BB8	0x0000021C	[  2] -[ViewController writefile]
0x100004DD4	0x0000006C	[  2] ___sanitizer_cov_trace_pc_guard_init
0x100004E40	0x0000004C	[  2] ___sanitizer_cov_trace_pc_guard
0x100004E8C	0x00000024	[  2] _sancov.module_ctor_trace_pc_guard
0x100004EB0	0x00000028	[  3] -[Register registerUser]
0x100004ED8	0x00000024	[  3] _sancov.module_ctor_trace_pc_guard
0x100004EFC	0x0000023C	[  4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100005138	0x00000090	[  4] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x1000051C8	0x0000001C	[  4] -[AppDelegate application:didDiscardSceneSessions:]
0x1000051E4	0x00000034	[  4] -[AppDelegate objc]
0x100005218	0x0000003C	[  4] -[AppDelegate setObjc:]
0x100005254	0x00000034	[  4] -[AppDelegate objc1]
0x100005288	0x0000003C	[  4] -[AppDelegate setObjc1:]
0x1000052C4	0x00000034	[  4] -[AppDelegate login]
0x1000052F8	0x0000003C	[  4] -[AppDelegate setLogin:]
0x100005334	0x00000034	[  4] -[AppDelegate registerOBj]
0x100005368	0x0000003C	[  4] -[AppDelegate setRegisterOBj:]
0x1000053A4	0x00000074	[  4] -[AppDelegate .cxx_destruct]
0x100005418	0x00000024	[  4] _sancov.module_ctor_trace_pc_guard
0x10000543C	0x00000104	[  5] _main
0x100005540	0x00000024	[  5] _sancov.module_ctor_trace_pc_guard
0x100005564	0x00000028	[  6] -[Util Utiltool1]
0x10000558C	0x00000028	[  6] -[Util Utiltool2]
0x1000055B4	0x00000028	[  6] -[Util Utiltool3]
0x1000055DC	0x00000028	[  6] -[Util Utiltool4]
0x100005604	0x00000028	[  6] -[Util Utiltool5]
0x10000562C	0x00000028	[  6] -[Util Utiltool6]
0x100005654	0x00000028	[  6] -[Util Utiltool7]
0x10000567C	0x00000028	[  6] -[Util Utiltool8]
0x1000056A4	0x00000028	[  6] -[Util Utiltool9]
0x1000056CC	0x00000028	[  6] -[Util Utiltool10]
0x1000056F4	0x00000024	[  6] _sancov.module_ctor_trace_pc_guard
0x100005718	0x0000001C	[  7] -[SceneDelegate scene:willConnectToSession:options:]
0x100005734	0x0000001C	[  7] -[SceneDelegate sceneDidDisconnect:]
0x100005750	0x0000001C	[  7] -[SceneDelegate sceneDidBecomeActive:]
0x10000576C	0x0000001C	[  7] -[SceneDelegate sceneWillResignActive:]
0x100005788	0x0000001C	[  7] -[SceneDelegate sceneWillEnterForeground:]
0x1000057A4	0x0000001C	[  7] -[SceneDelegate sceneDidEnterBackground:]
0x1000057C0	0x00000034	[  7] -[SceneDelegate window]
0x1000057F4	0x0000003C	[  7] -[SceneDelegate setWindow:]
0x100005830	0x00000038	[  7] -[SceneDelegate .cxx_destruct]
0x100005868	0x00000024	[  7] _sancov.module_ctor_trace_pc_guard
0x10000588C	0x00000028	[  8] -[Tool Tooltool1]
0x1000058B4	0x00000028	[  8] -[Tool Tooltool2]
0x1000058DC	0x00000028	[  8] -[Tool Tooltool3]
0x100005904	0x00000028	[  8] -[Tool Tooltool4]
0x10000592C	0x00000028	[  8] -[Tool Tooltool5]
0x100005954	0x00000028	[  8] -[Tool Tooltool6]
0x10000597C	0x00000028	[  8] -[Tool Tooltool7]
0x1000059A4	0x00000028	[  8] -[Tool Tooltool8]
0x1000059CC	0x00000028	[  8] -[Tool Tooltool9]
0x1000059F4	0x00000028	[  8] -[Tool Tooltool10]

这时加载顺序默认和xcode-> build phases -> compile source 的类的顺序一致

OK,到这里操作order file之前的样本我们准备完毕了,继续往下我看

设置demo的.order文件

拿前面我们的到的APP启动时依赖函数符号.order设置为ld链接器

可以看到link map生成符号信息也发生了改变,说明我们修改.order file生效了,确实按我们设置的函数符号顺序进行加载的

前面我们确定完成了ld优先设置启动相关的函数符号,同时通过link map确定生效了,但是对启动时间到底有多少提升呢,我们继续往下看

衡量二进制重排成果

xcode提供了System Trace工具提供发生page fault事件的次数和消耗时间

测试方案

  • 启动方式:接近冷启动,使用前多使用其他app,尽可能对app缓存进行覆盖清洗
  • 设备选择:最好低性能设备,这样每次测试前打开几个应用就可以清除测试app的内存
  • 测试前:需要确认link map的符号顺序确保是优化前后对应的,避免误测

优化前 page fault: 514 150.57ms

优化后 page fault: 170 55.89ms

相比优化前减少: 94.68s

相关信息

  • demo
  • 获取启动相关函数的pod库

感谢您的阅读,本篇文章已阅读完毕,欢迎浏览我的其他文章!

相关推荐
/**书香门第*/3 小时前
Laya ios接入goole广告,搭建环境 1
ios
wakangda9 小时前
React Native 集成 iOS 原生功能
react native·ios·cocoa
crasowas1 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store
ii_best1 天前
ios按键精灵脚本开发:ios悬浮窗命令
ios
Code&Ocean2 天前
iOS从Matter的设备认证证书中获取VID和PID
ios·matter·chip
/**书香门第*/2 天前
Laya ios接入goole广告,开始接入 2
ios
恋猫de小郭2 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨2 天前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题2 天前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
二流小码农3 天前
鸿蒙开发:简单了解属性动画
android·ios·harmonyos