Android之App启动时间的统计方法

本文详细介绍了如何使用ADB命令测量Android应用的启动时间,并区分了冷启动与热启动的不同场景。此外,还深入探讨了游戏启动过程中的系统部分与游戏自定义部分。

之前有人在知乎提问: “怎么计算apk的启动时间?” :

利用python或者直接用adb命令怎么计算apk的启动时间呢?就是计算从点击图标到apk完全启动所花费的时间。比如,对游戏来说就是点击游戏图标到进入到登录界面的这段时间。已知的两种方法貌似可以获取,但是感觉结果不准确:一种是,adb shell am start -w packagename/activity,这个可以得到两个值,ThisTime和TotalTime,不知道两个有什么区别,而且与实际启动时间不匹配,两者相加都可能比实际启动时间小(测试游戏的时候差别更大);另外一种是通过adb logcat的方式,感觉获取的结果也与实际有差别。

我和另外一个同事 启发 针对两个方面进行了回答,不过毕竟知乎上看的人会比较少,所以我在征得他的同意之后,将这两个答案整理了一下,记录到博客中,一来算是一个小的总结,之后自己看得时候比较方便,二来给需要的同学一个更加方便的途径。

1 应用启动场景

事实上 Android 中一个 App 的启动时间可以准确计算的.但是要分场景.也就是说要分开游戏和应用. 大家都知道,在Android中,游戏开发和应用开发是两码事.所以我们需要分开来说.

1.1 应用启动

我们平时在写应用的时候,一般会指定一个 mainActivity ,用户在桌面上点击这个 Activity 的时候,系统会直接起这个 Activity. 我们知道 Activity 在启动的时候会走 onCreate/onStart/onResume .这几个回调函数.

许多书里讲过,当执行完 onResume 函数之后,应用就显示出来了…其实这是一种不准确的说法,因为从系统层面来看,一个 Activity 走完 onCreate/onStart/onResume 这几个生命周期之后,只是完成了应用自身的一些配置,比如 window 的一些属性的设置/ View 树的建立(只是建立,并没有显示,也就是说只是调用了 inflate 而已) . 后面 ViewRootImpl 还会调用两次performTraversals ,初始化 Egl 以及 measure/layout/draw. 等.所以我们定义一个 Android 应用的启动时间, 肯定不能在 Activity 的回调函数上下手.而是以用户在手机屏幕上看到你在 onCreate 的 setContentView 中设置的 layout 完全显示为准,也就是我们常说的应用第一帧.

上面扯得有点远,不感兴趣的话可以不看,下面直接说方法.题主说的 adb shell am start -w packagename/activity,是可以完全应用的启动时间的.不过也要分场景.

1.2 应用第一次启动

也就是我们常说的冷启动,这时候你的应用程序的进程是没有创建的. 这也是大部分应用的使用场景.用户在桌面上点击你应用的 icon 之后,首先要创建进程,然后才启动 MainActivity.这时候adb shell am start -w packagename/MainActivity 返回的结果,就是标准的应用程序的启动时间(注意 Android 5.0 之前的手机是没有 WaitTime 这个值的):

➜ adb shell am start -W com.meizu.media.painter/com.meizu.media.painter.PainterMainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.meizu.media.painter/.PainterMainActivity }
Status: ok
Activity: com.meizu.media.painter/.PainterMainActivity
ThisTime: 355
TotalTime: 355
WaitTime: 365
Complete

总共返回了三个结果,我们以 WaitTime 为准.

关于ThisTime/TotalTime/WaitTime的区别,下面是其解释:

“adb shell am start -W ”的实现在 frameworks\base\cmds\am\src\com\android\commands\am\Am.java 文件中。其实就是跨Binder调用ActivityManagerService.startActivityAndWait() 接口(后面将ActivityManagerService简称为AMS),这个接口返回的结果包含上面打印的ThisTime、TotalTime时间.

  • startTime记录的刚准备调用startActivityAndWait()的时间点
  • endTime记录的是startActivityAndWait()函数调用返回的时间点
  • WaitTime = startActivityAndWait()调用耗时。

ThisTime、TotalTime 的计算在 frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java 文件的 reportLaunchTimeLocked() 函数中。

我们来解释下代码里curTime、displayStartTime、mLaunchStartTime三个时间变量.

  • curTime表示该函数调用的时间点.
  • displayStartTime表示一连串启动Activity中的最后一个Activity的启动时间点.
  • mLaunchStartTime表示一连串启动Activity中第一个Activity的启动时间点.

