ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android后台网络请求被限制?从Doze到应用待机分组全解析

Android后台网络请求被限制?从Doze到应用待机分组全解析 上个月有个做独立开发的朋友来找我说他的应用一锁屏上传任务就断用户已经骂了一周。他拿来复现的机器是一台刷了原生 Android 9 系统的旧设备问题非常典型App 退到后台后网络请求没有崩溃日志没有错误弹窗进程直接在后台消失。重新回到前台任务失败了日志里只有一条SocketTimeoutException。这台机器跑的是标准 AOSP 系统没有厂商自己那套后台管理所以问题源头相当干净——就是 Android 系统层面的后台网络请求限制不是他代码里某个变量写错了。实际上这个问题从 Android 6.0 开始就在逐步收紧到了 Android 9API 28又加入了应用待机分组机制把后台限制从“一刀切”变成了“按使用频率动态分级”。这篇文章就围绕这个场景展开后台网络请求到底被谁限制了、限制的机制是什么、遇到之后怎么一步步排查、正规的解决方案有哪些以及 Android 9 里最容易被人误判成“后台限制”的明文 HTTP 问题。适合正在做 App 后台同步、文件上传、消息推送、或者面试前想把这些机制彻底搞懂的同学。1. 先搞清楚“谁在限制你的网络请求”从 Doze 到应用待机分组1.1 Doze 模式不是你的 App 坏了是系统把所有人关进了同一间“限电宿舍”很多人第一次遇到后台网络请求失败第一反应是自己代码有 bug或者服务器挂了。实际上从 Android 6.0API 23开始系统就引入了一个叫 Doze 的机制。它的触发条件很明确设备处于静止状态、没有在充电、屏幕已经关闭一段时间。满足这三个条件后系统会进入休眠模式在这个模式下后台网络访问、CPU 任务、闹钟、WakeLock 都会被统一挂起。我习惯用一个比喻来解释 Doze它就像一个宿舍管理员晚上十一点准时强制断电。管理员不会针对某个同学而是把所有宿舍全部断电不管是学习还是打游戏一律不许偷偷开灯。但是管理员每隔一段时间会打开走廊灯让大家上个厕所、接杯水这个时间窗口在系统里叫维护窗口maintenance window。在维护窗口内被挂起的任务可以临时跑一小会儿跑完继续断电。所以你在 Doze 阶段看到的网络请求失败本质不是失败是被延迟了。TCP 连接长时间没有收到数据触发了超时客户端以为服务器出问题了实际上数据包还在系统队列里排队。这个问题一直到 Android 7.0 加入 Light Doze 后依然存在只是轻量版触发条件宽松一些——设备只要屏幕关闭、未充电哪怕还在运动也会进入轻度休眠后台任务被允许执行的频率更低。这里有一个很多人不知道的调试细节如果你在开发时把手机插着 USB 线连着电脑Doze 基本不会触发因为“正在充电”是 Doze 的禁止条件。很多开发者折腾半天复现不出来拔掉数据线、锁屏、静置半小时再试问题立刻出现。1.2 Android 9 的应用待机分组被划到“罕用”档位后网络请求只能排队到了 Android 9API 28Google 又引入了 App Standby Buckets中文叫应用待机分组。系统会根据用户近期的使用频率、通知交互次数、上次打开时间把应用划分到四个档位活跃Active、工作集Working Set、常用Frequent、罕用Rare。四个档位对后台网络和任务调度的宽容度完全不一样。活跃应用基本不受限制工作集应用可以比较频繁地跑后台任务到了常用这一档系统就开始压低后台 Job 的执行频率了而罕用应用的网络访问、JobScheduler 任务会被压到很低频可能很长时间才放行一次。这个机制对开发者来说最难受的一点是你没法用代码直接控制自己 App 被分到哪个档位。系统看的是用户与应用的交互行为比如用户有没有主动打开 App、有没有点你的通知。如果一个用户一周都不打开一次你的 App它就会被系统慢慢降级到罕用档位后台同步频率大幅下降这跟你代码写得多好没有关系。如果你在做 IM、新闻资讯这类需要周期性后台拉取数据的应用要正视这个机制尽力把用户拉回前台不管是优化通知点击率还是引导用户主动打开都比强行提升后台执行频率靠谱。到了 Android 12这个方向更进一步加入了 App Hibernation几个月不用直接进入休眠状态后台任务几乎全部冻结开发者的操作空间只会越来越小。1.3 厂商 ROM 才是真正的变量同样的代码在不同手机上表现完全不同AOSP 原生的 Doze 和待机分组只是“基础题”真正让安卓开发者头疼的是各家厂商自己加的省电策略。华为的设置里叫“应用启动管理”小米叫“神隐模式”OPPO 叫“耗电保护”vivo 叫“后台耗电管理”。这些策略是在 AOSP 之上叠加的私有逻辑也就是说哪怕你在原生 Android 9 上把所有后台机制都适配好了到了 MIUI 上照样可能被清后台。这些厂商策略一般分两层一层是限制应用自启动另一层是智能清理后台进程。加上各种“纯净后台”“极致省电”的开关用户经常在不知情的情况下就把你的 App 划进了限制名单。结果就是你的 App 在别的手机上跑得好好的到了某个特定品牌的机器上一锁屏任务就死打开日志发现连进程都没了这就是被厂商系统“冻住”了。对这一类问题正规的思路是引导用户到系统设置里把你 App 的后台权限放开前面说的是让你理解限制机制的来源不是让你去跟系统硬刚。真正要落地的是下面这套排查和方案选型。2. 一次真实的任务中断完整走一遍后台请求排查链路2.1 第一步永远先看日志用报错类型判断“锅”在哪一层接到“后台请求被限制”这类反馈我的排查顺序永远是先把问题定性这是请求发不出去还是发出去了没响应还是响应超时还是进程被杀了根本没有日志不同的现象对应完全不同的根因。如果你在 Logcat 里看到的是SocketTimeoutException说明请求其实已经发出但长时间没有收到响应。这个大概率是 Doze 或厂商省电策略把网络挂起了数据还在等待通路释放。如果看到的是UnknownHostException大概率不是后台限制而是网络切换到无 DNS 的环境比如手机从 Wi-Fi 切到移动网络时 DNS 缓存失效。如果看到的是ConnectException: Connection refused那问题基本在服务端端口没监听或防火墙拦截了跟后台没多大关系。还有一种情况连异常都没有。进程被杀掉后Application 重新冷启动原来的上传任务自然就没了。这种最隐蔽因为它不会在 Logcat 里留下任何网络层的错误信息只会在am_proc_died这类系统日志里看到进程死亡记录。判断标准是锁屏前 task 还在锁屏后 Logcat 里找不到任何你的业务日志大概率是被系统收回了。在一个刷了原生 Android 9 的设备上做复现实验时我通常建议把日志关键字分三层打第一层是网络库连接状态第二层是业务层的 task 生命周期第三层是进程存活标记。这样一旦出问题看日志顺序就能判断任务到底走到了哪一步。2.2 用 adb 把 Doze 和待机分组变成可复现实验很多后台问题难排查是因为触发条件太随机要等设备静止、灭屏、不充电时间不可控。其实 Android 系统早就留好了测试后门用 adb 命令可以直接强制进入 Doze几秒钟就能复现问题。# 模拟设备断开充电拔掉“电源线” adb shell dumpsys battery unplug # 强制进入 Doze 休眠 adb shell dumpsys deviceidle force-idle # 查看目标应用当前所属的待机分组 adb shell am get-standby-bucket com.example.app执行完 force-idle 后屏幕保持熄灭状态任务基本就被挂起了。这时候你可疑用另一个终端观察网络请求日志看它是在维护窗口被放行、还是直接超时失败。试验结束后记得恢复环境# 退出 Doze adb shell dumpsys deviceidle unforce # 恢复充电状态 adb shell dumpsys battery reset这个流程我建议在每一个涉及后台任务的改版里都跑一遍尤其是从 Android 8 升到 Android 9 的项目因为待机分组逻辑是新加的很多老代码都没适配过。2.3 区分“进程被杀”和“任务被挂起”处理路径完全不同同样表现为后台任务失败处理思路却可能完全相反。如果只是任务被挂起、进程还活着你要做的是优化超时重试策略加长超时时间让任务在系统放行后能继续跑。如果是进程被杀你要考虑的是把关键任务改到前台服务里或者拆分任务交给 WorkManager而不是单纯调网络库参数。判断方法也有讲究。锁屏后等 15 分钟然后重新亮屏、回到 App如果任务直接恢复继续执行多半是挂起而非被杀。如果在任务列表里已经找不到这个 App或者亮屏后 App 是冷启动界面那进程已经死了。我之前在 Android 9 的盒子上做过测试把视频上传任务放到普通 Service 里锁屏 10 分钟进程就被回收了改成前台服务后同样条件下进程存活只是网络请求被 Doze 延迟。这个实验说明区分这两种情况决定了你是要“修网络层”还是“改架构层”。3. 前台服务、WorkManager、系统推送三条正规通道怎么选3.1 前台服务唯一能保证持续执行的方案代价是常驻通知Android 8.0API 26之后后台启动普通 Service 的行为被严格限制。应用处于后台时不能随意调用startService()否则会抛IllegalStateException。要想在后台持续跑一个任务正路只有一条使用前台服务。前台服务通过一个常驻通知栏告诉用户“这个应用还在干活”系统会把它当成用户可见的组件不会轻易回收。文件上传、音频播放、导航这类需要长时间持续运行的任务都应该用前台服务承载。代价也很明显用户会看到一条常驻通知如果业务本身在用户感知上不适合长期驻留容易被用户手动杀掉。另外前台服务不是免死金牌部分厂商 ROM 还是会清理它只是优先级比普通 Service 高很多。Android 14API 34之后前台服务还必须声明具体类型比如dataSync、mediaPlayback、location等并在运行时申请对应的权限。如果你的 App 还要兼容新系统写前台服务时最好把foregroundServiceType一并配好。3.2 WorkManager把同步任务交给系统调度如果你的业务场景是“数据晚一点同步没关系但一定要同步”WorkManager 是更合适的选择。它内部封装了 JobScheduler、AlarmManager 等底层机制能根据设备电量、网络状态、Doze 周期自动安排执行时间。WorkManager 最大的优点是系统会保证任务最终被执行即使进程被杀死下次系统有空闲窗口也会重新调度。它还自带重试和退避策略失败后可以按指数退避重新执行省去你自己写重试逻辑的麻烦。代价是执行时机不可控。Doze 期间系统可能把任务拖到维护窗口才执行具体延迟多久完全由系统决定你的代码只能干等。所以如果任务是用户正在等待的关键操作比如用户点“立即上传”后希望马上看到结果这种不适合用 WorkManager但如果只是“把日志上报给服务器”“把缓存数据同步上去”用 WorkManager 反而减少不必要的后台唤醒。3.3 长连接的心跳与重连减少后台频率比硬保活更可靠有些场景绕不开长连接比如 IM 收消息、股票行情、远程控制。长连接最大的问题是心跳锁屏状态下心跳被 Doze 挂起连接就可能被服务端判定超时断开。我见过的错误做法是为了让连接存活把心跳间隔调到几十秒甚至更短结果就是频繁唤醒设备耗电剧增反而更容易被系统限制。正确的思路是接受系统调度锁屏且进入 Doze 后连接断开就断开等用户点亮屏幕、网络恢复后再快速重连并补齐离线消息。重连策略要配合网络状态监听别盲目定时重试。用 ConnectivityManager 监听网络切换事件在onAvailable回调里统一触发重连比写死心跳次数靠谱得多。这部分的核心思想是尽量降低后台时期的资源占用让系统觉得你的 App “很懂事”反而不容易被激进清理。3.4 三条通道的横向对比表我把三条正规通道的适用场景和限制整理成一张对比表方便你给任务做技术选型时快速做决策方案后台运行保障系统限制推荐使用场景代价前台服务较高但不绝对需常驻通知高版本需声明服务类型上传/下载、音视频播放、导航用户可见通知占用系统资源WorkManager保证最终执行不保证实时执行时间由系统决定Doze 下可能延迟较久数据同步、日志上报、定期任务延迟不可控不适合实时操作长连接重连持续连接但会被挂起心跳受系统合并与延迟限制IM、行情、远程控制需要额外处理断线重连和消息补拉如果业务确实需要“进程不在也能恢复任务”厂商推送是另一条路它相当于把消息送达交给系统级的推送服务推送到达后再唤醒 App 处理业务逻辑。这个方案不在本文展开但你在选型时应该把“系统推送触发”也纳入考虑范围。4. 代码实现一套“后台请求尽量不丢”的最小可运行方案4.1 网络层基础配置超时、重试、指数退避说完选型落到代码上。第一步先把网络层配置写好很多后台请求失败不是被系统杀掉的而是超时设置不合理导致任务在短暂的“被延迟窗口”里直接放弃了。以 OkHttp 为例建议的连接超时、读取超时、写入超时设置如下OkHttpClient client new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build();为什么读取超时要给到 30 秒因为在 Doze 维护窗口内系统可能只放行很短一段时间如果服务端处理稍慢短超时就会直接误判为失败。但超时也不是越长越好过长会让大量失败任务堆积占满线程池和内存。15 秒连接超时、30 秒读写超时是我在多数项目里实测下来比较稳的配置。重试策略上推荐指数退避第一次失败后等 2 秒重启试第二次 4 秒第三次 8 秒最高上限到 60 秒左右。这样既能在系统短暂放行时抓住机会又不会在无网环境下疯狂重试耗电。退避逻辑建议封装在任务调度层不要写在业务回调里方便统一维护。4.2 前台服务的正确写法5 秒内必须 startForeground如果你决定用前台服务承载一个上传任务写法上有一个必须注意的细节Android 8.0 之后要用startForegroundService()启动并且 Service 启动后的 5 秒内必须调用startForeground()否则系统直接报RemoteServiceException崩溃。一个最小可用的前台服务核心代码如下public class UploadService extends Service { private static final String CHANNEL_ID upload_channel; Override public void onCreate() { super.onCreate(); createNotificationChannel(); } Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification new Notification.Builder(this, CHANNEL_ID) .setContentTitle(任务同步中) .setContentText(正在上传待同步数据) .setSmallIcon(android.R.drawable.stat_sys_upload) .setOngoing(true) .build(); startForeground(1, notification); // 这里执行实际的上传逻辑 startUploadTask(); return START_STICKY; } private void createNotificationChannel() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( CHANNEL_ID, 上传服务, NotificationManager.IMPORTANCE_LOW ); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } } }启动端代码也要兼容 Android 8.0 以上的新 APIContextCompat.startForegroundService(context, new Intent(context, UploadService.class));如果项目 targetSdk 已经切到 34别忘了在 Manifest 里声明前台服务类型service android:name.UploadService android:foregroundServiceTypedataSync android:exportedfalse /这个dataSync类型还需要配套的运行时权限高版本机型的适配坑不少但如果你的 App 核心任务依赖后台上传这个适配跑不掉。4.3 用 WorkManager 承接“迟一点也要传”的任务不想长期占用前台通知但又不能丢任务用 WorkManager 最合适。下面的代码实现了一个简单的同步 Workerpublic class SyncWorker extends Worker { public SyncWorker(NonNull Context context, NonNull WorkerParameters params) { super(context, params); } NonNull Override public Result doWork() { boolean success uploadPendingData(); return success ? Result.success() : Result.retry(); } }提交任务时建议显式声明网络和电量约束避免在无网或低电量环境下盲目执行Constraints constraints new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build(); OneTimeWorkRequest request new OneTimeWorkRequest.Builder(SyncWorker.class) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build(); WorkManager.getInstance(context).enqueue(request);我把 WorkManager 理解为“系统帮你排队取号”它不会立刻执行但一定会执行。你在代码里要做的就是把任务拆细一点每次只处理一批数据避免单个 Worker 执行时间过长被系统中断。4.4 电池优化豁免可申请但要克制Android 系统提供了一个白名单机制可以让 App 绕过部分电池优化限制对应REQUEST_IGNORE_BATTERY_OPTIMIZATIONS。申请代码如下PowerManager pm (PowerManager) getSystemService(Context.POWER_SERVICE); if (pm ! null !pm.isIgnoringBatteryOptimizations(getPackageName())) { Intent intent new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse(package: getPackageName())); startActivity(intent); }但是这里必须泼一盆冷水这个权限在应用市场审核里被卡得很严非白名单类应用基本不允许使用。即使在国内市场上架弹窗让用户“关闭电池优化”也容易招致差评和投诉。我的建议是除非你的 App 是实打实的即时通信、导航、音乐播放类否则不要主动弹这个授权请求最多在设置页里放一个引导入口把选择权交给用户。现在顺势把范围扩大一点Android 9 上还有一个非常容易让人误解的现象明明网络请求正常锁屏后回来就失败日志却指向了另一个完全不同的系统策略——默认禁止明文 HTTP 流量。5. Android 9 里另一个容易被误判的“限制”默认禁止明文 HTTP5.1 现象后台回来请求直接抛 Cleartext 异常我接过好几个咨询开发者都是这样描述的App 退到后台后过一会儿再回来网络请求全挂了一开始以为是被系统限制后台了后来仔细看日志才发现报的是Cleartext HTTP traffic to xxx not permitted。这个报错跟后台限制没有半点关系它是 Android 9API 28的另一项策略默认禁止应用使用明文 HTTP 流量强制要求使用 HTTPS。当你的targetSdkVersion 28且运行在 Android 9 及以上设备时任何http://协议的请求都会被网络策略直接拦下。为什么这个报错经常跟后台任务扯在一起因为很多 App 只在后台同步时才走某个内网地址或者老接口平时前台用的主域名已经切到 HTTPS 了所以刷到的概率不高。直到锁屏后触发了后台同步任务那个走了 HTTP 的请求才会冒出来。看起来像是“后台被限制”实际是“明文流量被拦截”被现象误导的人非常多。5.2 根因与适配networkSecurityConfig 比全局开关更可控解掉这个问题的正路是给项目加网络安全管理配置。在 Manifest 的 application 节点里声明application android:networkSecurityConfigxml/network_security_config ... 然后在res/xml/network_security_config.xml里做精细化放行network-security-config base-config cleartextTrafficPermittedfalse / domain-config cleartextTrafficPermittedtrue domain includeSubdomainstrue192.168.1.100/domain /domain-config /network-security-config这里有个经验点要先说明domain标签只支持域名不支持 IP 字面量。如果你要用 HTTP 访问局域网 IP 或内网测试地址直接把 IP 写进domain是无效的这种情况下最省事的是临时把cleartextTrafficPermittedtrue配到 base-config或者全局开android:usesCleartextTraffictrue上线前再收紧。处理这个问题时我的建议很明确能用 HTTPS 的接口就尽快切 HTTPS这是治本网络配置里的白名单只留给那些实在改造不了的老接口。别为了省事全局放开明文不然等你想起来收紧的时候很难排查出还有哪些地方在走 HTTP。6. 把系统升级的“惊喜”堵在上线之前后台请求专项测试与回归6.1 用 adb 模拟 Doze、待机分组和断网的命令清单后台请求问题最大的风险是“偶发性”很多团队测不出来就是因为没用对工具。下面这套命令我已经整理成自己的测试脚本每次涉及后台任务改动都会跑一遍# 模拟断充并手动进入 Doze adb shell dumpsys battery unplug adb shell dumpsys deviceidle force-idle # 查询目标应用所在的待机分组 adb shell am get-standby-bucket com.example.app # 强制把应用放到罕用分组 adb shell am set-standby-bucket com.example.app rare # 进入网络不可用状态模拟弱网/断网 adb shell settings put global airplane_mode_on 1 adb shell am broadcast -a android.intent.action.AIRPLANE_MODE # 退出 Doze 并恢复环境 adb shell dumpsys deviceidle unforce adb shell dumpsys battery reset adb shell settings put global airplane_mode_on 0测试的核心不是看任务是否失败而是看任务在系统恢复后能不能自动补齐。把任务丢到 rare 分组后锁屏等 10 分钟回前台如果任务自动重试成功说明你的“任务恢复”机制是合格的如果任务直接丢了就要回去检查是不是缺少持久化记录。6.2 真机测试矩阵至少覆盖原生 三家厂商 ROMAOSP 的测试结果只能代表系统兼容性的及格线真机矩阵还得覆盖主流厂商 ROM。我自己一般会保证至少有这几类测试设备设备类型覆盖问题最低要求原生 Android 9 / 10Doze 与待机分组标准行为必测某一台国产 ROM 新机厂商自启动管理、后台清理策略至少覆盖华为或小米另一台国产 ROM 老机老版本系统 厂商旧策略的叠加覆盖 OPPO 或 vivo真机测试的步骤重复性很高每次都手动操作不现实我会把“锁屏 → 等待 5 分钟 → 查看任务结果”固化成脚本利用 adb 批量执行。这里特别提醒一点测试时要真的拔掉 USB 线或者断开充电我第一次跑厂商 ROM 测试时就是因为插着数据线Doze 从来没触发过白白浪费了半天。另外测试时 targetSdk 的影响要区分清楚后台限制主要看设备系统版本明文流量限制同时看系统版本和 targetSdk。也就是说哪怕你把 targetSdk 降到 27在 Android 9 设备上照样会受 Doze 和待机分组约束你并不能靠降低 targetSdk 绕过后台限制。6.3 线上监控给后台任务单独建一张成功率报表后台任务有一个特点用户不主动反馈就不会有人发现。所以我建议在上报数据里加一个场景字段区分“前台发起”和“后台发起”然后单独统计后台任务的成功率。实现思路很简单在任务发起时记录当前亮屏状态和是否充电比如用 PowerManager 判断isInteractive()和isDeviceIdleMode()把这两个值固化到上报字段里。这样你就能在监控后台里看到“后台同步成功率”这个指标而不是混在前台请求数据里看不出问题。这个指标的价值在于它能帮你定位系统升级带来的隐性变化。比如某天你把 targetSdk 从 29 升到 33前台请求一切正常但后台任务成功率从 95% 掉到 70%那基本可以断定是新的后台限制生效了需要重新执行一遍上面说的 Doze 和厂商 ROM 测试流程。没有这个指标这类问题可能要等用户投诉才会暴露。我在实际项目中的体会是后台网络请求被限制这件事永远不要指望一次适配永久有效。Android 每个大版本都在收紧后台能力厂商策略也在持续调整。与其花精力去对抗系统、研究各种灰色保活手段不如把任务设计成“可延迟、可恢复、可重试”的形态让系统在什么时候执行都由它说了算但你的代码要保证任务最终能被执行完。发布前专门安排一个迭代去跑真机测试矩阵把 Doze、待机分组、厂商后台管理这些场景全部过一遍能挡掉绝大部分线上事故。
返回列表