
web-to-app 使用统计与 URL 健康监测实战指南本地会话追踪与网站健康检测全解析本文围绕 web-to-app 的「使用统计」功能展开该功能从主界面「⋮ → 使用统计」入口见 更多打开包含「使用统计」与「URL 健康」两个标签页分别回答我的应用都用得怎么样和应用指向的网站还活着吗两个问题。读完本篇你将理解统计数据的本地采集链路启动、会话暂停/恢复、关闭如何落库、汇总指标的 SQL 计算方式以及 URL 健康检测的状态判定规则、超时与慢响应阈值、批量并发策略和记录清理机制。一、功能入口与整体结构从源码看「使用统计」页面由 StatsScreen.kt 实现是一个双 Tab 界面使用统计 TabUsageStatsTab顶部是汇总卡片总启动数、总使用时长、活跃应用数、平均会话时长下方是可搜索、可排序的应用排行列表URL 健康 TabHealthMonitorTab顶部是四色状态概览在线 / 慢 / 离线 / 未知计数右上角「检查」按钮触发全部检查下方逐应用展示状态徽章、响应时间与 HTTP 状态码列表按「离线优先」排序方便先处理异常应用。数据层由三个核心类协作DI 装配见 AppModule.kt类职责AppUsageTracker.kt会话生命周期采集启动、暂停、恢复、关闭AppStatsRepository.kt读写仓库记录启动与时长、汇总指标、健康记录存取AppHealthMonitor.ktURL 健康检测单检、批量检查、后台定时监控两者均持久化到 Room 数据库表结构定义在 AppUsageStats.kt。二、使用统计指标定义与数据模型2.1 界面上的六个核心能力对照 使用统计文档界面能力与源码的对应关系如下启动次数—— 总计和各应用的启动次数。总计来自SUM(launchCount)每应用数值直接读自统计行活跃应用—— 有多少应用被使用过。对应 DAO 查询COUNT(*) FROM app_usage_stats WHERE launchCount 0平均会话与上次会话—— 会话时长指标。其中平均会话 总使用时长 ÷ 总启动数AppStatsRepository.getOverallStats()中avg usage / launches上次会话时长读自lastSessionDurationMs排行—— 按启动次数或最近使用排名列表带名次序号与相对最大值的比例进度条排序—— 实际实现提供三个排序档位StatsSortMode枚举按启动次数LAUNCHES、按使用时长TIME、按最近使用RECENT清除全部—— 重置统计触发WtaAlertDialog确认后调用clearAllStats()成功后弹出 Snackbar 提示。2.2 统计表结构统计持久化为两张 Room 实体表均在 AppUsageStats.kt 中声明Entity(tableName app_usage_stats, indices [ Index(value [appId], unique true), Index(value [lastUsedAt]), Index(value [launchCount]) ], foreignKeys [ ForeignKey(entity WebApp::class, parentColumns [id], childColumns [appId], onDelete ForeignKey.CASCADE) ]) data class AppUsageStats( PrimaryKey(autoGenerate true) val id: Long 0, val appId: Long, val launchCount: Int 0, // 启动次数 val totalUsageMs: Long 0, // 累计使用时长 val lastUsedAt: Long 0, // 最近使用时间戳 val lastSessionDurationMs: Long 0, // 上次会话时长 val createdAt: Long System.currentTimeMillis() )几个值得注意的设计点appId建唯一索引保证一个应用一行统计增量更新走UPDATE而非删插外键指向WebApponDelete CASCADE—— 删除应用时统计行自动级联清除不会产生孤儿数据lastUsedAt与launchCount单独建索引直接支撑「最近使用」排序和排行查询。健康记录表app_health_records同样定义在该文件字段包括url、status枚举UNKNOWN / ONLINE / SLOW / OFFLINE由 AppUsageStatsTest.kt 中的用例断言了这四个取值、responseTimeMs、httpStatusCode、errorMessage、checkedAt。三、会话是怎么被追踪下来的3.1 采集链路WebViewActivity → AppUsageTracker从源码结构看埋点接在 WebViewActivity.kt 的生命周期上文件约 L784-L787、L1126-L1134、L1202-L1204生命周期调用行为创建并确定目标应用trackLaunch(appId)记录会话开始时间异步repository.recordLaunch(appId)累加启动次数onPausetrackPause(appId)若本次片段 ≥ 1 秒则落库累计时长然后重置开始时间为当前时刻onResumetrackResume(appId)刷新开始时间为当前时刻onDestroytrackClose(appId)移除会话若最后一段 ≥ 1 秒则落库时长核心实现在 AppUsageTracker.kt用一个ConcurrentHashMapLong, Long保存各应用的会话起始时间戳数据库写入全部通过Dispatchers.IO协程异步执行不阻塞 UI 线程。一个容易忽略的细节时长落库有 1000ms 的最低门槛if (duration 1000)。快速打开又秒退、或前后台切换不足一秒的会话不会被计入使用时长避免碎片化事件把总时长污染成噪声但启动次数不受该门槛影响只要trackLaunch触发就 1。3.2 落库的增量 SQLAppUsageStatsDao.kt 中的两条增量更新是统计正确性的关键——计数器是「加上去」的不是读改写-- 启动 1同时刷新最近使用时间 UPDATE app_usage_stats SET launchCount launchCount 1, lastUsedAt :timestamp WHERE appId :appId -- 累加时长并记录本次会话时长 UPDATE app_usage_stats SET totalUsageMs totalUsageMs :durationMs, lastSessionDurationMs :durationMs, lastUsedAt :timestamp WHERE appId :appIdrecordLaunch与recordUsageDuration见 AppStatsRepository.kt都带「查无则插、有则增量」的兜底逻辑且异常只记日志不上抛统计失败不会影响应用主流程。3.3 汇总指标与时间格式化页面顶部的汇总卡片数据来自getOverallStats()val launches dao.getTotalLaunchCount() // COALESCE(SUM(launchCount), 0) val usage dao.getTotalUsageMs() // COALESCE(SUM(totalUsageMs), 0) val active dao.getActiveAppCount() // COUNT(*) WHERE launchCount 0 val avg if (launches 0) usage / launches else 0L注意「平均会话」在实现上是总时长 ÷ 总启动数即「每次启动平均使用多久」这与 UI 中stats.avgSession的展示口径一致。时长的展示由StatsFormat同在 AppStatsRepository.kt完成格式化规则有单元测试背书AppUsageStatsTest.kt输入时长展示结果30 秒、0 毫秒国际化文案「不足 1 分钟」25 分钟25m90 分钟1h 30m10 小时10h 0m最近使用时间的相对时间格式为刚刚 / N 分钟前 / N 小时前 / N 天前 / N 个月前formatRelativetimestamp 0时显示「从未使用」。四、URL 健康状态判定与检测参数4.1 状态机ONLINE / SLOW / OFFLINE / UNKNOWN文档描述的四态在线 / 慢 / 离线 / 未知在 AppHealthMonitor.kt 中由一次 HTTP 请求的状态码 响应耗时共同决定val status when { statusCode in 200..399 responseTime SLOW_THRESHOLD_MS - HealthStatus.ONLINE statusCode in 200..399 - HealthStatus.SLOW else - HealthStatus.OFFLINE }判定规则与全部检测参数均为该类companion object常量参数值含义SLOW_THRESHOLD_MS2000 ms响应超过 2 秒判为「慢」TIMEOUT_MS5000 msOkHttp 连接/读超时均为 5 秒CHECK_INTERVAL_MS30 * 60 * 1000 ms后台定时监控周期请求方式HEAD只取响应头User-Agent为Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36跟随 HTTP/HTTPS 重定向边界情况也都有记录语义任何异常DNS 失败、超时、证书错误等都会写入一条OFFLINE记录responseTimeMs -1errorMessage保存异常信息——这就是健康卡片上会显示红色错误文案的来源。UNKNOWN则是「从未检查过」的初始态记录默认值见测试断言AppHealthRecord default status is UNKNOWN。4.2 单检、批量检查与后台监控单检健康 Tab 中点击任意应用卡片触发checkUrl(appId, url)结果实时写入app_health_records批量检查右上角「检查」按钮走checkApps(apps)。两个过滤条件值得注意——只检查appType AppType.WEB且url以http开头的应用即本地项目类应用不参与 URL 健康并发度用Semaphore(5)限制为 5 路。批量结束后会顺带调用cleanupOldHealthRecords()后台监控startMonitoring(appsFlow)以 30 分钟为周期循环拉取应用列表并批量检查stopMonitoring()/destroy()可取消。从 DI 装配看它是全局单例AppHealthMonitor.getInstance(...)可以推断其生命周期与应用进程一致。4.3 记录清理与在线率健康记录是「只增历史」的但有两条内置回收策略AppStatsRepository.ktcleanupOldHealthRecords()删除checkedAt早于7 天前的记录控制表体积getUptimePercent(appId)以近 24 小时为窗口计算在线率SQL 中把ONLINE与SLOW都计为「可用」占比即为在线率AppHealthSummary.uptimePercent的来源。「某应用最新一条记录」的取法用了一个经典子查询 JOINAppUsageStatsDao.ktgetAllLatestHealthRecords先GROUP BY appId求MAX(checkedAt)再回表连接取整行保证健康 Tab 概览与列表读取的是每个应用的最近一次检测结果。4.4 健康状态与应用卡片的联动文档中提到「同样的状态会以健康状态点显示在应用卡片上」见 应用列表。从实现链路看卡片数据源与统计页共享同一批Flowrepository.getAllLatestHealthRecords()是响应式流任何一次检查落库后应用列表上的状态点与统计页健康 Tab 会同步刷新无需手动重载。五、数据本地性说明文档最后一句「统计在设备上本地收集」在源码层面完全成立两张表都落在应用私有 Room 数据库中AppHealthMonitor的 HEAD 探测是设备直发 HTTP 请求没有任何统计数据的上传通道。这意味着两点使用上的含义隐私上使用次数、会话时长、健康历史完全离线卸载或「清除全部」即彻底清除行为上「清除全部」只清空app_usage_stats对应 DAO 的deleteAllStats()健康记录另有 7 天自动清理机制二者生命周期相互独立。六、小结一张表读懂实现映射界面能力实现位置关键依据启动次数统计WebViewActivity.kt → AppUsageTracker.kttrackLaunch生命周期埋点 launchCount 1增量 SQL会话时长平均/上次AppUsageTracker.ktpause/resume/close 三段式计时≥1s 才落库排行与三档排序StatsScreen.ktStatsSortMode.LAUNCHES / TIME / RECENT清除全部带确认StatsScreen.ktWtaAlertDialog确认 →clearAllStats()URL 健康四态AppHealthMonitor.kt200–399 2s → ONLINE≥2s → SLOW其余/异常 → OFFLINE批量检查与定时监控AppHealthMonitor.kt5 路并发、30 分钟周期、仅检 WEB 型 http URL在线率与 7 天清理AppStatsRepository.kt24h 窗口 SQL 占比cleanupOldRecords(7天前)格式化与状态断言AppUsageStatsTest.kt时长/相对时间/HealthStatus取值的单元测试整体来看web-to-app 的使用统计是一个「采集Tracker—存储Room 增量 SQL—展示Compose Flow 响应式—检测OkHttp HEAD 阈值判定」闭环清晰的本地化统计模块统计与 URL 健康各自独立成表又通过同一套 RoomFlow在统计页、应用卡片之间共享最新状态且全部数据不出设备。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考