正常情况下点击桌面图标只启动一个有界面的 Activity,此时 displayStartTime 与mLaunchStartTime 便指向同一时间点,此时 ThisTime=TotalTime。另一种情况是点击桌面图标应用会先启动一个无界面的 Activity 做逻辑处理,接着又启动一个有界面的Activity,在这种启动一连串 Activity 的情况下(知乎的启动就是属于这种情况),displayStartTime 便指向最后一个 Activity 的开始启动时间点,mLaunchStartTime 指向第一个无界面Activity的开始启动时间点,此时 ThisTime!=TotalTime。这两种情况如下图:

在上面的图中,我用①②③分别标注了三个时间段,在这三个时间段内分别干了什么事呢?

  • 在第①个时间段内,AMS 创建 ActivityRecord 记录块和选择合理的 Task、将当前Resume 的 Activity 进行 pause
  • 在第②个时间段内,启动进程、调用无界面 Activity 的 onCreate() 等、 pause/finish 无界面的 Activity
  • 在第③个时间段内,调用有界面 Activity 的 onCreate、onResume

看到这里应该清楚 ThisTime、TotalTime、WaitTime 三个时间的关系了吧。WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

Event log中 TAG=am_activity_launch_time 中的两个值分表表示 ThisTime、TotalTime,跟通过 “adb shell am start -W ” 得到的值是一致的。

最后再说下系统根据什么来判断应用启动结束。我们知道应用启动包括进程启动、走 Activity生命周期 onCreate/onResume 等。在第一次 onResume 时添加窗口到WMS中,然后measure/layout/draw,窗口绘制完成后通知 WMS,WMS 在合适的时机控制界面开始显示(夹杂了界面切换动画逻辑)。记住是窗口界面显示出来后,WMS 才调用reportLaunchTimeLocked() 通知 AMS Activity 启动完成。

最后总结一下,如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。

1.2 应用非第一次启动

如果是你按Back键,并没有将应用进程杀掉的话,那么执行上述命令就会快一些,因为不用创建进程了,只需要启动一个Activity即可。这也就是我们说的应用热启动。

2 游戏启动场景

游戏启动的话,就不适用用命令行的方法来启动了,因为从用户点击桌面图标到登录界面,既有系统的部分也有游戏自己的部分。

2.1 系统部分

游戏也有一个Activity,所以启动的时候还是会去启动这个Activity,所以系统启动部分也就是用户点击桌面桌面响应到这个Activity启动。

2.2 游戏部分

一般游戏的主Activity启动后,还会做一些比较耗时的事情,这时候你看到的界面是不能操作的,比如:加载游戏数据、联网更新数据、读取和更新配置文件、游戏引擎初始化等操作。从游戏开发的角度来看,到了真正用户能操作的界面才算是一个游戏真正加载完成的时间。那么这个时间,就得使用Log来记录了,因为加载游戏数据、联网更新数据、读取和更新配置文件、游戏引擎初始化这些操作,都是游戏自己的逻辑,与系统无关,所以得由游戏自己定义加载完成的点。

对于游戏的启动时间,我们更倾向于计算从 点击桌面图标用户可以与游戏进行交互 这个时间段作为一个游戏的启动时间。

3 总结

计算机最让人着迷的一点就是其准确性,1+1永远等于2,启动耗时多久就是多久,每一次可能不一样,但每一次的时间都是这一次的准确时间。

不过每个公司由于对应用的定位不同,所以对应用启动的要求也不一样。比如有的做 ROM 的公司,其内置应用的启动时间一定是要非常快的,这样给用户的第一感觉就是快、流畅;互联网公司的 App 则不是很关心启动速度,大部分互联网公司的应用都有一个启动页,用来展示广告或者功能介绍之类的,然后才会进入到主界面。需求不一样,这么做也无可厚非,不过从消费者的角度来看,越早见到主界面当然越好。

所以在做一个 Android App 的时候,一定要记得将应用的启动时间作为一个性能指标,毕竟

天下武功,唯快不破!

