本文的编写目的,更多的在于介绍性能、启动测试以及我进行启动测试背后的缘由。但若是您只是但愿可以快速得到结论,能够直接参考下面的内容:android
$ for i in `seq 1 100` > do > adb shell am force-stop com.android.samples.mytest > sleep 1 > adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2 > done
上面的命令会循环 100 次: 启动应用、输出启动过程耗时,而后终止进程以准备好下一次循环。shell
我最近须要测试一款应用的启动性能 (同时摆弄了一下 Startup 库来了解它是如何影响启动性能的,将来的文章中会有更多相关内容)。我发现,就像我 以往作这类事情时同样,启动性能并不容易明确地被测试出来。架构
若是您正在测试一段运行时代码,那么有许多解决方案供您选择。从 "编写紧密的循环并使用 System.currentTimeMillis()
计算时间增量" 这种琐碎的方法,到更复杂和有用的解决方案,如使用 AndroidX benchmark 库所提供的功能。app
可是按照定义,应用启动时的许多操做运行在系统调用您的代码以前。那么您要如何肯定整个启动过程所须要的时间呢?工具
我浏览了一些日志信息、检查了一些底层 API,并询问了一些平台团队的工程师,终于得到了一些有用的信息。更棒的是,我如今可使用 adb shell 工具彻底自动化个人测试并输出信息,从而能够轻松地将结果导入到电子表格中进行分析。性能
我会在下面的文字中解释上述命令所使用的一些代码片断,并向您展现一到两个启动测试的简单步骤。测试
正如我在早些时间的一篇 博客 (不幸的是该博客已通过时并且并不正确) 中所写的那样,在 KitKat 发布后,有一个十分方便的日志一直在记录系统信息。不管什么时候,当一个 Activity 启动时,您都能看到日志中工具输出了如下信息:gradle
ActivityTaskManager: Displayed com.android.samples.mytest/.MainActivity: +1s380ms
这个持续时间 (本例中为 1,380ms) 表示了从启动应用到系统认为其 "已启动" 所花费的时间,其中包括绘制第一帧 (因此是 "已显示" 的状态)。ui
到达 "已显示" (Displayed
) 状态的过程并不须要包含您应用就绪以前所作的事情的花费时间。只要您的应用肯定已完成加载和初始化,就能够经过调用 Activity.reportFullyDrawn()
) 向系统提供这些额外的信息。当您调用了该可选方法时,系统会记录另外一个带有时间戳和持续时间的日志:google
2020-11-18 15:44:02.171 1279-1336/system_process I/ActivityTaskManager: Fully drawn com.android.samples.mytest/.MainActivity: +2s384ms
我只想要到 "已显示" 时所持续的时间,因此内建的日志对我来说已经足够好了。
性能测试老是应当屡次去运行测试用例,以排除结果中的可变因素。进行的运行次数越多,平均结果就越可靠。我至少会尝试运行测试十次,可是作的次数更多效果会更好。根据结果的变化程度以及时间的长短 (由于变量的存在会对持续时间更短的测试产生更大的影响),可能须要运行更屡次才行。
疯狂就是重复作相同的事情,却期待不一样的结果。
——阿尔伯特 爱因斯坦
性能测试推论:
"疯了" 就是同一件事只作一次,却但愿获得最佳结果。
——不是爱因斯坦说的
经过点击图标来连续屡次启动应用是一件很是繁琐的事情。并且这种操做不具有一致性,且有许多难以预测的因素,由于很容易就会引入变量——如您偶然间错误地启动了另外一个应用,或者使系统作了额外的工做而没法得到计时结果。
所以,我真正想要的是某种从命令行启动应用的方式。有了它,我就能够反复运行该命令来执行相同的操做,从而避免手动启动应用带来的可变性 (和乏味)。
adb (Android 调试桥,阅读至此的读者应该都对它很熟悉了吧) 提供了我所须要的东西。更具体地说,adb shell 提供了用于启动应用的命令行界面: adb shell am start-activity。该命令还可以在应用启动完成以前保持阻塞状态,所以咱们还要使用 -W 参数 (这对下一步来讲是必需的。咱们下一步将使用后续命令杀死启动后的应用)。这是完整的启动命令:
$ adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity
最后一个参数是应用的包名与组件信息。您能够看到它们与上一部分中 ActivityTaskManager
输出的日志相同。
运行此命令将启动应用 (除非该应用已经在前台,但这种状况并非理想的状态,咱们将在下一步对这种状况进行处理),并输出如下信息:
Starting: Intent { cmp=com.android.samples.mytest/.MainActivity } Status: ok LaunchState: COLD Activity: com.android.samples.mytest/.MainActivity TotalTime: 1380 WaitTime: 1381 Complete
检查一下 TotalTime
结果: 结果与咱们在日志中看到的信息彻底相同:
ActivityTaskManager: Displayed com.android.samples.mytest/.MainActivity: +1s380ms
这意味着咱们无需翻看 logcat,而是能够直接从运行命令的控制台中即可获取这些信息。更棒的是,咱们能够剥离多余的文本并仅保留启动结果,从而更轻松地提取此数据以供其余地方使用。
为了将上面的输出转换为启动持续时间,我使用 grep 和 cut shell 命令来输出内容 (有多种方法能够执行此操做,我只是随机选择了其中一个):
adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
如今,当我运行这条命令时,就能如我预期般的只得到一个简单的数字:
$ [start-activity command as above...] 1380
在您检查启动性能前,最好先了解 "冷启动" 和 "热启动" 之间的区别。
"冷启动" 是指您的应用在安装后的第一次启动、重启,或者不在后台时的启动。
另外一方面,"热启动" 是指您的应用已经启动且正在后台运行 (但被暂停了) 时的启动。
这两种状况都值得去测试和理解。但总的来讲,冷启动才是您进行启动性能测试的最佳起点,这其中有两个缘由:
为了在每次运行时强制进行冷启动,您须要在两次运行期间终止应用。再一次强调,在屏幕上执行这一操做 (例如,将应用从启动器的 "概览" 列表中滑出) 是乏味且容易出错的,而 adb shell 能够解决这一问题。
有几个不一样的 shell 命令可用于终止应用。最显而易见的是 adb shell am kill…... 但事实上这条命令并不能解决问题。当您启动应用后,应用会处在前台,而 kill 不会终止处在前台的应用。做为替代,您须要使用 force-quit 命令:
adb shell am force-stop com.android.samples.mytest
您可使用应用的包名告诉它须要终止哪一个应用。
如今,您已经有了能够启动应用、输出启动持续时间数据,以及退出应用并使其能够再次启动的一系列命令。您能够一遍又一遍地在控制台中输入这些内容,可是在 shell 中,咱们能够将这些命令放在循环里,而后只用一个命令就能够重复运行它。
在执行此操做时,为了不应用被终止而产生反作用 (例如,当应用程序被终止时,系统会将启动器拉到前台),您可能会想要在终止应用后延缓下一次的启动。为此,我增长了一秒钟的 sleep 以在两次操做之间插入一个小的缓冲时间。
下面是我所使用的命令的最终版本,其中包括了终止应用、等待一秒钟,而后重启应用。我将这一过程循环执行了 100 次,从而能够提供一个合理的样本量:
$ for i in `seq 1 100` > do > adb shell am force-stop com.android.samples.mytest > sleep 1 > adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2 > done
在运行此命令时,每当启动完成,我均可以得到输出到控制台的启动持续时间,而这正是我要跟踪和分析的数据。
注意 : 以上操做其实有更简单的方式,您可使用 -S
(用于首先中止 Activity) 和 -R COUNT
(用于执行 start-activity
命令 COUNT
次) 来循环启动 Activity,因此我也能够用下面的命令完成以上操做:
$ adb shell am start-activity -S -W -R 100-n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2
可是,为了在应用的终止和启动之间加入缓冲时间,以确保其处于非活动的状态,我但愿能使用 sleep 1 命令,所以我采用了更为冗长的方式进行循环。此外,shell 脚本的代码很是优雅,不是吗?
CPU 架构,尤为是 CPU 频率,是影响移动设备性能的重要因素。具体而言,移动设备减小电量消耗及避免出现过热的问题的主要方法之一,即是限制 CPU 速度。
限制 CPU 对于节省电量颇有用,但却对性能测试有负面影响,由于在这类测试中,结果的一致性相当重要。
理想状况下,在运行性能测试时,您应该控制 CPU 频率。然而您是否可以执行这一操做取决于您所拥有的设备——您须要拥有设备的 root 访问权限才能控制 CPU 调速器,从而才能控制 CPU 频率,而且不一样的设备执行这一行为的方式也可能不一样。
接下来的内容仅适用于您的设备容许且您能够取得 root 访问权限的状况。而在设备方面,我知道 Pixel 设备能够得到访问权限,但这不表明其余设备也一样能够。
在任何状况下,若是能够的话,建议您锁定 CPU 主频。对于您特定的测试而言,可能不会有明显的影响 (实际上,系统一般会在启动应用时使 CPU 运行在较高的频率上,所以可能已经提供了所需的一致性)。可是,这么作至少能够消除 CPU 主频这一可变因素。
手动锁定 CPU 频率可能很棘手,但幸运的是,AndroidX benchmark 帮您简化了这一操做。实际上,您甚至不须要为 benchmark API 编写代码——您能够经过使用其提供的 lockClocks
与 unlockClocks
工具来使用该库。
首先,向工程级别的 build.gradle 文件中加入 benchmark 的依赖:
// 查看 Benchmark 库的最新版本号 // https://developer.android.google.cn/jetpack/androidx/releases/benchmark def benchmark_version = "1.0.0" classpath "androidx.benchmark:benchmark-gradle-plugin:$benchmark_version"
接下来,在应用级别的 build.gradle 文件中应用 benchmark 插件:
apply plugin: androidx.benchmark
如今,您能够同步您的工程 (Android Studio 可能已经在强迫您执行此操做),同步完成后即可以从 gradlew
中使用锁定任务。
如今,您能够经过在命令行上运行命令来锁定主频了 (我是经过 Android Studio 内部的 "终端" 工具运行它的,可是您也能够在 IDE 外部运行它):
$ ./gradlew lockClocks
当我运行完命令后,即可以在命令行看到以下输出:
Locked CPUs 4,5,6,7 to 1267200 / 2457600 KHz Disabled CPUs 0,1,2,3
这段输出代表 benchmark 能够在个人 Pixel 2 上正常工做。更好的消息是,个人启动测试如今花费的时间比之前要长得多。您也许会好奇,为何主频变慢了?
该 benchmark 工具将主频锁定在便于持续运行的级别,而不是高性能级别。若是将主频设置为尽量高,则可能会得到更好的性能,可是:
请注意,完成测试后,您须要将主频解锁。设备会在从新启动时进行解锁,可是您也能够经过运行相反的 gradle 任务来解锁主频:
$ ./gradlew unlockClocks
其实这一命令只是从新启动设备以执行重置操做。(若是您想了解 benchmark 锁定功能的更多信息,请查阅 用户指南)。
锁定时钟后,我准备好了一切: 可以可靠重现启动情况的系统、一个执行后能够返回结果流的简单命令行。我能够复制结果并粘贴到电子表格中并进行分析 (经过将启动时间平均值与我想尝试的各类状况进行比较)。
理想状况下,我不须要撰写文章来讲明如何完成全部这些操做。老实说,您并不须要上文中的所有说明。(可是知道事情的工做原理和缘由老是更有趣,不是吗?) 您真正须要的只是 for() 循环 shell 命令,以及可选的锁定主频的方法。
$ for i in `seq 1 100` > do > adb shell am force-stop com.android.samples.mytest > sleep 1 > adb shell am start-activity -W -n com.android.samples.mytest/.MainActivity | grep "TotalTime" | cut -d ' ' -f 2 > done
为了简化性能测试和分析,以及整体上提升应用程序性能,咱们的团队正在研究简化此过程的方法,请持续关注咱们以得到后续分享的内容。同时,但愿以上命令和信息对您的启动性能测试有所帮助。