iOS构建性能保障体系之自动化性能测试

1,321 阅读7分钟

本系列文章将围绕 iOS 客户端性能保障话题,分别从开发周期的各个阶段来讨论如何构建完整的性能保障体系。

- 开发阶段

     框架选型

     模板代码

     性能测试工具

- 测试阶段

     自动化性能测试

- 线上运行阶段

    APM 性能监控

性能测试是指收集每次版本迭代的性能差异,暴露进程启动和运行时交互响应快慢、帧率等和体验息息相关的指标。有些团队会定义性能基线,每次回归测试时由人工执行一些主要用例。之所以从性能测试开篇,是因为网上资料相对少,且因为 iOS 平台的限制对自动化技术的发展造成相当的阻碍。

对于性能测试而言,持续的性能测试是防止性能劣化有效的手段,而如何保证持续性,关键就是实现人工的替换,而尽可能采用自动化无人干预的执行方式。

本文将从实操的角度出发,概述如何实现完备的、可实施的 iOS 自动化性能测试。

Instruments

Instruments 是 Xcode 工具链中重要的性能分析工具,能够揭示大部分性能问题。在 Instruments 10 时迎来了重大重构版本的 Instruments。将界面和分析核心完全解耦,使开发者可以自定义模板,阶梯性的定义开发者需要的初级、中级以及高级应用。 

Instruments 10 将命令行工具集成到 xcrun xctrace 工具中,基于 xctrace 可以很方便的将分析结果 trace 文件解析成 xml 让其他工具链读取。

`# 命令行启动 TimeProfile 并导出分析结果
xcrun xctrace record --template TimeProfile --launch --output ./output.trace xxxx-ios-app.app
# 将分析结果导出为 xml 格式
xcrun xctrace export --input ${trace} --toc  --output ${exportXMLName}`

将 xcrun 集成到我们的自动化任务中,是不是就可以执行导出测试数据做进一步分析和展示了?

但是 Instruments 生成的数据是类数据库表的二进制格式,在 Instrument 10 发布之前,要解析性能数据可以借助开源库 TraceUtility 来实现,而 TraceUtility 是通过逆向工程调用了 Instruments 相关的 Framework 来实现的,也仅仅是实现了一部分的数据导出,如果要完全解析将是一个漫长的逆向过程。

在 Instruments 10 之后,xctrace 提供了导出的功能,但是导出的数据是未经符号化的函数调用地址,且仅有函数地址,缺少符号化必要的偏移量、基地址等信息。

如果需要采集方法级别的数据,为了获得完整信息,我们需要绕过 Instruments 客户端,直接和 Instruments server 通信。

要知道什么是 instruments Server,可以先研究一下 Troy Bowman 在 2018 年的分享发现 server 的秘密

image.png

简单讲 Server 是运行在 iOS 上的特殊进程,可以和电脑端进行通信从而发送性能数据,这里作者开源了如何构建消息结构和 Server 进行通信的开源实现 github.com/troybowman/…

Instruments 统计方法耗时有其无可比拟的优势,无侵入性且能采集到信息非常丰富,但是由于其统计方法耗时是基于抓取栈帧的方式,无法统计方法调用次数(据说字节火山团队可以统计到),以及每次所耗时间,这对我们做历史对比时会造成一些不便。

除了方法耗时外,Instruments 的另外一些模板也是非常值得使用的,比如获取启动耗时、获取所有网络请求记录等,这些不需要符号化的模板可以直接拿来集成到自动化任务中。

运行时统计方法耗时

相比 Instruments 的基于采样,运行时统计有一定侵入性,统计代码本身也可能会影响性能数据,但相比 Instruments 采样的数据更精确,也更适合做劣化对比。

所有 OC 方法的调用都会走 objc_msgSend 方法,所以理论上通过 Hook objc_msgSend 方法 就可以统计所有方法的调用及耗时,而且很容易获取到 Class 、SEL 等符号信息。

image.png

具体如何实现可参考 DoraemonKit 的代码片段,或者博文 github.com/QiShare/Qi_… 本文将不再展开概述 hook 原理。 

统计相关代码可以 2 种方式注入测试对象工程:

  1. 通过 cocoapods 指定 debug 环境引入,最好是不要发布到线上,造成线上的性能影响。

  2. 下载构建好的包直接注入重签的方式引入,这种方式的优点是无需重新构建,速度会快很多。

APP 端统计到耗时方法后需要做进一步的处理,比如拿到方法名称后去拉取 Git log 定位修改人,这时候就需要电脑端和 app 进行通信,本文推荐使用 peertalk(github.com/rsms/peerta…) 通过 USB 通信,peertalk 提供了更高级别的封装。通过在 APP 端构建一个 http 服务,在电脑端脚本就可以一边执行自动化测试、一边通过 http 请求获取 APP 端实时采集到的性能数据了。

集成自动化测试

有了上面的获取耗时的方法,我们还需要自动执行操作,iOS 自动化测试限于平台限制,各个框架基本都是基于 WebDriveAgent 的原理来实现的,这种方式有一定缺点,第一是比较慢,第二是无法保证测试任务的高可用性,实际测下来总有一些环节会失败,所以测试代码需要增加必要的重试逻辑,可以结合业务对各个事件调用做进一步的封装增加可达性。

本文采用了老牌的 Appium 来驱动:

image.png

对于测试脚本,我使用的是 JavaScript,把性能采集和 appium 结合以后,写出来的测试代码大概是这样:

image.png

最终导出来的方法耗时效果:

image.png

定位到耗时方法后,再通过 Git 提交记录来获取到修改人,从而通知到相关开发,甚至自动的创建 jira 任务跟踪修复情况。

而对于其他性能数据,比如 FPS、CPU 占用等,如何获取可以参考滴滴的 DoraemonKit 的相关实现,获取到之后通过 peertalk 传到电脑端做进一步的处理,之所以推送到电脑端来处理是为了尽可能减少注入的统计代码对原 APP 整体性能的影响。

持续产出性能报告

另外还需要一些必要的基础设施,例如对性能数据的统计分析、展示页面等,推荐使用 Nodejs 也是为了能够更方便的构建这些基础设施,毕竟 Full-Stack JavaScript 的学习成本还是比较低的。

本文主要逻辑代码是一个 Nodejs 工程,集成了包括定时任务、Appium 接口封装、APP通信、匹配git 提交人、生成测试报告等逻辑,单独部署到一台 Macmini 上。而测试报告的展示直接使用了 Docusaurus ,它的好处是脚本生成markdown 非常方便,然后又可以很方便集成 gitlab ci 构建出静态页再部署到 gitlab pages 上,学习一点 React,还可以集成很多优秀的图表库到你的测试报告中来,例如 Echarts、D3等。

image.png

整体流程可以总结为:

定时任务触发 => 拉取代码构建新包/或者直接下载最新测试包重签注入hook和控制代码 => 执行 Appium 测试脚本及和APP通信获取性能数据 => 导出和保存测试过程收集的性能数据 => 生成可阅读测试报告(Docusaurus) => 提交测试报告 => 发出阈值报警等

【未完】