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模式下三个核心问题的原因和解决方案。我画图时特意把关键路径标红了,方便你快速定位。
22.5 避坑指南:我曾经踩过的坑
最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。
坑1:我曾经在Doze模式下用JobScheduler做定时任务,结果发现任务根本不执行。查了半天,原来是JobScheduler在Doze模式下也有延迟。后来改用WorkManager,问题解决。
坑2:有个项目用了setExact()设置Alarm,结果在Android 12上不生效。原因是Android 12开始,SCHEDULE_EXACT_ALARM权限需要用户手动授予。我建议如果不需要精确到秒,就用setAndAllowWhileIdle()。
坑3:同步任务里用了Retrofit做网络请求,结果Doze模式下请求被挂起。后来发现是OkHttp的connectTimeout设置得太短。我建议在Doze模式下,把超时时间设置得长一些,比如30秒以上。
嗯,今天就聊到这里。Doze模式下的问题其实就三个核心:网络、同步、定时。只要把这三点处理好,大部分问题都能解决。记住一个原则:尊重系统的省电策略,但也要为用户体验争取合理的资源。
公众号:蓝海资料掘金营,微信deep3321