Return

Android 后台保活的实现

饱受期末忍不住刷手机的困扰,我决定开发一个自律应用 Pebble

核心功能是监测用户当前是否打开“黑名单 App”,

如果打开,会在屏幕顶部落下“石头”遮挡用户视线。

想实现这个监测功能,最大的难点就是保证应用要常驻后台以检测用户行为。

我尝试了多种方法,最后用一个取巧的方式 绕开 了 MIUI 的后台监测。

无障碍服务

因为之前在开发中接触过无障碍服务的接口,我在一开始选择了 AccessibilityService 方案。

这个方案理论上是可行的:系统在监测到应用切换后主动推送 AccessibilityEvent 给挂在后台的应用。

但方案在实际开发中遇到了无法解决的 MIUI 制裁

  1. 事件积压与爆发:MIUI 为了优化前台流畅度,经常挂起后台的无障碍事件分发。这直接导致了我在测试中遇到的问题:手机切入抖音没有任何反应,等回到 Pebble 时,系统瞬间吐出大量 Switch 日志。
  2. SystemUI 干扰:唤起多任务界面时会打断“当前应用”的判定,状态切回 home ,致使用户逃脱黑名单管辖。
  3. 热启动漏测:从多任务界面快速切换时,部分机型不发送 WINDOW_STATE_CHANGED 事件。

为了修补这些问题,我尝试了“主动查询 rootInActiveWindow”、“心跳轮询补漏”、“防抖动逻辑”,代码越写越复杂,但这些问题没有得到丝毫解决。

UsageStats 与“消失的轮询”

补丁越打越多,问题却得不到解决,只能选择重构所有代码。

方案切换

放弃被动的无障碍服务,改用 UsageStatsManager (使用情况统计) 配合 前台服务 (Foreground Service) 进行每秒一次的主动轮询。

  • 数据源:使用 queryEvents() 查询实时的系统动作流,避开 queryUsageStats 的延迟。
  • 轮询机制:在 Service 中启动一个协程或 Handler,每 400ms~1000ms 检查一次。

诡异的 Bug

在 USB 连接调试时,一切都可以正常运行。但是一旦拔掉 USB 线,使用 Release 包安装测试,事件积压(后台冻结)又发生了:

  1. 只要 Pebble 在前台,检测正常。
  2. 一旦 Pebble 切到后台,轮询停止
  3. 切回 Pebble,日志才突然大量跳出。

原因分析:这是 MIUI 等国产 ROM 激进的省电策略:USB 调试模式下系统会豁免进程冻结,但在 Release 模式下,即便我有前台服务通知,系统依然判定我“无用户交互”,直接挂起我的进程,监测中止。

像素保活

所以经过一系列的尝试,发现最终的实现手段应该是保持应用的活跃状态,让系统认为我正在与用户交互,从而规避冻结。

经过查询,发现了开发者的常用手段:在屏幕上始终保留一个 View 视图

只要 WindowManager 认为我在画图,我就属于“图形处理前台进程”,可以一直保持高优先级,避免被灭活。

实现细节

不再把平时状态的 View 设为 View.GONE,这样系统知道我只是在后台静默,依旧惨遭冻结毒手。

我们对视图的要求应该是用户不可见,但系统可感知,即透明化处理。

修改 OverlayManager,定义两种状态:

  1. 显示态:正常的落石画面。
  2. 隐藏态 (保活):一个 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 环境下做实时监测的架构:

  1. 像素锚点 (TYPE_APPLICATION_OVERLAY):欺骗 WindowManager,防止进程挂起。
  2. UsageStats 事件流 (queryEvents):获取绝对准确、无延迟的系统操作记录。
  3. HandlerThread 轮询:脱离主线程的独立心跳,保证检测频率稳定。

在最后,Pebble 终于完全实现了“秒级响应”的体验。