本文选自「抖音 Android 性能优化」系列文章。
「抖音 Android 性能优化」系列文章是由抖音 Android 基础技术部门技术专家倾力打造的技术干货内容,和大家分享基础技术团队在打造极致用户体验的抖音的过程中,收获的性能优化方法论、工具和实践,与各位技术同学一起交流成长。
用户交互响应的耗时,作为 Android 用户日常感知最深的一项性能指标,在日常开发中有着非常重要的意义。而抖音 Android 基础技术团队为打造极致的交互响应体验,一直在致力于极致性能的探索,其中就包括如何打造极致的耗时检测工具。
概述
俗话说,工欲善其事,必先利其器,我们要做好性能优化,首要是要能够发现性能的问题,这就需要有靠谱的工具来帮助我们做性能分析。市面上主流的性能分析工具有:Systrace、TraceView、Android Studio 的 CPU Profiler。相信做性能优化的同学对这些工具应该都是非常熟悉了,抖音最早也是用 Systrace 作为主要的分析工具,在优化前期也发挥了比较大的作用。随着抖音的性能优化来到了深水区,我们需要发现并解决更细粒度、更多维度的性能问题,我们会关注几毫秒的耗时,关注线上一些低端机用户遇到的锁阻塞和 IO 等待问题。而市面上这些主流的性能分析工具因其使用的局限性和较大的性能损耗,已经无法满足抖音性能优化的需求。为了能够百尺竿头更进一步,我们需要开发更加灵活、精细化以及多元的信息和工具来辅助我们进行高效的优化工作。
在这样的背景之下,抖音 Android 基础技术团队开发了 Rhea( [ˈriːə] 瑞亚,寓意时光女神)跟踪器(Tracer),其是一种通过静态代码插桩技术自动添加 Trace,用来分析 APP 运行时耗时的性能分析工具,意思是要做一个功能全面、追求效率、大家都喜欢的女神,也符合我们工具的核心设计原则。Rhea 跟踪器获取 Trace 不仅要性能损耗低,还要能脱离 PC 端工具在 App 侧直接抓取,跟踪更多常规函数耗时的同时还要可以跟踪系统调用,如:锁信息、I/O 耗时、Binder IPC 以及更多其他信息。最后,还提供转换脚本工具,用于将原始跟踪文件生成可视化报告,便于用户分析性能问题。
优势对比
Rhea 当前因其无侵入、高性能、信息全等优势已在字节多个 APP 上落地使用,效果明显,已多次帮助大家快速发现性能问题,其包含的信息包括不限层级的应用层函数、IO、锁、Binder、CPU 调度等耗时信息等,其部分效果如下所示:

相对于其他 Android 性能排查工具,其具体优势表现为:
当前,Systrace 只能监控特定系统信息,监控应用层的耗时则需要手动打点;TraceView 性能跟采样率关系密切,采样过于频繁性能开销巨大,采样过低又难以精准发现问题函数;Nanoscope 虽然几乎没有性能损耗,但每次都需定制 ROM 刷机,使用成本非常高,并且这些工具都只支持 debugable 的应用程序线下分析,这些工具在针对 APP 性能优化都有不甚完美之处,而 Rhea 是一个集大成者,融合了各工具优势并弥补了相关缺陷。
架构演进之路
第一阶段:基于 Systrace 补充函数耗时 Trace
Systrace 是 Android 性能调试优化的常用工具,它可以收集进程的活动信息,如函数调用耗时、锁等;也可以收集内核信息,如 CPU 调度、IO 活动、Binder 调用信息等;这些信息会统一时间轴,在 Chrome 浏览器中显示出来,方便工程师性能调试、优化卡顿等工作。因此,抖音早期性能优化首选 Systrace 作为主要工具,其大致流程如下:

1. 功能改造
Systrace 工具只能监控特定系统调用的耗时情况,它不支持应用程序代码的耗时分析,所以在使用时有一些局限性。原生 Systrace 需要开发者在方法的起止位置手动加入 Trace.beginSection
与 Trace.endSection
方法对,这个过程就变成了开发者预判耗时位置,然后在手动加入监控函数对,通过不断重复添加监控点、打包、运行、采集数据,从而一步步完成耗时方法定位,这也使得 Systrace 的使用成本变得极高。
为了提高 Systrace 的易用性,我们开发了 Rhea 1.0 对 Systrace 功能进行了改造,加入了自动插桩机制:通过字节码插桩自动完成 Trace.beginSection
和 Trace.endSection
方法对的插入,并且通过运行时限制方法层级的方式,来有效控制因引入监控带来的性能损耗。
插桩类及桩方法伪代码:
class Tracer{
method_stack = list()
max_size = 6
methodIn(method_id, method_name){
if(method_stack.size()<=max_size){
method_stack.push(method_id)
Trace.beginSection(method_name)
}
}
methodOut(){
if(method_stack.size>0){
method_stack.pop()
Trace.end()
}
}
}
被插桩方法:
method1(){
Tracer.methodIn(1,method1)
...
Tracer.methodOut()
}
输出数据如下所示,指定层级内所有方法即可按照预期展示在输出 html 中:
2. 方法 Did not finished 问题
在使用改造后的 systrace 时,我们时常会遇到如下问题:

分析发现,主要原因在于方法在运行期执行中被中断,例如:方法执行过程中发生异常后,被其调用者方法捕获,发生异常方法的 Systracer.o 方法未被调用。如图:test 方法中的 error 方法执行时出现 arr[2]的数组越界,导致 test 方法中的插桩方法 SysTracer.o(13L)未调用,异常被 onCreate 中的 catch 块捕获,从而导致 test 的插桩方法没有被成对调用,最终导致了 test 外层所有的方法调用都无法正确闭合。(注意:本小结提到的桩方法,即 SysTracer 相关方法,均是通过字节码插桩自动插入)

解决办法,在外层所有异常捕获的位置,额外插入桩方法,重新这种异常调用链下的桩方法不成对问题。如下图:
3. 依然存在的问题
性能问题
随着 Rhea 1.0 功能的深入使用,在带来极大便利的同时,功能本身的不足也逐渐暴露出来。在采集数据过程中,其本身的性能损耗会导致在一些实际性能优化过程中会带偏方向。经我们严格测试,其性能损耗有 11.5%左右,如下所示:

在实际使用过程中发现,在开启 Systrace 之后,对应 Sleep 耗时占比在极端情况下会超过 40%以上。一方面是 APP 锁带来的 Sleep 耗时。例如,在抖音启动路径上 SharedPreference 优化过程中,在开启 Rhea 1.0