Android应用使用时长统计:基于UsageStatsManager的完整实现指南

发布时间:2026/8/1 1:54:45

Android应用使用时长统计:基于UsageStatsManager的完整实现指南 1. 项目概述为什么我们需要统计应用使用时长在Android开发中尤其是涉及数字健康、家长控制、应用分析或设备管理类的项目时一个非常核心的需求就是精确地知道用户在各个应用上花费了多少时间以及打开了多少次。这听起来简单但背后涉及到系统权限、数据聚合、后台服务等一系列复杂问题。你可能想做一个类似“屏幕时间”的功能或者在你的App里加入一个“今日使用报告”的小组件。直接的想法可能是去监听应用的前后台切换但这种方式既不准确无法统计前台服务时间也容易被系统优化掉。这时Android系统自带的UsageStatsManager服务就成了我们的“官方答案”。它就像一个系统级的“时间记录员”由系统内核统一收集所有应用的活动数据我们只需要通过合适的权限去查询它整理好的报告。相比于自己“造轮子”使用UsageStatsManager不仅数据更权威、更省电也避免了因滥用后台监听而被系统限制的风险。今天我就结合自己多次在数字健康类App中的实战经验带你彻底搞懂UsageStatsManager从权限申请、数据查询到数据处理手把手实现一个可靠的应用使用时长与次数统计模块。2. 核心权限与配置跨过第一道门槛在开始写代码之前我们必须先搞定权限和配置。这是很多新手最容易栽跟头的地方因为相关的配置项比较隐蔽且在不同Android版本上行为差异很大。2.1 必不可少的PACKAGE_USAGE_STATS权限UsageStatsManager查询的数据属于高敏感信息因此Android系统要求应用必须拥有android.permission.PACKAGE_USAGE_STATS权限。这个权限比较特殊它属于“系统签名或用户授权”权限。这意味着什么普通应用无法通过在AndroidManifest.xml里简单声明就自动获得此权限。用户必须主动进入系统的“设置” - “安全与隐私” - “特殊应用权限” - “使用情况访问”不同厂商手机路径可能略有不同如“应用使用情况”页面手动为你的应用开启开关。在代码中引导用户授权我们不能假设用户已经打开了权限。因此在尝试查询数据前必须检查权限状态并引导用户去设置页面。以下是标准的检查与引导流程fun checkUsageStatsPermission(context: Context): Boolean { val appOps context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager val mode appOps.checkOpNoThrow( AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.packageName ) return mode AppOpsManager.MODE_ALLOWED } fun requestUsageStatsPermission(activity: Activity) { if (!checkUsageStatsPermission(activity)) { // 构建一个明确的提示对话框 AlertDialog.Builder(activity) .setTitle(需要「使用情况访问」权限) .setMessage(此功能需要获取应用使用统计信息以准确计算使用时长。请点击「去开启」然后在系统设置中找到本应用并打开开关。) .setPositiveButton(去开启) { _, _ - // 跳转到系统设置页面 val intent Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS) activity.startActivity(intent) } .setNegativeButton(取消, null) .show() } }注意在Android 11API 30及以上版本即使拥有PACKAGE_USAGE_STATS权限应用也只能查询到自身以及其他可见应用的使用情况。所谓“可见应用”通常指那些在Launcher中有图标的应用。一些系统核心进程或完全隐藏的应用可能不会出现在查询结果中这是出于隐私保护的设计你的代码需要能处理这种数据不完整的情况。2.2 AndroidManifest.xml 中的权限声明尽管需要用户手动授权但在AndroidManifest.xml文件中声明该权限仍然是必须的否则系统根本不会把你的应用列入“使用情况访问”的可选列表里。manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.yourapp uses-permission android:nameandroid.permission.PACKAGE_USAGE_STATS / application ... ... /application /manifest实操心得在实际项目中我强烈建议将权限检查封装成一个独立的工具类并在App主页面或需要使用该功能的页面入口处进行校验。弹窗提示的文案要尽可能清晰直接告诉用户需要去“系统设置”里操作因为很多用户并不清楚“使用情况访问”这个系统级权限入口在哪里。测试时务必在真机上反复测试权限开启和关闭的流程模拟用户可能遇到的所有情况。3. UsageStatsManager 核心API详解拿到权限后我们就可以和UsageStatsManager打交道了。首先通过Context.getSystemService(Context.USAGE_STATS_SERVICE)获取它的实例。UsageStatsManager提供了几种主要的查询方法我们需要根据不同的场景选择使用。3.1 查询指定时间区间内的使用统计queryUsageStats这是最常用、最核心的方法。它返回一个ListUsageStats包含了在给定时间区间内所有有活动的应用的数据快照。val usageStatsManager getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager // 定义查询的时间区间查询过去24小时的数据 val calendar Calendar.getInstance() val endTime calendar.timeInMillis calendar.add(Calendar.DAY_OF_YEAR, -1) val startTime calendar.timeInMillis // 执行查询 val stats: ListUsageStats usageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_DAILY, // 时间间隔类型用于系统内部优化聚合 startTime, endTime )关键参数解析intervalType(时间间隔类型)INTERVAL_DAILY: 日级聚合。适合查询最近几天的数据系统可能为此优化了存储。INTERVAL_WEEKLY: 周级聚合。INTERVAL_MONTHLY: 月级聚合。INTERVAL_YEARLY: 年级聚合。INTERVAL_BEST: 系统根据你提供的startTime和endTime自动选择最合适的聚合区间。在大多数情况下推荐使用INTERVAL_BEST让系统帮你做最优选择避免因为区间类型不匹配导致查询不到数据或数据不准确。startTime和endTime查询的起止时间戳毫秒。需要注意的是UsageStatsManager的数据保留时间是有限的。通常系统只会保留最近几天例如7天的详细使用记录更早的数据可能已被聚合或清除。如果你查询一个非常早的时间段返回的列表可能为空或数据不全。返回的UsageStats对象包含哪些信息这是数据处理的源头务必理解每个字段的含义packageName: 应用包名如com.tencent.mm。firstTimeStamp/lastTimeStamp: 该应用在查询区间内首次和末次被使用的时间戳。lastTimeUsed: 该应用最后一次被使用切换到前台的时间戳。totalTimeInForeground:核心字段。该应用在查询区间内处于前台的总时间毫秒。这就是我们计算“使用时长”的依据。mLaunchCount(通过getLaunchCount()方法获取):核心字段。该应用在查询区间内被启动带到前台的次数。这就是“使用次数”。3.2 查询聚合后的使用情况queryAndAggregateUsageStats如果你不关心每个独立的时间片段只想要整个查询区间内的汇总数据这个方法更高效。它返回一个MapString, UsageStats以包名为 Key对应的UsageStats中的totalTimeInForeground和mLaunchCount已经是整个区间的累加值。val aggregatedStats: MapString, UsageStats usageStatsManager.queryAndAggregateUsageStats( startTime, endTime ) // 直接获取微信的总使用时间 val weChatStats aggregatedStats[com.tencent.mm] val weChatUsageTime weChatStats?.totalTimeInForeground ?: 0L3.3 查询实时事件流queryEvents对于需要实时监控应用使用行为如实现“应用使用限制”一到时间就锁屏的高级场景可以使用queryEvents。它返回一个UsageEvents对象其中包含了一系列UsageEvents.Event。每个事件代表一个状态变化比如MOVE_TO_FOREGROUND(应用切换到前台) 和MOVE_TO_BACKGROUND(应用退到后台)。val events: UsageEvents usageStatsManager.queryEvents(startTime, endTime) val event UsageEvents.Event() while (events.hasNextEvent()) { events.getNextEvent(event) when (event.eventType) { UsageEvents.Event.MOVE_TO_FOREGROUND - { Log.d(Usage, ${event.packageName} 进入前台 at ${Date(event.timeStamp)}) } UsageEvents.Event.MOVE_TO_BACKGROUND - { Log.d(Usage, ${event.packageName} 退到后台 at ${Date(event.timeStamp)}) } } }注意事项queryEvents会产生更细粒度的数据但数据量也大得多处理起来更复杂且对性能有一定影响。除非有实时响应的需求否则对于简单的时长统计使用queryUsageStats或queryAndAggregateUsageStats就足够了。4. 从原始数据到业务数据数据处理全流程拿到ListUsageStats只是第一步里面的数据是原始的、按包名分列的。我们需要将其转换成用户可读的、按应用归集的今日/本周使用报告。这个过程涉及到数据过滤、聚合、排序和格式化。4.1 数据清洗与过滤查询返回的列表中可能包含一些我们不需要的条目系统组件或包名异常的应用有些包名以android、com.android、system开头或者没有对应实际应用。总使用时间为0的应用这些应用可能只是被系统唤醒过但用户并未主动使用。自身的应用如果你不想统计自己可以过滤掉。fun processUsageStats(rawStats: ListUsageStats): ListUsageStats { return rawStats.filter { stats - // 过滤掉使用时间为0的项 stats.totalTimeInForeground 0 }.filterNot { stats - // 过滤掉一些常见的系统包可根据需要扩充列表 stats.packageName.startsWith(android) || stats.packageName.startsWith(com.android) || stats.packageName.startsWith(system) || stats.packageName packageName // 过滤自身 } }4.2 关键指标计算时长、次数与占比对于过滤后的列表我们可以开始计算核心指标。data class AppUsageInfo( val packageName: String, val appName: String, // 需要通过包名解析出应用名 val totalTimeMs: Long, // 总使用时长毫秒 val launchCount: Int, // 启动次数 val percentage: Float // 占总使用时间的百分比 ) fun calculateUsageInfo(processedStats: ListUsageStats, context: Context): ListAppUsageInfo { // 1. 计算所有应用的总使用时间 val totalUsageTimeMs processedStats.sumOf { it.totalTimeInForeground } // 2. 转换并计算百分比 return processedStats.map { stats - val appName resolveAppName(context, stats.packageName) val usageTimeMs stats.totalTimeInForeground val percentage if (totalUsageTimeMs 0) { (usageTimeMs.toFloat() / totalUsageTimeMs.toFloat()) * 100 } else { 0f } AppUsageInfo( packageName stats.packageName, appName appName, totalTimeMs usageTimeMs, launchCount stats.getLaunchCount(), percentage percentage ) }.sortedByDescending { it.totalTimeMs } // 按使用时长降序排序 } /** * 根据包名获取应用名称 */ private fun resolveAppName(context: Context, packageName: String): String { return try { val pm context.packageManager val appInfo pm.getApplicationInfo(packageName, 0) pm.getApplicationLabel(appInfo).toString() } catch (e: PackageManager.NameNotFoundException) { // 如果应用已卸载或找不到则返回包名 packageName } }4.3 时间格式化与展示用户看不懂毫秒我们需要将其转换成“X小时Y分钟”或“X分Y秒”的友好格式。fun formatDuration(milliseconds: Long): String { val seconds milliseconds / 1000 val hours seconds / 3600 val minutes (seconds % 3600) / 60 val remainingSeconds seconds % 60 return when { hours 0 - String.format(%d小时%d分钟, hours, minutes) minutes 0 - String.format(%d分钟%d秒, minutes, remainingSeconds) else - String.format(%d秒, remainingSeconds) } } // 在UI中展示 val usageInfo: AppUsageInfo ... textViewTime.text formatDuration(usageInfo.totalTimeMs) textViewPercentage.text String.format(%.1f%%, usageInfo.percentage)实操心得数据处理部分最容易出性能问题的地方在于resolveAppName。如果列表中有几十个应用每个都去查询PackageManager会阻塞主线程。务必在子线程如使用viewModelScope.launch(Dispatchers.IO)中执行整个数据处理流程然后将最终结果post到主线程更新UI。可以考虑增加一个简单的内存缓存MapString, String将包名与应用名的映射关系缓存起来避免重复查询。5. 构建一个完整的后台统计服务对于需要持续记录、即使App退到后台也要工作的场景如完整的屏幕时间统计我们需要一个Service。但要注意Android系统对后台服务的限制越来越严格。5.1 使用前台服务 (Foreground Service)从Android 8.0 (API 26) 开始如果服务需要在后台长时间运行必须启动为前台服务并显示一个无法被清除的通知。AndroidManifest.xml 中声明权限和服务uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / service android:name.UsageStatsCollectorService android:enabledtrue android:exportedfalse android:foregroundServiceTypedataSync / !-- 根据实际用途选择type如 dataSync, location等 --在Service中启动为前台服务class UsageStatsCollectorService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 创建通知渠道Android 8.0 必需 createNotificationChannel() // 构建通知 val notification NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(使用情况统计中) .setContentText(正在后台收集应用使用数据) .setSmallIcon(R.drawable.ic_stat_icon) .setPriority(NotificationCompat.PRIORITY_LOW) .build() // 启动为前台服务 startForeground(NOTIFICATION_ID, notification) // 开始你的定时统计任务... startPeriodicCollection() return START_STICKY } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val channel NotificationChannel( CHANNEL_ID, 使用统计服务, NotificationManager.IMPORTANCE_LOW ).apply { description 用于后台收集应用使用时长数据 } val manager getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } // ... 其他代码 }5.2 实现定时查询策略在服务中我们不能无限循环查询这太耗电。通常采用以下两种策略之一WorkManager定时任务这是最推荐的方式。WorkManager能保证任务被执行同时会考虑系统的省电策略。你可以设置一个周期性任务例如每15分钟或每小时执行一次数据查询和保存。// 在Application或主Activity中初始化一次性的周期性任务 val periodicWorkRequest PeriodicWorkRequestBuilderUsageStatsWorker( repeatInterval 15, // 间隔周期 TimeUnit.MINUTES ).setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.NOT_REQUIRED) .setRequiresBatteryNotLow(false) // 根据需求设置 .build() ).build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( usage_stats_collection, ExistingPeriodicWorkPolicy.KEEP, // 如果已有任务则保留旧的 periodicWorkRequest )UsageStatsWorker是一个继承自Worker的类在doWork()方法中执行查询和保存数据的逻辑。AlarmManagerBroadcastReceiver这是一种更传统、更“准时”但也更耗电的方式。可以设置一个精确的重复闹钟在指定时间唤醒设备执行任务。在Android 6.0之后为了省电AlarmManager的定时可能不精确。除非对实时性要求极高否则优先选择WorkManager。数据存储查询到的数据需要持久化。根据数据量大小可以选择Room数据库适合存储大量历史记录、SharedPreferences适合存储简单的聚合数据如今日总计或文件存储。建议设计一个数据表包含字段id,packageName,date(日期如20231015),duration(时长),launchCount(次数)。6. 常见问题、兼容性处理与避坑指南在实际开发中你会遇到各种各样的问题。下面是我踩过坑后总结出来的经验。6.1 查询结果为空或数据不准确问题调用queryUsageStats后返回的列表是空的或者totalTimeInForeground为0。排查步骤首先确认权限再次用checkUsageStatsPermission检查确保用户真的已经授权。这是最常见的原因。检查时间区间确保endTime大于startTime。查询一个未来的时间区间会返回空。检查intervalType如果你查询过去7天的数据却使用了INTERVAL_DAILY可能无法获取完整数据。始终优先使用INTERVAL_BEST。系统限制在Android 11上你的应用可能无法看到某些应用的记录。这是正常行为你的UI需要能优雅地处理“未知应用”或数据缺失的情况。设备休眠与统计延迟系统对使用情况的统计可能存在数分钟到数小时的延迟特别是在设备长时间休眠后。对于“今日”数据的实时性不要期望达到秒级。6.2 不同Android版本的兼容性问题Android 版本关键变化与注意事项5.0 (API 21)引入了UsageStatsManager。基础功能可用。7.0 (API 24)无重大API变化但后台优化更严格。8.0 (API 26)后台执行限制。如需长时间后台统计必须使用前台服务。9.0 (API 28)对queryEvents获取的数据增加了更多限制。10.0 (API 29)引入了android:foregroundServiceType属性启动前台服务时必须指定类型。11.0 (API 30)重大变更PACKAGE_USAGE_STATS权限变为“部分限制”。应用只能看到“可见应用”的使用情况。需要在AndroidManifest.xml中声明queries标签来指定你想查询的其他应用如果知道包名但这对于统计所有应用来说不现实。通常只能接受这个限制。12.0 (API 31)前台服务启动限制更严格。精确的闹钟 (AlarmManager.setExact) 需要新的权限SCHEDULE_EXACT_ALARM。强烈建议迁移到WorkManager。兼容性编码建议对于Android 11的可见性限制我们在获取应用名时要做降级处理private fun resolveAppName(context: Context, packageName: String): String { return if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { // Android 11尝试获取可能失败 try { val pm context.packageManager val appInfo pm.getApplicationInfo(packageName, 0) pm.getApplicationLabel(appInfo).toString() } catch (e: Exception) { // 获取失败可能是不可见应用返回包名或一个默认名称 packageName } } else { // 旧版本正常获取 // ... 正常逻辑 } }6.3 电量与性能优化频繁查询UsageStatsManager比如每秒一次是极其耗电且不必要的。遵循以下原则降低频率对于数据展示类功能每分钟甚至每5分钟查询一次都足够了。对于后台统计使用WorkManager设置合理的间隔如15分钟。批量处理一次性查询一天的数据而不是分多次查询。避免在主线程操作所有查询和数据处理都必须在子线程进行。及时注销监听器如果你使用了UsageStatsManager的事件回调某些定制ROM可能有在组件销毁时务必注销。6.4 数据隐私与合规性应用使用数据是高度敏感的个人信息。在你的应用中明确告知用户在申请权限前和隐私政策中清晰说明你收集哪些数据、为何收集、如何存储以及如何使用。本地处理优先尽量在用户设备本地完成数据计算和存储避免不必要的网络上传。提供数据清除选项允许用户清除你收集的所有使用统计数据。遵守相关法律法规如GDPR、CCPA等确保你的数据处理流程合规。最后测试环节至关重要。你需要在不同品牌、不同系统版本的手机上进行测试特别是权限授予流程和后台数据收集的稳定性。华为、小米、OPPO、vivo等国内厂商的定制系统可能会对后台服务和自启动有额外的限制需要在对应的“电池优化”、“自启动管理”设置中引导用户将你的应用加入白名单否则定时任务可能无法正常运行。这部分引导逻辑虽然繁琐但对于提升功能的可靠性至关重要。

相关新闻