点击围观!腾讯 TAPD 助力金融行业研发提效、敏捷转型最佳实践! 了解详情
写点什么

手淘架构组最新实践:iOS 基于静态库插桩的二进制重排启动优化

  • 2020-01-06
  • 本文字数:2499 字

    阅读完需:约 8 分钟

手淘架构组最新实践:iOS基于静态库插桩的二进制重排启动优化

背景

近期抖音和 Facebook 分享了自己通过二进制重排优化启动时间的方案,手淘 iOS 架构团队也对二进制重排进行了研究,由于手淘工程模块已经二进制化,因此实现了一套基于静态库插桩的重排方案。

APP 启动 和 PageFault

当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段内存时才会分配。这种延迟分配物理内存的方式就通过 page fault 机制来实现的。当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配, cpu 都会生成一个 page fault ,进而执行操作系统的 page fault handler 。如果是因为还未分配物理内存,操作系统会立即分配物理内存给当前进程,然后重试产生这个 page fault 的内存访问指令。



App 在启动时,需要执行各种函数,我们需要读取 TEXT 段代码到物理内存中,这个过程会发生缺⻚中断,由于启动时所需要执行的代码分布在 TEXT 段的各个部分,会读取很多⻚面,导致启动时 Page Fault 数量非常多。与直接访问物理内存不同, page fault 过程大部分是由软件完成的,消耗时间比较久,所以是影响启动性能的一个关键指标。


例如下图中,手淘启动时首先的调用的几个方法 会分布在虚拟内存的各个⻚面中, 执行这些方法时,需要从读取到物理内容中,就会产生多次 page fault 。


如果能将启动阶段需要的读取代码集中排布,将这些方法全都放到相邻的区域中,我们读取这些方法可能就只需要极少的 page fault 次数。可以减少不必要的 page fault 时间。达到优化启动时间的效果。


重排前后的函数在页面的布局对比:


重排方案

如何获取方法的执行顺序

为了生成 order_file , 我们需要确定应用启动时方法的执行顺序。之前抖音和 facebook 都分享过自己的方案,在实际操作的过程中,我们发现抖音和 facebook 的方案并不适用于手淘。


抖音通过静态扫描和运行时 Trace 等方法确定 order_file,该方案无法覆盖 initialize、block 和 C++ 通过寄存器的间接函数调用静态扫描不出来调用。


facebook 分享过通过 llvm 插桩的确定 order_file 的方案,需要使用源码重新打包。由于手淘几乎全是已经编译好的二进制模块,在手淘使用该方案不现实。


只能想其他办法…


手淘之前已经做过 pod 预编译,我和师兄念纪想到了是否可以通过在汇编层面对 pod 编译后的静态库进行插桩。在启动时,插桩后的方法都会调用记录方法,从而获得启动方法的执行顺序。在参考了离青对汇编插桩的研究后,确定了静态库插桩的实现方案。

静态库插桩

我们编译过的静态库由 .o 文件组成,我们可以对 .o 中的函数代码进行修改,在每个函数的开头插入调用我们指定记录函数的指令。


举个例子:


插入前 -[MyApp window]: 的汇编代码


-[MyApp window]:0000000000002d88 adrp x8, #0x0000000000002d8c ldrsw x8, [x8, #0xf18]; 0x2f18@PAGEOFF, _OBJC_IVAR_$_MyApp._window0000000000002d90 ldr x0, [x0, x8]0000000000002d94 ret
复制代码


插入后的 汇编代码,可以看到 增加了跳转到 _record_method 的指令,并且补上了 prologue 和 epilogue 。


