利用 Python 脚本收集 Activity 启动时间

说到性能优化,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 启动时间执行流程

详细具体的流程如下图所示:
脚本执行流程

最终展示结果如下: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 搭建简易系统步骤总结

参考文章:Android 中如何计算 App 的启动时间?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您可以按照以下步骤使用Python脚本启动Appium服务: 1. 首先,您需要确保已经安装了Appium和Python。 2. 在Python脚本中导入Appium Python客户端库。 3. 创建Appium服务的配置对象并设置所需的选项。 4. 使用Appium Python客户端库启动Appium服务。 5. 在需要使用Appium的测试代码中使用WebDriver对象与Appium服务进行通信。 以下是一个使用Python脚本启动Appium服务的示例代码: ``` python from appium.webdriver.appium_service import AppiumService # 创建Appium服务配置对象 appium_service_config = { 'address': '127.0.0.1', 'port': 4723, 'command_timeout': '7200', 'session_override': True, 'log_path': './appium.log' } # 启动Appium服务 appium_service = AppiumService() appium_service.start(**appium_service_config) # 使用Appium服务的WebDriver对象执行测试代码 from appium.webdriver import Remote desired_caps = { 'platformName': 'Android', 'deviceName': 'Android Emulator', 'appPackage': 'com.android.calculator2', 'appActivity': '.Calculator' } driver = Remote('http://localhost:4723/wd/hub', desired_caps) # ... 在WebDriver对象上使用Appium服务进行测试 ... # 停止Appium服务 appium_service.stop() ``` 请注意,您需要在Appium服务配置对象中设置要使用的地址、端口、命令超时时间、是否允许会话覆盖和日志路径等选项。然后,通过调用AppiumService对象的start()方法来启动服务。一旦服务启动,您可以使用WebDriver对象与Appium服务进行通信并执行您的测试代码。最后,您需要调用AppiumService对象的stop()方法来停止服务。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值