既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
我最近需要测试一款应用的启动性能 (同时摆弄了一下 Startup 库来了解它是如何影响启动性能的,未来的文章中会有更多相关内容)。我发现,就像我 以往做这类事情时一样,启动性能并不容易明确地被测试出来。
如果您正在测试一段运行时代码,那么有许多解决方案供您选择。从 “编写紧密的循环并使用 System.currentTimeMillis()
计算时间增量” 这种琐碎的方法,到更复杂和有用的解决方案,如使用 AndroidX benchmark 库所提供的功能。
但是按照定义,应用启动时的许多操作运行在系统调用您的代码之前。那么您要如何确定整个启动过程所需要的时间呢?
我浏览了一些日志信息、检查了一些底层 API,并询问了一些平台团队的工程师,终于获得了一些有用的信息。更棒的是,我现在可以使用 adb shell 工具完全自动化我的测试并输出信息,从而可以轻松地将结果导入到电子表格中进行分析。
我会在下面的文字中解释上述命令所使用的一些代码片段,并向您展示一到两个启动测试的简单步骤。
正如我在早些时间的一篇 博客 (不幸的是该博客已经过时而且并不正确) 中所写的那样,在 KitKat 发布后,有一个十分方便的日志一直在记录系统信息。无论何时,当一个 Activity 启动时,您都能看到日志中工具输出了以下信息:
ActivityTaskManager: Displayed com.android.samples.mytest/.MainActivity: +1s380ms
复制代码
这个持续时间 (本例中为 1,380ms) 表示了从启动应用到系统认为其 “已启动” 所花费的时间,其中包括绘制第一帧 (所以是 “已显示” 的状态)。
到达 “已显示” (Displayed
) 状态的过程并不需要包含您应用就绪之前所做的事情的花费时间。只要您的应用确定已完成加载和初始化,就可以通过调用 Activity.reportFullyDrawn()
向系统提供这些额外的信息。当您调用了该可选方法时,系统会记录另一个带有时间戳和持续时间的日志:
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 脚本的代码非常优雅,不是吗?
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-meFJM1Qv-1715165745215)]
[外链图片转存中…(img-j4NpYDae-1715165745215)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!