-[MyApp window]:0000000000002ebc stp x29, x30, [sp, #-0x10]!0000000000002ec0 mov x29, sp0000000000002ec4 bl _record_method0000000000002ec8 ldp x29, x30, [sp], #0x0000000000002ecc adrp x8, #0x0000000000002ed0 ldrsw x8, [x8, #0xc0]0000000000002ed4 ldr x0, [x0, x8]0000000000002ed8 ret
复制代码

生成 order file

linkmap 记录了连接过程中的相关信息。其中包含链接用到的 symbol 相关的信息。通过 pc address 减去 slide 得到的地址,我们可以在 linkmap 中找到对应的 symbol .


address = pc - slide. // 因为ASLR, APP 可执行文件随机载入的原因,需要处理一下偏移量。
复制代码


我们需要将之前记录的地址转换成对应的符号,为了真实还原线上的执行环境,我们只是在 app 中简单地的记录了 pc 地址 和 Image 的偏移量。通过解析 linkmap ,获取函数的地址区间, 得到距离 address 最近的 symbol ,生成 order_file 。


linkmap 文件:


# Symbols:# Address Size File Name0x100001630 0x00000039 [ 2] -[ViewController viewDidLoad]0x100001670 0x00000092 [ 3] _main0x100001710 0x00000080 [ 4] -[AppDelegate application:didFinishLaunchingWithOptions:]0x100001790 0x00000040 [ 4] -[AppDelegate applicationWillResignActive:]0x1000017D0 0x00000040 [ 4] -[AppDelegate applicationDidEnterBackground:]0x100001810 0x00000040 [ 4] -[AppDelegate applicationWillEnterForeground:]0x100001850 0x00000040 [ 4] -[AppDelegate applicationDidBecomeActive:]0x100001890 0x00000040 [ 4] -[AppDelegate applicationWillTerminate:]
复制代码

更改符号的排列顺序

默认情况下, ld 链接器会按照链接的顺序将各个 .o 文件的数据重新布局生成可执行文件。ld 链接器提供 -order-file 选项操控数据排列的顺序。在 Xcode 中可以通过 Order File 选项指定符号排序文件。


//Order file 内容例子:+[xxxxx1 load]+[xxxxx2 swizzleResumeAndSuspendMethodForClass:]+[xxxxx3 load]+[xxxxx4 initialize]___+[xxxxx5 initialize]_block_invoke+[xxxxx6 initialize]___+[xxxxx7 initialize]_block_invoke...
复制代码

优化效果

通过精准的启动函数重排,最后重排效果还是很可观的,在 iPhone6 上优化了 400ms 的启动时间。

参考

感谢抖音团队和 Facebook 团队提供优化新思路


抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

Improving iOS Startup Performance with Binary Layout Optimizations

https://atscaleconference.com/videos/performance-scale-improving-ios-startup-performance-with-binary-layout-optimizations/

Linux 下 Page Fault 的处理流程 https://cloud.tencent.com/developer/article/1459526


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650405083&idx=1&sn=f52e8dcf03faafcf910d58add9904550&chksm=839530c3b4e2b9d5cab0e84da1a0d6c210ed7fb2fc4a66d01213fdfb2fcffbbe332c87b8eb76&scene=27#wechat_redirect


2020-01-06 10:002452

评论

发布
暂无评论
发现更多内容

架构师训练营 - 第 7 周命题作业

红了哟

阿里、力扣、政采云的15位专家分享前端面试与招聘视角

三钻

面试 大前端

解析 HashMap 源码之基本操作 put

shengjk1

Java hashmap

LeetCode题解:88. 合并两个有序数组,双指针遍历+从前往后,JavaScript,详细注释

Lee Chen

大前端 LeetCode

【DevOps】我们忽视了Daily Build(每日构建)吗?

Man

DevOps jenkins 每日构建

让你起飞的20个Linux命令骚操作

我是程序员小贱

Github被攻击。我的GitPage博客也挂了,紧急修复之路,也教会你搭建 Jekyll 博客!

小傅哥

Java GitHub 小傅哥 博客

1 时间复杂度总结

我是程序员小贱

解析 hashMap 源码之基本操作 get

shengjk1

Java hashmap

docker入个门

书旅

Docker 容器 Dockerfile

MEDO 项目开发中遇到的问题汇总

陈皮

troubleshoot之:使用JFR分析性能问题

程序那些事

Java 性能分析 jfr

如何学习一个框架?

云帆

你生日那天的宇宙什么样子知道?我全部给你吧!

我是程序员小贱

学习技术先从学会使用搜索引擎开始

我是程序员小贱

平均负载是什么?

我是程序员小贱

敏捷到底是个什么鬼?

刘华Kenneth

程序员 敏捷 change

如何隐藏你的数据库密码

Rayjun

安全 服务器

真正的异步API网关Agate

dinstone

Async API Gateway

Elasticsearch学习

张明森

Apache Mina和Netty的历史

dinstone

1 学习性能优化的要点

我是程序员小贱

MySQL 基准测试

多选参数

MySQL

为什么考研,考研能给你带来什么?说说我的感受!

我是程序员小贱

翻译: Effective Go (7)

申屠鹏会

翻译 Go 语言

Spring如何选择类构造器

申屠鹏会

翻译 Go 语言

翻译: Effective Go (6)

申屠鹏会

翻译 Go 语言

解析 HashMap 源码概括

shengjk1

Java hashmap

源码分析 | Mybatis接口没有实现类为什么可以执行增删改查

小傅哥

Java 源码分析 小傅哥 mybatis

并不想吹牛皮,但!为了把Github博客粉丝转移到公众号,我干了!

小傅哥

Java 小傅哥 博客 微信公众号

鲲鹏一粤,智算万里

脑极体

手淘架构组最新实践:iOS基于静态库插桩的二进制重排启动优化_架构_谢俊逸(极目)_InfoQ精选文章