腾讯亿级用户规模自研业务的上云实践解读,立即报名 了解详情
写点什么

一行代码解决!iOS 二进制重排启动优化

  • 2019-10-31
  • 本文字数:4014 字

    阅读完需:约 13 分钟

一行代码解决!iOS二进制重排启动优化

随着产品给的的需求越来越多,堆叠的功能也越来越复杂,整个 App 应用大小也越来越大,而越来越多的功能也导致了越来越多的体验和性能问题;而其中这最能直观影响用户的就是启动速度。

传统的启动优化是基于减少不必要代码,懒加载,划分任务优先级,利用多线程来做的,此类相关优化的策略已经很普遍了,主要是从减少主线程任务的角度来出发,很难再做出大的提升。今天,我们从另一个角度去思考启动优化–内存加载机制。本方案在我们应用上可以将启动速度平均优化 10%左右,且无需改动一行代码。


关于启动

当用户打开你的 App 时,到底发生了什么?



你可能会说,开始加载二进制,dyld 初始化,objc 初始化,执行 load 函数执行 c++构造函数,最后进入 main 函数,然后执行 App 初始化逻辑,最终首页出现,启动完成。但在这之前呢?在这之后呢?在这所有的过程中都需要干什么呢?


答案是:需要执行代码,需要 pc 寄存器不停的跳转,完成函数的调用和上下文切换,从而实现具体的逻辑。


当 dyld 初始化 App 时,会把程序的二进制 mmap 到内存里,当需要使用具体内存时,再去触发物理内存加载(懒加载),然后访问。而当程序不停的执行和初始化时,就会涉及到 pc 寄存器不停的跳来跳去,寻址,取指令,译码,执行。这其中最为耗时的就是取指令,因为取指需要不断的涉及到内存的缺页访问,即 page fault。page fault 一般流程如下:



page fault:当待访问的 VA 虚拟地址不存在对应的物理内存地址时,MMU 将会触发 page fault 中断来加载对应的物理页,建立起虚拟内存和物理内存的映射关系。page fault 一般耗时有多少呢?如下图:



在较差的情况下,page fault 居然耗时可以轻松达到 1ms;而即便在较正常的情况下,一次 page fault 大概也要耗时 0.3~0.6ms 左右;那么 App 启动期间到底大概需要发生多少次 page fault 呢?比如在我们应用中的数据如下:(XCode 中 File Backed Page in 就是 page fault)



一次正常的 cold launch,需要触发至少 2000 多次 page fault,总 page fault 耗时居然达到了 300 多 ms 如果我们能将这 300 多 ms 尽量优化,那就会是一次非常好的启动优化了。


备注:本文所有测试数据基于 XCode 11beta5, iOS12.1 , iphone6s


二进制重排

讲完了背景和介绍,我们接下来看看到底怎么解决 page fault 过多的问题。


1. page fault 的危害

前面我们说了频繁发生 page fault 的问题在于我们的二进制需要不停的执行指令,如果当需要执行的代码文件偏移过于随机时,则会导致 pc 寄存器不停发生切换,从而不停的触发页内存加载。那么,当应用的 page fault 频率过高时会有什么问题:


  • 增加指令执行的耗时(取指慢)

  • 增大 disk thrashing 的风险


以上二者会导致我们指令的执行时间慢,从而导致主线程不停被阻塞而最终影响启动速度。另外在 iOS 上 A7,A8-based 处理器的物理页大小为 4kb,而 A9 之后的处理器物理页大小为 16kb。如果我们能利用好物理页的限制,让我们所有的待执行的关键指令和代码都紧凑的排列在相邻的物理页内,那么我们就能尽可能的减少 page fault 的次数,也能极大降低 disk thrashing 的概率。因此二进制重排的概念就出来了。


disk thrashing : thrashing occurs when a computer’s virtual memory resources are overused, leading to a constant state of paging and page faults, inhibiting most application-level processing.[1] This causes the performance of the computer to degrade or collapse.


2. 重排的目的

重排的本质就是为了解决上面 2 个问题,频繁 page fault 和 disk thrashing。原理就是将所有启动期间先后执行的函数代码,紧凑的排列在顺序的二进制中,使得 pc 寄存器的指令跳转幅度大幅降低。让单个物理页能尽可能的加载更多的当前或下一条待执行的函数。


3. 怎么重排

要使得函数符号按特定顺序排列在二进制中,XCode 早就提供了支持,具体的苹果也一直身体力行,比如 objc 的源码就采用了二进制重排优化,如下图:



我们只要在编译设置里指定一个 order file 即可;而 order file 的内容如下:



将所有符号按顺序排列,以换行符分隔,编译器就会按照 order file 指定的符号顺序来排列二进制代码段。由此就能达到重排优化了。