<think>好的,用户现在想在自己的Android应用中实现统计应用使用时间的功能。根据之前的对话历史,用户可能已经了解了一些关于应用锁定的实现方案,现在转向另一个相关但不同的功能:使用时间统计。我需要结合用户提供的引用内容,特别是引用[2]和[3]提到的UsageStatsManager类,来给出具体的实现步骤。 首先,用户的需求是统计应用的使用时间,这和之前讨论的应用锁定不同,但都属于监控应用使用情况的范畴。需要确认用户是否清楚需要哪些权限,以及不同Android版本的兼容性问题。根据引用[2]和[3],UsageStatsManager是Android 5.0及以上版本推荐的方案,而更旧的版本可能需要其他方法,比如ActivityManager。 接下来,我需要考虑用户可能的开发经验。用户可能已经具备一定的Android开发基础,所以需要提供具体的代码示例,比如Kotlin或Java的实现。同时,用户可能关心如何获取准确的数据,比如前台使用时间和后台时间的区分,以及如何处理权限请求,特别是PACKAGE_USAGE_STATS权限,这个权限需要用户手动授权,不能通过常规的运行时权限请求。 另外,用户提到的引用[4]提到了应用启动时间统计,这可能与应用使用时间统计相关,但用户的主要需求是总使用时间,所以需要明确区分。应用启动时间可能指的是冷启动或热启动时间,而使用时间统计更关注应用在前台的时间段总和。 需要提醒用户注意权限申请的步骤,因为使用UsageStatsManager需要用户手动在设置中开启权限,这可能会影响用户体验,需要适当引导用户。同时,数据查询的时间范围选择也很重要,比如按天、周或自定义区间,这会影响如何聚合数据。 此外,数据准确性方面,不同的Android版本可能有不同的限制,比如某些版本可能无法精确到毫秒,或者有后台限制导致数据不更新。需要建议用户进行充分的测试,尤其是跨版本测试,确保功能的稳定性。 最后,用户可能想知道如何展示这些数据,比如在应用内生成报告或图表,但当前问题可能集中在如何获取数据上,所以需要先解决数据获取的问题,再考虑展示方式。同时,需要考虑到电量优化和后台服务的限制,避免因为频繁查询而影响设备性能。 总结步骤应该是:检查权限,使用UsageStatsManager查询使用情况,处理返回的数据,计算总时间,并处理兼容性和权限问题。同时提供代码示例和注意事项,确保用户能够顺利实现功能。</think>#### 一、核心实现方案(基于Android 5.0+) 1. **关键技术类** - 使用`UsageStatsManager`获取应用使用数据(Android 5.0+)[^2][^3] - 需要声明权限:`<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>` 2. **实现步骤** ```kotlin // 1. 检查权限是否授予 fun checkUsageStatsPermission(context: Context): Boolean { val appInfo = context.packageManager.getApplicationInfo(context.packageName, 0) val enabled = (context.getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager) .queryUsageStats(UsageStatsManager.INTERVAL_DAILY, System.currentTimeMillis() - 1000 * 60 * 60 * 24, System.currentTimeMillis()) return enabled.isNotEmpty() } // 2. 获取应用使用数据 fun getAppUsageStats(context: Context, packageName: String): Long { val usageStatsManager = context.getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager val endTime = System.currentTimeMillis() val startTime = endTime - 1000 * 60 * 60 * 24 * 7 // 查询最近7天 val stats = usageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_BEST, startTime, endTime ) var totalTime = 0L stats?.forEach { usageStats -> if (usageStats.packageName == packageName) { totalTime += usageStats.totalTimeInForeground } } return totalTime // 返回毫秒级使用时间 } ``` #### 二、关键问题处理 1. **权限申请流程** ```xml <!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> ``` ```kotlin // 跳转到授权页面 val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) startActivity(intent) ``` 2. **数据精度优化** - 前台时间计算:`totalTimeInForeground`字段包含应用在前台的总时间(包括多任务场景)[^3] - 跨版本兼容:Android 7.0+需注意`INTERVAL_BEST`参数的行为变化 - 时间区间建议:`INTERVAL_DAILY`(按天聚合)或`INTERVAL_WEEKLY` #### 三、兼容性处理 | Android版本 | 实现方案差异 | |------------|--------------| | 4.4及以下 | 使用`ActivityManager.getRecentTasks()`(已废弃) | | 5.0-6.0 | 需处理`UsageStatsManager`的延迟数据更新(最长2分钟)[^2] | | 7.0+ | 需动态申请`PACKAGE_USAGE_STATS`权限 | #### 四、高级功能扩展 1. **实时监控实现** ```kotlin // 注册使用事件监听器 usageStatsManager.registerUsageStatsObserver( UsageStatsManager.INTERVAL_DAILY, object : UsageStatsManager.UsageStatsCallback { override fun onStatsUpdated(usageStats: UsageEvents) { // 实时处理事件流 } } ) ``` 2. **后台服务保活方案** ```kotlin // 使用WorkManager定期同步数据 PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS).build().also { WorkManager.getInstance(context).enqueue(it) } ``` #### 五、数据准确性验证 1. **调试方法** ```bash adb shell dumpsys usagestats adb shell dumpsys batterystats --enable full-wake-history ``` 2. **常见误差场景** - 系统休眠期间的时间计算误差 - 分屏模式下的时间分配问题 - 系统预装应用的豁免规则
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值