Android 后台保活的实现
Table of Contents
饱受期末忍不住刷手机的困扰,我决定开发一个自律应用 Pebble 。
核心功能是监测用户当前是否打开“黑名单 App”,
如果打开,会在屏幕顶部落下“石头”遮挡用户视线。
想实现这个监测功能,最大的难点就是保证应用要常驻后台以检测用户行为。
我尝试了多种方法,最后用一个取巧的方式 绕开 了 MIUI 的后台监测。
无障碍服务
因为之前在开发中接触过无障碍服务的接口,我在一开始选择了 AccessibilityService 方案。
这个方案理论上是可行的:系统在监测到应用切换后主动推送 AccessibilityEvent 给挂在后台的应用。
但方案在实际开发中遇到了无法解决的 MIUI 制裁:
- 事件积压与爆发:MIUI 为了优化前台流畅度,经常挂起后台的无障碍事件分发。这直接导致了我在测试中遇到的问题:手机切入抖音没有任何反应,等回到 Pebble 时,系统瞬间吐出大量
Switch日志。 - SystemUI 干扰:唤起多任务界面时会打断“当前应用”的判定,状态切回
home,致使用户逃脱黑名单管辖。 - 热启动漏测:从多任务界面快速切换时,部分机型不发送
WINDOW_STATE_CHANGED事件。
为了修补这些问题,我尝试了“主动查询 rootInActiveWindow”、“心跳轮询补漏”、“防抖动逻辑”,代码越写越复杂,但这些问题没有得到丝毫解决。
UsageStats 与“消失的轮询”
补丁越打越多,问题却得不到解决,只能选择重构所有代码。
方案切换
放弃被动的无障碍服务,改用 UsageStatsManager (使用情况统计) 配合 前台服务 (Foreground Service) 进行每秒一次的主动轮询。
- 数据源:使用
queryEvents()查询实时的系统动作流,避开queryUsageStats的延迟。 - 轮询机制:在 Service 中启动一个协程或 Handler,每 400ms~1000ms 检查一次。
诡异的 Bug
在 USB 连接调试时,一切都可以正常运行。但是一旦拔掉 USB 线,使用 Release 包安装测试,事件积压(后台冻结)又发生了:
- 只要 Pebble 在前台,检测正常。
- 一旦 Pebble 切到后台,轮询停止。
- 切回 Pebble,日志才突然大量跳出。
原因分析:这是 MIUI 等国产 ROM 激进的省电策略:USB 调试模式下系统会豁免进程冻结,但在 Release 模式下,即便我有前台服务通知,系统依然判定我“无用户交互”,直接挂起我的进程,监测中止。
像素保活
所以经过一系列的尝试,发现最终的实现手段应该是保持应用的活跃状态,让系统认为我正在与用户交互,从而规避冻结。
经过查询,发现了开发者的常用手段:在屏幕上始终保留一个 View 视图。
只要 WindowManager 认为我在画图,我就属于“图形处理前台进程”,可以一直保持高优先级,避免被灭活。
实现细节
不再把平时状态的 View 设为 View.GONE,这样系统知道我只是在后台静默,依旧惨遭冻结毒手。
我们对视图的要求应该是用户不可见,但系统可感知,即透明化处理。
修改 OverlayManager,定义两种状态:
- 显示态:正常的落石画面。
- 隐藏态 (保活):一个 1x1 像素、透明度极低、不可点击的 View。
// 隐藏态 (锚点) 参数配置
hiddenParams = WindowManager.LayoutParams(
1, // 宽 1px
1, // 高 1px
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_PHONE,
// 关键 flag:不可触摸、不可获取焦点
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.LEFT
// 关键 trick:不能是 0 (全透明会被优化),0.01 是系统认为可见的最小值
alpha = 0.01f
x = 0
y = 0
}
配合 HandlerThread
除了 UI 锚点,我还将轮询逻辑从主线程移到了独立的 HandlerThread。
主线程在后台容易被系统降低调度权重,而独立的后台线程配合前台服务,存活率更高。
// 启动一个具有前台优先级的后台线程
monitorThread = HandlerThread("MonitorThread", android.os.Process.THREAD_PRIORITY_FOREGROUND)
monitorThread.start()
monitorHandler = Handler(monitorThread.looper)
// 在这个线程里跑死循环轮询
monitorHandler.post(pollRunnable)
总结
经过一番鏖战,总结出在恶劣的 ROM 环境下做实时监测的架构:
- 像素锚点 (
TYPE_APPLICATION_OVERLAY):欺骗 WindowManager,防止进程挂起。 - UsageStats 事件流 (
queryEvents):获取绝对准确、无延迟的系统操作记录。 - HandlerThread 轮询:脱离主线程的独立心跳,保证检测频率稳定。
在最后,Pebble 终于完全实现了“秒级响应”的体验。