Android 后台保活的实现
Table of Contents
最近饱受期末分心困扰,决定开发一个自律应用 Pebble 。
核心功能是监测用户当前是否打开“黑名单 App”(如抖音、小红书),
并在检测到时投下“落石”进行干扰。
为了实现这个监测功能,我从“拥抱系统”到“对抗系统”,
终于通过 1 个像素 解决了 MIUI 后台冻结问题。
第一关:无障碍服务
一开始选择了 AccessibilityService (无障碍服务)。
理论上这是个完美方案:系统主动推送 AccessibilityEvent,应用后台接收。
而且我在之前的开发中已经使用过,颇有了解。
但在实际开发中遇到了无法解决的 MIUI 痛点:
- 事件积压与爆发:MIUI 为了优化前台流畅度,经常挂起后台的无障碍事件分发。直接导致:切进抖音没反应,等回到 Pebble 时,系统瞬间吐出大量
Switch日志。 - SystemUI 干扰:多任务界面会打断“当前应用”的判定,影响落石效果。
- 热启动漏测:从多任务界面快速切换时,部分机型不发送
WINDOW_STATE_CHANGED事件。
为了修补这些问题,尝试了“主动查询 rootInActiveWindow”、“心跳轮询补漏”、“防抖动逻辑”,代码越写越复杂,但稳定性依然堪忧。
第二关:UsageStats 与“消失的轮询”
为了追求稳定性,我决定重构。
方案切换
放弃被动的无障碍服务,改用 UsageStatsManager (使用情况统计) 配合 前台服务 (Foreground Service) 进行每秒一次的主动轮询。
- 数据源:使用
queryEvents()查询实时的系统动作流(Action Stream),避开queryUsageStats的聚合延迟。 - 轮询机制:在 Service 中启动一个协程或 Handler,每 400ms~1000ms 检查一次。
诡异的 Bug
在 USB 连接调试时,一切正常运行。一旦拔掉 USB 线,打出 Release 包安装,事件积压(后台冻结)又发生了:
- 只要 Pebble 在前台,检测正常。
- 一旦 Pebble 切到后台,轮询停止。
- 切回 Pebble,日志才突然跳出。
原因分析:这是 MIUI 等国产 ROM 激进的省电策略。USB 调试模式下系统会豁免进程冻结,但在 Release 模式下,即便我有前台服务通知,系统依然判定我“无用户交互”,直接挂起 (Suspend) 了我的进程。进程挂起,线程(监测)自然就停止。
第三关:1 像素锚点
为了让系统认为我“正在与用户交互”,从而规避冻结,我采取了“欺骗”手段:在屏幕上始终保留一个 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 锚点,我还将轮询逻辑从主线程(Main Looper)移到了独立的 HandlerThread。
主线程在后台容易被系统降低调度权重,而独立的后台线程配合前台服务,存活率更高。
// 启动一个具有前台优先级的后台线程
monitorThread = HandlerThread("MonitorThread", android.os.Process.THREAD_PRIORITY_FOREGROUND)
monitorThread.start()
monitorHandler = Handler(monitorThread.looper)
// 在这个线程里跑死循环轮询
monitorHandler.post(pollRunnable)
总结
经过一番鏖战,总结出在“恶劣”的 ROM 环境下做实时监测的架构:
- 1 像素锚点 (
TYPE_APPLICATION_OVERLAY):欺骗 WindowManager,防止进程挂起。 - UsageStats 事件流 (
queryEvents):获取绝对准确、无延迟的系统操作记录。 - HandlerThread 轮询:脱离主线程的独立心跳,保证检测频率稳定。
在最后,Pebble 终于完全实现了“秒级响应”的体验。
有时候,解决复杂系统问题的,往往就是 1 个像素的思路转变。