说到性能优化,App的启动时间是经常谈到的话题,通过 adb 命令可以收集到,那么如果要统计一个App中每个页面的启动时间,应该如何收集呢?这里可以使用三种方式获取 Activity 的启动时间:
- 使用 adb am start -W 命令
- Activity 启动后查看 Android Studio 日志:I/ActivityManager: Displayed xxx/xxx: + 100ms
- 使用 AOP 在 Activity 创建和可见时间点进行相应方法 hook 实现
以下只会讲一下如何第一种命令方式获取 Activity 的启动时间以及如何利用 Python 脚本收集整个 App 中页面的启动时间,前两种是相关的。adb 命令收集这种可用于本地 debug 测试,AOP 方式则可用于线上收集不同手机型号的启动数据。【Tips:这里的主要针对应用型App】
那么要知道 Activity 启动时机,无论通过am start 命令启动系统还是AOP方式,总该知道启动的时间起始点,知道了整个启动过程执行了哪些操作,我们也就可以针对这些方法进行优化减少 Activity 启动时间。So,后面会有一波源码讲解介绍,精简但足够了解前因后果。
嗯,要开始装厉害了,哈哈哈……
一、使用 am start -W 命令启动 Activity
使用场景:启动一个应用;启动应用中的某个页面。
完整命令:adb shell am start -W com.coral.aop/.hook.HookInstrumentationActivity
命令结果如下:
得到这样的结果是不是很神奇?作为爱究根到底的程序员,肯定是很想知道前因后果。带着源码看问题,更有助于理解。以下是当时我最想弄懂的几个问题:
1. am start -W 为什么能启动一个应用中的某个页面?
2. am start -W 启动应用后终端输出的结果参数的含义。
3. am start -W 启动 Activity 同 startActivity() 启动有什么区别?
4. am start 命令启动得到的时间和方式二中控制台打印的 Displayed 日志有什么区别和联系?
5. Hook 统计时间从 execStartActivity 开始到 onResume 结束是否为有效统计?
问题一:adb shell am start -W 为什么能启动一个应用中的某个页面?
这里其实主要是通过 adb 这个命令行工具去执行手机中 /system/bin 目录下的 shell 脚本,而 am 命令则是执行该目录下的一个名为 am 的shell 脚本文件。这里可以通过 adb shell am ls /system/bin 命令列出该目录下的所有脚本文件。其他的命令可以查看官方 adb 命令文档。
查看 am 这个脚本文件,内容如下:
#!/system/bin/sh
if [ "$1" != "instrument" ] ; then
cmd activity "$@"
else
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"
fi
这个脚本作用主要是通过 app_process 命令去执行 framework 下的am.jar 包中的 Am 类入口 main() 方法。要确认这个问题,首先得去看Framework底层源码,点击这里 可到github上下载aosp镜像代码库。省略 app_process 相关源码,只需知道 app_process 主要是用来启动 zygote 或其他 Java 程序的应用程序。直接定位 cmds/am/com/android/commands/am/Am.java,通过分析源码得到 am start 命令启动 Activity 具体流程如下:
总结:通过 am start 启动一个应用中的某个页面,其实最终还是会通过 AMS 来启动并进行管理 Activity 。
问题二:am start -W 启动应用后终端输出的结果参数的含义。
Starting: 启动 Activity 的 Intent
Status: 两种值 timeout or ok
Activity: 启动的 Activity(如果在 onCreate 销毁或者 crash 则为下一个展示的 Activity)
ThisTime: 表示一连串 Activity 的最后一个 Activity 的启动耗时
TotalTime: 表示新应用启动耗时,包括新进程启动和Activity启动
WaitTime: 总耗时,包括前一个应用 Activity pause 时间和新应用启动时间
问题三:am start -W 启动 Activity 同 startActivity() 启动有什么区别?
简单来说,通过 am start 命令启动和 通过 startActivity() 方式启动,最终都会走到 AMS.startActivityAndWait() 方法,之后的执行流程基本一致。
具体的不同,通过查看源码,最终整理出了这样一张很长很长的流程图,中间涉及到的一些颇复杂的跳转,因为自己也没完全理清,暂时省略,只绘制了整体的流程走向,如下图所示:
问题四:am start 命令启动得到的时间和方式二中控制台打印的 Displayed 日志有什么区别和联系?
这个问题,可以直接看问题三中的启动流程图,两者得到的参数其实是在同一个地方进行赋值和打印(仅针对单个启动的Activity来说),最终是在 窗体可见后回调执行 ActivityRecord.reportLaunchTimeLocked() 方法中对 totalTime 和 thisTime 进行赋值操作。
问题五:Hook 统计时间从 execStartActivity 开始到 onResume 结束是否为有效统计?
为什么会有这样的一个疑问,因为当时我这边先用AOP方式收集启动时间,之后使用 am start 命令收集,但是发现通过 AOP 方式收集从 execStartActivity 到 onResume 这段时间总是小于 命令得到的 启动时间(主要看单个Activity启动得到的 ThisTime 数据),一直不知道为什么少,因为若是统计初次启动渲染时间,这两个时间值是不会差太多。最终通过看源码理了一下流程,得到问题三中的图,发现同启动时间相关的几个方法执行顺序如下:
把 onResume() 当做 Activity 对用户真正可见的时间点并不准确,而应该将 onWindowFocusChanged() 方法作为该时间点(具体可查看Activity中对这两个方法的注释说明)。而通过 am start 命令得到的 ThisTime 这一个统计时间,记录的结束点也是在窗体被绘制完成后记录的结束时间点(两种方式的时间差距可对标红色箭头标识部分,可忽略不计。)
二、Python 脚本收集 Activity 启动时间执行流程
详细具体的流程如下图所示:
最终展示结果如下:
优化点:向 AndroidManifest.xml 文件中给 Activity 插入 android:exported 属性可以通过写一个 gradle task 来实现。
*Tips:为什么需要添加 android:exported=“true” ?
— android:exported 表示是否允许跨进程调用,activity 属性声明为true,表示该 Activity 组件可以被其他应用调用。通过 Component 形式打开某个应用的 Activity,也需要设置 exported 属性才能打开。*
三、总结
这一篇通过命令收集 Activity 的启动时间,收集到的仅是从 Activity 启动到窗体第一次绘制完成的时间,为初次渲染时间,可用于本地调试。如果需要获取渲染网络数据后的时间,则需要通过 AOP 方式获取。为什么要收集 Activity 启动时间呢?主要是优化一些启动比较耗时的 Activity,但是大多数时候,这个命令经常用于优化 App 启动的时间,也即是首页Activity 启动的时间,但是由此统计非首屏 Activity 的启动时间也够用,毕竟除去首屏Activity,如果发现了其他耗时启动的页面也还不错呢!
Activity收集脚本和服务器源码实现,有需要的欢迎到 github 进行下载。
项目地址:https://github.com/CoralXss/ActivityLaunchTimeCollector
如何搭建vue前端和python后台服务,详细步骤可参看:Flask + Vue 搭建简易系统步骤总结