4. 怎么做

由上,我们已经较为清晰的知道了二进制重排的意义了,那么我们需要怎么做呢?针对应用中的 objc,c,c++代码和符号我们要怎么知道他们的执行顺序并监控呢?即只要我们能通过某种手段 trace 到所有启动阶段执行的函数符号,然后把这些函数符号按顺序排列好,组成 order file 交给编译器即可。实现如下:


  • objc 方法


对于 objc 的方法,我们只要 hook 掉所有的 objcmsgSend,以及 objcmsgSendSuper2 来建立监控即可。代码大概如下:


.text .align 2 .global _pgoobjcmsgSend _pgoobjcmsgSend:
// push stp q6, q7, [sp, #-32]! stp q4, q5, [sp, #-32]! stp q2, q3, [sp, #-32]! stp q0, q1, [sp, #-32]! stp x8, lr, [sp, #-16]! stp x6, x7, [sp, #-16]! stp x4, x5, [sp, #-16]! stp x2, x3, [sp, #-16]! stp x0, x1, [sp, #-16]!
//call stub函数监控 bl pgoastub_msgSend mov x9, x0
// pop ldp x0, x1, [sp], #16 ldp x2, x3, [sp], #16 ldp x4, x5, [sp], #16 ldp x6, x7, [sp], #16 ldp x8, lr, [sp], #16 ldp q0, q1, [sp], #32 ldp q2, q3, [sp], #32 ldp q4, q5, [sp], #32 ldp q6, q7, [sp], #32
// Call original objc_msgSend. br x9 ret
复制代码


  • block 方法


同理也是 hook block 来做到的,block 的内存结构如下:struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; BlockInvokeFunction invoke; struct Block_descriptor_1 *descriptor; // imported variables };通过 hook block 的 retain,copy 等操作,交换其原始 invoke 函数并保存,从而达到了 hook。另外由于个别原因,个别 block 无法被 hook,我们采取了其它 workround 绕过了这些场景,提高了 block hook 的准确性。


  • load 方法


load 方法我们也是采取 hook 的方式,但利用了一个 DATA 段的 R/W 特性,即通过插桩 stub 函数,然后在 stub 函数里回调我们的 hook 代码即可。当然也可以采取简单粗暴的静态扫描的方式,但就没那么能保证顺序了。


  • c++构造函数


c++构造函数即:全局 c++变量或 constructor 修饰的函数会在 main 函数之前执行,同理这些函数列表也是存在 DATA 段内的,我们也可以利用插桩 stub 函数,再在 stub 函数里回调我们的 hook 代码即可。


  • initialize


同样利用 objc_stub 函数在发消息前先判定 cls 是否已经 initialize,已经 initialize 则忽略,否则记录对应函数符号。后续会改为采取插桩的方式。


  • 其它


以上我们通过 hook 或插桩的方式解决的 90%以上 objc 程序的问题,但是如果里面还涉及到 c,c++等代码的执行那就暂时行不通了。此时我们需要借助于静态分析。c,c++代码函数调用的本质是 bl 指令,所以我们是通过递归扫描 bl 指令来完成的,大概如下:


