如何判断设备是否处于 Doze 模式?

解读

面试官问“如何判断设备是否处于 Doze 模式”,并不是想听你背官方文档,而是考察三点:

  1. 你是否真的在 6.0+ 机器上踩过“后台任务被挂起”的坑;
  2. 你是否能把“系统行为”翻译成“业务可感知”的指标;
  3. 你是否知道国内无 GMS 场景下该怎么“曲线救国”。
    一句话:给出“可上线、可灰度、可兜底”的完整判断链,而不是抛一句“看 ACTION_DEVICE_IDLE_MODE_CHANGED”就结束。

知识点

  1. Doze 状态机:Deep Idle、Light Idle、Maintenance Window、Idle Exit 四段式,6.0 引入,7.0 加入 Light Idle,国内 ROM 普遍把 Deep Idle 窗口缩到 15 min 以内。
  2. 官方 API:
    PowerManager.isDeviceIdleMode() → Deep Idle;
    PowerManager.isLightDeviceIdleMode() → Light Idle(API 29+)。
  3. 广播:ACTION_DEVICE_IDLE_MODE_CHANGED / ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED,只在系统 UID 与少数白名单应用收到,普通 App 收不到。
  4. 国内“无 GMS”真相:Doze 仍生效,但 Idle 窗口由厂商自定义,且可能被后台清理策略提前打断;部分 ROM(MIUI、ColorOS)把 Idle 判断与“自启动”权限绑定。
  5. 可执行脚本:dumpsys deviceidle get deep / get light 能拿到实时状态,需 shell 权限或 adb;App 内可通过 root 或 Shizuku 方案调用。
  6. 兼容灰度:对 API<23 返回 false;对 API 29 以下机型 Light Idle 永远 false;对国内 ROM 需加“厂商名单”白名单缓存,防止误判。
  7. 业务兜底:若无法拿到系统级状态,可退而监听 JobScheduler/AlarmManager 延迟执行时间,当单次延迟超过 5 min 即认为“极可能进入 Idle”,作为弱提示。

答案

线上代码采用“分层降级”策略,兼顾合规与灰度:

object DozeCheck {
    private val powerManager = app.getSystemService(Context.POWER_SERVICE) as PowerManager

    /**
     * 返回当前是否处于 Deep Doze;Light Idle 另行暴露接口。
     * 无权限要求,可在主线程调用。
     */
    fun isInDeepDoze(): Boolean =
        if (Build.VERSION.SDK_INT >= 23) powerManager.isDeviceIdleMode else false

    /**
     * API 29+ 才有 Light Idle;以下版本恒 false。
     */
    fun isInLightDoze(): Boolean =
        if (Build.VERSION.SDK_INT >= 29) powerManager.isLightDeviceIdleMode else false

    /**
     * 国内灰度:若持有 DUMP 权限(如通过 Shizuku),优先用 dumpsys 校准,
     * 防止厂商把 Deep Idle 提前结束但 API 仍返回 true 的极端 case。
     */
    fun maybeInDoze(): Boolean {
        if (isInDeepDoze() || isInLightDoze()) return true
        return try {
            val output = Shell.exec("dumpsys deviceidle get deep")
            "true" in output
        } catch (ignored: Throwable) {
            false
        }
    }
}

注册广播补充实时性(仅系统应用有效,普通 App 可放弃):

if (app.isSystemApp()) {
    registerReceiver(
        DozeReceiver(),
        IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
    )
}

对普通应用,推荐用 WorkManager 加 setRequiredNetworkType(NetworkType.CONNECTED)setRequiresBatteryNotLow(true),让系统替我们“感知” Idle 窗口;再记录任务实际触发时间与期望时间的差值,差值 > 5 min 即写入弱提示日志,用于后台运营分析。

拓展思考

  1. 国内推送保活:Doze 会冻结高功耗网络访问,厂商通道(小米、华为、OPPO)利用系统签名的白名单广播绕过 Idle;自建推送必须申请厂商“自启动”+“电池无限制”权限,否则即使判断准确也无法拉活。
  2. 折叠屏/多窗口场景:当设备处于桌面 Dock(模拟车载)时,Doze 规则更激进,Deep Idle 可缩短至 5 min;此时若 App 有前台服务且带媒体播放,需加 FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 才能豁免。
  3. 合规风险:Android 13 后,调用 dumpsys 被严格审计,普通 App 通过反射读取 IPowerManager 会触发 StrictMode 违规;线上务必走 Shizuku/Root 授权,否则只能放弃精确值,改用“任务延迟”弱指标。
  4. 测试技巧:adb 强制进入 Idle ——
    adb shell dumpsys deviceidle enable deep
    adb shell dumpsys deviceidle force-idle deep
    配合 adb shell dumpsys battery unplug 可模拟纯离线场景,验证数据库压缩、日志上报、心跳包等模块是否真正暂停。