22. Doze模式下的常见问题:消息延迟、同步失败、Alarm不准时

好,咱们今天聊点实际的。Doze模式的理论讲了不少,但真正让开发者头疼的,永远是那几个老问题——消息延迟、同步失败、Alarm不准时。我在项目里被这些问题折磨过不止一次,今天就把我踩过的坑和解决方案一次性说清楚。

22.1 消息延迟:为什么你的推送总迟到?

先说消息延迟。你想想看,App在后台,手机进入Doze,系统把网络访问给掐了。这时候服务器发来的消息,App根本收不到。等手机退出Doze,消息才一股脑涌进来。用户体验?嗯,很糟糕。

为什么会这样?说白了,Doze模式下系统会限制网络访问。App的Socket连接会被挂起,长连接直接断掉。我见过一个IM应用,用户锁屏后消息延迟了整整15分钟。用户投诉说“你们App是不是死了”。

核心原因:Doze模式下,系统会暂停App的网络访问权限。只有等到维护窗口(Maintenance Window)才会恢复。

那怎么解决?我个人习惯用FCM(Firebase Cloud Messaging)的高优先级消息。FCM有专门的“高优先级”通道,系统会优先传递这类消息。但注意,高优先级消息也不是100%可靠——我在项目中遇到过,某些国产ROM会拦截FCM的高优先级通道。

我的建议:如果消息实时性要求极高,可以考虑申请“忽略电池优化”权限。但别滥用,用户会反感。

还有一个坑:有些开发者以为用前台服务就能绕过Doze。嗯,前台服务确实能提高优先级,但Doze模式下,前台服务的网络访问也会被限制。我曾经有个项目,前台服务里跑着WebSocket,结果锁屏后照样断连。后来改用FCM + 前台服务双保险,才算稳定下来。

22.2 同步失败:数据为什么没传上去?

同步失败是另一个高频问题。App在后台做数据同步,结果Doze模式一开,同步任务直接被挂起。等用户再次打开App,发现数据还是旧的。

我记得有个项目是做健康监测的。用户戴着设备,数据通过蓝牙传到手机,手机再同步到云端。结果用户反馈说“我走了两万步,App里只显示五千步”。查了半天,发现是Doze模式下同步任务被延迟了。用户运动完锁屏,数据没传上去,等下次打开App才同步。

解决方案其实不复杂。Google官方推荐用WorkManager来做同步任务。WorkManager会感知Doze状态,自动选择合适的时机执行任务。但要注意,WorkManager的NetworkType.CONNECTED约束在Doze模式下可能不生效——因为系统认为网络“可用”,但实际上App的网络访问被限制了。

// 正确的做法:使用 WorkManager 的 PERIODIC 任务
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES
).setConstraints(
    Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
).build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "sync_work",
        ExistingPeriodicWorkPolicy.KEEP,
        syncRequest
    )

注意:不要用AlarmManager做同步任务。AlarmManager在Doze模式下会被合并到维护窗口,时间不准。我见过有人用AlarmManager每5分钟同步一次,结果Doze模式下变成每30分钟才执行一次。

还有一个技巧:如果同步任务对实时性要求不高,可以设置setBackoffCriteria。WorkManager会自动重试,但重试间隔会指数增长。我一般设置初始延迟30秒,最大延迟1小时。这样既不会频繁唤醒设备,也不会让数据丢失太久。

22.3 Alarm不准时:定时任务为什么跑偏?

Alarm不准时,嗯,这是Doze模式下最让人头疼的问题。你明明设置了10分钟后执行Alarm,结果它30分钟后才响。用户骂你,你还得背锅。

原因很简单:Doze模式下,系统会把多个Alarm合并到同一个维护窗口执行。这是为了省电,但代价就是时间不准。Google官方文档说“Alarm的精度会降低到几分钟级别”,但我在项目中实测,某些设备上延迟能达到1小时以上。

那怎么办?分情况讨论:

  • 如果必须准时:使用setAlarmClock()。这个接口会强制退出Doze模式,系统会保证准时执行。但代价是耗电,别滥用。
  • 如果允许一定延迟:使用setAndAllowWhileIdle()。这个接口在Doze模式下也能触发,但频率有限制——每个App每小时只能触发几次。
  • 如果完全不关心准时:setExact()配合SCHEDULE_EXACT_ALARM权限。但注意,Android 12以后,这个权限需要用户手动授予。

我的经验:我曾经做一个闹钟App,用户投诉闹钟不响。查了半天,发现是Doze模式下Alarm被延迟了。后来改用setAlarmClock(),问题解决。但用户又投诉耗电——嗯,这就是取舍。

还有一个细节:setAndAllowWhileIdle()的触发频率限制是每个App每9分钟一次。如果你在9分钟内连续调用多次,只有第一次会生效。我建议用setExactAndAllowWhileIdle(),这个接口的触发频率更高,但也要注意别滥用。

22.4 一张图看懂Doze模式下的问题与对策

下面这张图总结了Doze模式下三个核心问题的原因和解决方案。我画图时特意把关键路径标红了,方便你快速定位。

Doze模式常见问题与对策 消息延迟 原因:网络访问被挂起 Socket连接中断 长连接失效 对策:FCM高优先级 同步失败 原因:同步任务被延迟 WorkManager约束失效 数据上传中断 对策:WorkManager+重试 Alarm不准时 原因:Alarm被合并执行 维护窗口限制 频率限制 对策:setAlarmClock() 解决方案汇总 • 消息延迟 → FCM高优先级 + 前台服务(双保险) • 同步失败 → WorkManager + 合理设置重试策略 • Alarm不准时 → setAlarmClock()(准时) / setAndAllowWhileIdle()(允许延迟) • 通用原则:能不唤醒就不唤醒,能合并就合并

22.5 避坑指南:我曾经踩过的坑

最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。

坑1:我曾经在Doze模式下用JobScheduler做定时任务,结果发现任务根本不执行。查了半天,原来是JobScheduler在Doze模式下也有延迟。后来改用WorkManager,问题解决。

坑2:有个项目用了setExact()设置Alarm,结果在Android 12上不生效。原因是Android 12开始,SCHEDULE_EXACT_ALARM权限需要用户手动授予。我建议如果不需要精确到秒,就用setAndAllowWhileIdle()

坑3:同步任务里用了Retrofit做网络请求,结果Doze模式下请求被挂起。后来发现是OkHttpconnectTimeout设置得太短。我建议在Doze模式下,把超时时间设置得长一些,比如30秒以上。

嗯,今天就聊到这里。Doze模式下的问题其实就三个核心:网络、同步、定时。只要把这三点处理好,大部分问题都能解决。记住一个原则:尊重系统的省电策略,但也要为用户体验争取合理的资源。

公众号:蓝海资料掘金营,微信deep3321