define PGOAINSBL (0x94000000)define PGOAINSBL_FLAG (0xfc000000)static void pgoascansubroutiner(intptrt func,int depth) { if(depth <= 0) return ; pgoainssst p = (pgoainssst)func; int i=0; while(i<2048) { intptrt vpc = (intptrt)p; int value = (int)*p; if((value & PGOAINSBLFLAG) == PGOAINSBL) { ... ... //此处省略关键代码 if(pgoavamainvalid(va)) { pgoaaddfunc(va); pgoascansubroutiner(va, depth-1); } } else if((value & PGOAINSRET_FLAG) == PGOAINSRET) { //ret return ; } i++; p++; } }
复制代码


这个方式能解决绝大部分问题,但是缺陷在于会有较大概率的误扫描,即可能存在部分死代码或极低概率才走的代码而被优化,反而浪费的部分重排空间。


5. 优化效果

采用了上述优化方案后,我们应用的 cold launch 启动速度大概提升了 10%,page fault 次数减少了 15%左右。



一键接入

看完上文是不是觉得怎么搞个二进制重排优化那么复杂呢?需要搞那么多,没那么人力精力来优化啊,没关系,可以用我们提供的 sdk。sdk 支持一行代码接入后,运行一次 App 即能把需要重排的符号给输出来。然后只要在 XCode BuildSetting 里设置 order file 路径即可。


if defined(arm64) || defined(aarch64)bool debug = false;
ifdef DEBUGdebug = true;
endifpgoa_logall(debug,nil);
endif
复制代码


然后将生成的 order_symbol.txt 文件导入到工程配置 order file 后重新编 Release 包后即可。


结语

1.对比与展望

对比其他已有公开的方案,我们的方案有什么特点:


  • 支持 sdk 一键接入

  • 支持动态 hook c++ constructor

  • 支持 initialize 方法 hook 和插桩

  • 支持所有 block 的 hook

  • 支持所有的 bl 函数调用(c/c++代码)


但通过分析我们也发现目前方案仍然存在一些问题,例如:


  • 静态扫描会导致部分符号顺序略微有出入

  • 常量,全局变量的随机访问导致频繁触发 page fault

  • 本机翻译符号效率较低

  • 考虑从更底层的方式去 trace


后续我们会尽快完善已知问题并可提供给外部使用,并持续优化部分已知问题。


2. 再谈 PGO

本方案本质也是一种 PGO(Performance Guided Optimization)。PGO 的目的就是根据 profile 调优的数据来倾向性的去做优化。其实苹果本身也提供了 PGO 的方式,但苹果本身的方案放在我们这些采用 CI 工具构建的大型 app 上部署和使用起来较为麻烦,且不利于我们自己去发现分析问题。比如通过自行完善 PGO,我们可以做到了解所有启动代码的顺序和时序,有更好的数据来帮助我们分析启动过程。而且能完美适应当前的 CI 构建工具,不需要做额外的适配和改变。


3. 参考

About the App Launch Sequence


About the Virtual Memory System


Thrashing(computerscience)


Optimizing App Launch


Improving iOS Startup Performance with Binary Layout Optimizations


objc4


Hook objc_msgSend – 从 0.5 到 1


hook C++ static initializers


本文转载自公众号云加社区(ID:QcloudCommunity)。


原文链接:


https://mp.weixin.qq.com/s/JEFqg0kKROyfaYJFHeudFw


2019-10-31 13:343380

评论 3 条评论

发布
用户头像
SDK可以提供吗?
2022-10-31 10:09 · 广东
回复
用户头像
SDK可以提供吗?
2022-10-31 10:09 · 广东
回复
用户头像
博主你好,文中提到的SDK,接入后,运行一次 App 即能把需要重排的符号给输出来。请问这个SDK是什么?是否可以给个demo看下效果
2021-07-19 14:41
回复
没有更多了
发现更多内容

星巴克涨价引热议!中国现磨咖啡市场目前到底如何?

易观分析

星巴克涨价 中国咖啡市场

再见了,我的散装研发管理平台;再见了,4台ECS!

阿里云云效

阿里云 DevOps 云原生 研发 敏捷研发

2022年1月视频行业用户洞察:假期影响下活跃用户开始回升

易观分析

移动视频 视频app

网络安全kali渗透学习 web渗透入门 如何进行NESSUS漏洞检测

学神来啦

Apache ShardingSphere 5.1.0 正式发布

SphereEx

数据库 开源社区 SphereEx Apache ShardingSphere

英特尔2022年投资者大会:以软件解锁更大增长机遇

科技新消息

会声会影2022语音转文字功能怎么用

懒得勤快

《数字经济全景白皮书》数字冰雪篇 重磅发布

易观分析

数字经济 冬奥会

会声会影2022重磅发布!会声会影2022全新功能详解

懒得勤快

记录一些Oracle操作命令

wong

oracle

会声会影2022美颜功能介绍 教你玩转视频美颜

懒得勤快

虎符Hoo研究院:Cosmos是如何实现链与链的“港口”相连的?

区块链前沿News

Hoo 虎符交易所 虎符研究院 Cosmos

哪里可以查到网络安全等级测评与检测评估机构目录?

行云管家

网络安全 等保 等级测评

系统学习 TypeScript(一)——认识 TypeScript

编程三昧

typescript

如何理解用户的行为?

石云升

产品经理 用户研究 用户模型 2月月更

浪潮国资云:国资为引,助力国企上云用数赋智

浪潮云

云计算运维

数据同步与缓存一致性问题

Mars

布隆过滤器 缓存一致性

第十四节:SpringBoot使用JdbcTemplate访问操作数据库基本用法

入门小站

springboot

在线时序流程图制作工具

入门小站

黑客马拉松(Hackathon)是什么?

Speedoooo

黑客马拉松 黑客松

晟盾科技加入龙蜥社区,共建开源新生态

OpenAnolis小助手

Linux 开源

迁移学习综述与未来展望 | 社区征文

战场小包

人工智能 迁移学习 新春征文 2月月更

学生管理系统模块4作业

刘洋

#架构实战营 「架构实战营」

医疗保健行业如何从区块链中受益?

CECBC

【案例】正浩创新:多云多资产,实现敏捷云上运维

行云管家

云计算

一行代码解决!iOS二进制重排启动优化_文化 & 方法_rhythm_InfoQ精选文章