前言
性能优化的过程分两部分:
- 发现性能瓶颈
- 制定方案,解决性能问题
解决性能问题的方案需要具体情况具体分析,并没有完全固定的路子,更多的是靠经验的积累,本文不做涉及。但是发现性能瓶颈确实有着固定的方法。本文主要介绍 如何找到性能瓶颈
。
如何找到性能瓶颈
常用的性能检测工具是traceview,集成于 Android Device Monitor
中。从Android Studio3.0开始, Android Device Monitor
被废弃,取而代之的是 Android Profiler
,其中提供了 Memory Prodiler
、CPU Profiler
、Network Prodiler
三大功能。
内存优化(包括内存泄漏)常用的是 MAT 或者 LeakCanary ,而 Memory Profiler 相当于将 MAT 的简化版功能集成到 AS 中。相对的在性能优化方面,CPU Profiler 相当于将 traceview 的功能集成到了 AS 中。
所以,使用AS3.0之前版本的,可以使用traceview,而使用AS3.0以后版本的,除了traceview,还可以选择CPU Profiler。
如果想追踪系统进程的详细数据,以解决帧引起的界面卡顿等问题,可以使用
systrace
,本文不做涉及。可参考:
Perform on-device system tracing
systrace
traceview 使用方法
使用 traceview 需要首先使用 Debug
类进行 插桩
,当应用执行到被插桩的代码时就会在手机sdcard中自动生成 .trace
文件,之后使用 traceview 或者 AS(3.0以上版本)打开文件即可。
一、插桩
插桩需要使用到 Debug
类,并且会在 sdcard 中生成 .trace
文件,所以你必须首先保证你的应用具有写外部存储( WRITE_EXTERNAL_STORAGE
)的权限。
在想要跟踪的代码逻辑开头和结尾处分别插桩:
// Starts recording a trace log with the name you provide. For example, the
// following code tells the system to start recording a .trace file to the
// device with the name "sample.trace".
Debug.startMethodTracing("sample");
...
// The system begins buffering the generated trace data, until your
// application calls stopMethodTracing(), at which time it writes
// the buffered data to the output file.
Debug.stopMethodTracing();
生成的 .trace
文件会被保存在固定目录下,与 getExternalFilesDir()
返回的目录相同,即 /sdcard/Android/data/[YOUR_PACKAGE_NAME]/files
下。
请注意,如果您的应用在未更改跟踪日志名称的情况下再次调用 startMethodTracing(),则会覆盖已保存至设备的现有日志。如果希望每次运行都保存至不同的日志文件,可以使用如下代码:
// Uses the SimpleDateFormat class to create a String with
// the current date and time.
SimpleDateFormat date =
new SimpleDateFormat("dd_MM_yyyy_hh_mm_ss");
String logDate = date.format(new Date());
// Applies the date and time to the name of the trace log.
Debug.startMethodTracing(
"sample-" + logDate);
如果系统在您调用 stopMethodTracing() 之前达到最大缓冲值,则会停止跟踪并向管理中心发送通知。 开始和停止跟踪的函数在您的整个应用流程内均有效。 也就是说,您可以在 Activity 的 onCreate(Bundle) 函数中调用 startMethodTracing(),在 Activity 的 onDestroy() 函数中调用 stopMethodTracing()。
二、查看 .trace 文件
插好桩后,安装应用并运行被检测部分的功能,然后就可以通过 AS 或者 traceview 查看文件了。
使用 AS 查看
在AS中点击 View - Tool Windows - Android File Explorer
打开 Android File Explorer :
在 /sdcard/Android/data/[YOUR_PACKAGE_NAME]/files
下即可找到生成的 .trace 文件,双击文件即可打开。
将 .trace 文件保存至电脑,直接拖入AS窗口,也可直接打开该视图。
在打开的视图中,左上方可以选择想要查看的线程。可以查看监控期间指定线程运行了多久、执行了哪些方法、每个方法执行了多久等等。
其中有4个名词需要解释一下:
- Wall Clock Time:壁钟时间,表示实际经过的时间,即进入某个方法到退出该方法的时间,不考虑线程是活动还是休眠状态。
- Thread time:线程时间,表示实际经过的时间减去线程没有消耗 CPU 资源(处于休眠)的时间部分。 对于任何给定函数,其线程时间始终少于或等于其壁钟时间。 使用线程时间可以让您更好地了解线程的实际 CPU 使用率中有多少是给定函数消耗的。
- Inclusive Time:方法执行自己代码的时间 + 执行自己child方法的时间。
- Exclusive Time:方法执行自己代码的时间。
使用 traceview 查看
要使用 traceview 查看,需要首先将 .trace 文件保存到电脑:
adb pull /sdcard/Android/data/[YOUR_PACKAGE_NAME]/files/sample.trace D:\Documents\sample.trace
打开 Android Device Monitor
。AS3.0以前的版本,就是LogCat所在的窗口,再切换一下tab页即可。AS3.0以后,进入 android-sdk/tools/
路径,运行以下命令:
monitor
虽然 Android Device Monitor 的 DDMS 也有 File Explorer ,但是未 root 的手机,查看不到上述路径,因此只能将 .trace 文件保存到电脑查看。
在 Android Device Monitor 中,依次点击 File - Open File
,选择 .trace 文件路径即可打开:
内容与AS打开时类似,相差较大的主要是图标部分,没有AS的 Call Chart
直观形象。
其中也有4个概念:
- Cpu Time:相当于AS中的 Thread time。
- Real Time:相当于AS中的Wall Clock Time。
- Inclusive Time:同AS一样。
- Exclusive Time:同AS一样。
使用 AS 查看还是使用 traceview 查看
这个就见仁见智了,根据我个人使用的感觉来看,建议使用AS查看。原因有二:
- AS更简单。不需要单独打开ADM,更不需要将 .trace 文件保存到电脑。
- AS的调用图( Call Chart )更加直观,cpu时间的消耗一目了然。
Call Chart 的水平轴表示函数调用(或调用方)的时间段和时间,并沿垂直轴显示其被调用者。 下图展示了一个调用图表示例,并描绘了给定函数的 self time、children time 以及总时间的概念。
最后需要注意一点,跟踪分析过程中,应用的运行速度会减慢。所以,通过 traceview 得到的分析数据并不能精确反应某个方法在实际执行时的绝对时间。关于这一点,在最后的注意事项中再做详细分析。
Google还提供了基于样本的分析方式,以减少分析对运行时性能的影响。要启用样本分析,需调用
Debug.startMethodTracingSampling()
方法(而非Debug.startMethodTracing()
方法)。系统会定期收集样本,直至调用stopMethodTracing()
。
CPU Profiler 使用方法
使用 CPU Profiler 进行函数跟踪比 traceview 更简单。不需要做任何代码上的植入,下面做一个简单的介绍:
首先,通过 View - Tool Windows - Android Profiler
打开 Android Profiler 。手机连接电脑后运行应用,在 Android Profiler 中会看到以下视图:
左上角可以选择设备和进程,点击 CPU 区域,即可进入CPU Profiler视图:
左上角可以选择跟踪模式:
- Sampled:按默认采样率捕获应用的调用堆栈。该模式的固有问题是,如果应用在一次捕获后进入一个函数并在下一次捕获前退出该函数,则分析器不会记录该函数调用。如果对此类生命周期很短的跟踪函数感兴趣,可以使用“Instrumented”跟踪。
- Instrumented:以在每个函数调用的开始和结束时记录时间戳。 分析比较时间戳,以生成函数跟踪数据。 需要注意的是,设置与函数关联的开销会影响运行时性能,甚至分析数据,对于生命周期相对较短的函数,这一点更为明显。 此外,如果应用短时间内执行大量函数,则分析器可能会迅速超出它的文件大小限制,且不能再记录更多跟踪数据。
- Edit configurations:自定义采样率。与 traceview 中的
Debug.startMethodTracingSampling()
类似。
.trace
文件的大小是有限制的。对于给定录制,当分析器到达该限制时,AS 将停止收集新数据(不过,这不会停止记录)。 在执行“Instrumented”跟踪时,这种情况通常会更快发生,因为与“Sampled”跟踪相比,此类跟踪在较短时间里会收集更多数据。
如果你使用的是Android 8.0(API 26)或更高版本的设备,则对于跟踪数据的文件大小没有限制,此值可忽略。不过,你仍需留意每次记录后设备收集了多少数据,因为 AS 可能难以解析大型跟踪文件。
点击上方的“开始录制”按钮,然后在应用中操作执行被追踪的功能,结束后再点击“停止录制”按钮。CPU Profiler 会自动开始分析并生成数据。
以上就是 CPU Profiler
和 traceview
的使用方法。至于如何制定优化方案,就不展开了,并没有完全固定的路子。就我本例的 onRebuild()
方法而言,是针对耗时的Contact构造过程做了并行处理,将上百个有序的构造过程平分到5个线程中并发执行,然后再按顺序合并数据到一个线程中。最终 onRebuild()
执行速度从15秒提升到了2.5秒,对我来说已经够用了。
重要注意事项
无论是使用 traceview 还是 CPU Profiler 进行函数跟踪,有一点需要注意:跟踪分析过程中,应用的运行速度会减慢。所以,分析数据并不能精确反应某个方法在实际执行时的绝对时间。下面是我在优化项目中的 onRebuild(boolean)
方法时,记录的4组数据,让我们来对比一下:
- 实际执行时间:不启用分析模式,正常运行状态下通过打印日志得到的实际执行时间。
- Profiler统计时间:使用 CPU Profiler 分析获得的执行时间。
- traceview统计时间:通过分析 traceview 产生的 .trace 文件,从中获得的执行时间。
- traceview实际时间:使用 traceview 的情况下,通过打印日志得到的实际执行时间。
为什么针对 traceview 会例举两个时间呢?这是因为测试过程中发现 traceview 自动分析出来的时间比
实际执行时间
不仅没有慢,反而快了很多,疑惑下又在启用 traceview 的情况下通过以下代码测算了一下实际的时间,这个倒是真的比实际执行时间
慢了。
Debug.startMethodTracing("smssdk_onrebuild");
long curr = System.currentTimeMillis();
onRebuild(true);
Log.d(TAG, "onRebuild lasts: " + (System.currentTimeMillis() - curr));
Debug.stopMethodTracing();
实际执行时间 | Profiler统计时间 | traceview统计时间 | traceview实际时间 |
---|---|---|---|
15s | 35s | 3.5s | 64s |
从上表数据可见,无论是 CPU Profiler 还是 traceview ,统计出来的时间都不能准确代表实际执行时间。更甚者,traceview自动分析出来的数据也与traceview跟踪模式下实际的时间有巨大差别,关于这一点,我没找到详细的解释,如果有人知道,还望不吝赐教。
既然跟踪分析得到的时间都不能表示实际的时间,那么这些数据是不是没用呢?当然不是!它们至少在以下两个方面具有价值:
- 在一次检测得到的数据中,线程内各个方法执行所耗时间在整个线程执行时间中所占比例具有一定参考价值。占比高的方法当是优化的重点目标。
- 优化前后两次检测得到的数据,有比较价值,以确认优化方案是否真的生效。
通过这些工具跟踪函数,也只能做一个相对的参考,并不能完全正确的反应函数的执行性能。比如我通过 CPU Profiler 获得的
onRebuild()
方法的分析数据显示,整个执行过程中 Contact 的构造方法占了60%左右,Contact.toString() 方法占了40%左右,但实际上在 onRebuild() 方法消耗的15秒中,Contact.toString() 只消耗了百毫秒级,而九成以上时间都被其构造方法消耗了,说明 CPU Profiler 的监控过程对 Contact.toString() 的性能产生了更大的影响。
而同样的问题却并没有出现在 traceview 的分析结果中。
请注意,CPU Profiler 和 traceview 不能同时使用,如果代码中植入了插桩的代码,则有可能导致 CPU Profiler 无法正常开始或停止录制。
traceview 和 CPU Profiler 的对比
从用法上来看,traceview 比 CPU Profiler 稍微复杂一点。类似于MAT需要首先获取 .hprof
堆转储文件,traceview 也要首先获取 .trace
文件,然后使用traceview分析该文件。而 CPU Profiler 则可以直接对应用进行分析。
从最终生成的图表上来看,CPU Profiler 生成的图表有 Call Chart
、Flame Chart
,它们可以非常形象的表示出线程内执行了哪些函数,函数的执行时间,调用栈等等,一目了然,而且在任意函数上点击右键,可以直接跳转至对应的代码,非常方便,在这一点上,相对于 traceview 要优秀。
参考文献
Inspect threads and methods traces
Inspect trace logs with Traceview
Generate trace logs by instrumenting your app