3. 网络约束:NetworkType 的全面拆解

网络约束,是我在 WorkManager 里用得最多的一个约束。说实话,没加网络约束的任务,就像没系安全带的赛车——你敢跑,用户就敢投诉。

为什么要专门开一节讲 NetworkType?因为很多人只用过 CONNECTED,觉得「连上网就行」。但 Android 的网络类型比你想的复杂:有 WiFi、移动数据、热点、漫游……每个场景对流量计费的态度都不一样。WorkManager 给了我们 5 种网络类型选择,用对了省流量、保体验,用错了要么任务不执行,要么悄悄烧掉用户的话费。

3.1 NetworkType 枚举一览

WorkManager 的 NetworkType 是一个枚举,定义在 androidx.work 包里。咱们先看它都包含哪些值:

枚举值 含义 适用于 Android 版本要求
CONNECTED 只要设备连着任何网络(WiFi 或移动数据),就满足约束 大部分联网任务,不关心计费 API 14+
UNMETERED 仅当设备连接的是「非计量网络」(通常指 WiFi,但不包括移动热点) 大文件下载、视频预缓存、日志上传 API 14+
METERED 仅当设备连接的是「计量网络」(移动数据、热点等按流量计费的网络) 轻量同步、小数据包请求 API 22+
NOT_REQUIRED 不关心网络状态,有网没网都执行 本地数据库清理、纯 CPU 计算 API 14+
TEMPORARILY_UNMETERED 当前网络临时被标记为非计费(比如运营商限免流量时段) 推送唤醒后快速同步,避免触发计费 API 31+(Android 12)
我个人的习惯:给上传类任务至少配 UNMETERED,但如果用户可能开热点,我会再追加 METERED 做 fallback(用 or 逻辑?不行,NetworkType 是单选。后面你会看到怎么用 Constraints.Builder 配合自定义策略)。

3.2 配置网络约束:代码示例

其实很简单,一行代码就能搞定:

// 创建一个网络约束:要求连接非计费网络
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .build()

val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(constraints)
    .build()

如果你想用 CONNECTED,把 UNMETERED 换掉就行。注意:METERED 在 API 22 以下会抛异常,所以使用前要做版本判断。我记得以前在公司项目里,有个同事直接写了 METERED,结果在 Android 5.0 以下的设备上 crash 了……从那以后我每次用都会加个 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)。

3.3 连接状态:不只是「有网」

WorkManager 的约束检查是基于 ConnectivityManager 回调的。系统会在网络连接建立、断开、类型切换时通知 WorkManager。但是,你需要注意一个细节:

  • 任务只在约束满足时被执行。如果你提交任务时网络不符合,WorkManager 会等,直到满足条件。
  • 网络类型的切换:如果任务正在执行,突然间网络从 WiFi 切到移动数据(比如你拿着手机走出办公室),任务会不会被中断?答案是:不会。WorkManager 只保证任务启动时约束满足,运行时不再检查。除非你设置 setBackoffCriteria 让它重试。

我在项目里遇到过这样一个坑:用户用 WiFi 拍摄了大量照片,任务用 UNMETERED 启动上传,上传过程中 WiFi 断了,自动切换到移动数据。结果任务继续在计费网络下跑完了,用户月底一看流量超标……后来我加了一个 NetworkObserver 自己去监听网络变化,发现网络类型变了就主动取消任务,让 WorkManager 重试。

千万注意:CONNECTED 不等于「有高带宽」。它只保证有连接,不区分是 2G 还是 5G。如果你的任务需要高速网络(比如视频直播),建议在 Worker 内部检测带宽,或者用 TEMPORARILY_UNMETERED 做优化。

3.4 计量网络 vs 非计量网络:背后的直觉

你可能会问:为什么要有 METERED?明明 CONNECTED 就够了啊。

想想看:有些任务你希望它只在「不心疼流量」的时候跑——比如用户在用家中的 WiFi。但有些任务恰恰相反:你希望它在「计费网络」下优先跑?比如用户正在用移动数据,说明他当前在外面,可能更急于同步消息。当然这种场景比较少,但确实存在。

WorkManager 判断「计量网络」的依据是 ConnectivityManager.isActiveNetworkMetered()。大多数情况下,WiFi 返回 false,移动数据返回 true。但有例外:

  • 移动热点:手机开热点时,热点本身被认为是计费网络(因为消耗手机流量)。所以即使连接的是热点 WiFi,也会被判定为 METERED。
  • 企业 WiFi:有些公司内网可能被运营商标记为计费?理论上不会,但曾经有同事反馈过奇怪的现象——后来发现是 VPN 导致的。

避坑指南:我曾经在一个直播项目中,用了 UNMETERED 来触发高清视频下载。结果用户反馈在咖啡厅连了免费热点,任务就是不跑。排查后发现:免费热点属于「计量网络」(因为登录页面向运营商计费)。改成 CONNECTED 后,配合用户手动点击「开始下载」按钮解决了。所以,千万别把 UNMETERED 当成「免费 WiFi」——它只代表系统判断不计费,不代表用户不花钱。

3.5 无网络场景:NOT_REQUIRED 的使用边界

什么时候该用 NOT_REQUIRED?很多初学者把所有后台任务都设成 NOT_REQUIRED,因为「省事」。但这不是好习惯。

我个人建议:只有那些跟网络完全无关的任务才用 NOT_REQUIRED,比如:

  • 本地数据清洗、压缩
  • 计算斐波那契数列(开个玩笑)
  • 上报日志到本地存储,等下次网络好的时候再真正上传

如果你在 doWork() 里调用了网络接口,却配了 NOT_REQUIRED,那么一旦用户在飞行模式下触发了这个任务,网络请求必然失败,然后触发重试——白白浪费 CPU 和电池。

我记得以前有个同事把图片上传任务设为 NOT_REQUIRED,然后在 doWork() 里 try-catch 网络异常,期望「有网就传,没网就等下次」。结果 WorkManager 的重试机制默认是 EXPONENTIAL,几分钟重试一次,用户在地铁里没信号,手机一直在那高频尝试……耗电感人。所以:网络相关任务,务必加上对应约束。

3.6 网络约束的知识图谱

下面这张图帮你理清各类型之间的关系和适用场景:

NetworkType 类型决策树 NetworkType CONNECTED UNMETERED METERED NOT_REQUIRED 任何可用网络 WiFi / 移动数据 不区分计费 非计费网络 通常 WiFi 热点除外 计费网络 移动数据 / 热点 需要 API 22+ 不关心网络 离线任务 慎用联网行为 TEMPORARILY_UNMETERED Android 12+ 特权网络 临时非计费标记

这张图里我特意加了一个 TEMPORARILY_UNMETERED,虽然它使用频率不高,但在 Android 12 上是个好东西。比如某些运营商会在夜间限免流量,WorkManager 能感知到这种临时状态,让你的任务在「白嫖时段」执行。

3.7 综合实战建议

  • 上传日志 / 图片:UNMETERED(如果数据量小,也可用 CONNECTED,但要考虑用户流量)
  • 下载安装包 / 缓存视频:必须 UNMETERED,并在 doWork() 里再次检查 isActiveNetworkMetered(),如果变了就取消
  • 即时聊天消息同步:CONNECTED,因为用户希望尽快收到消息,不在乎那几 KB
  • 本地数据分析:NOT_REQUIRED,完全离线的任务

最后分享一个我踩过的深坑:当时给一个国际漫游场景做应用,发现漫游时网络被系统标记为 METERED(即使连接的是酒店 WiFi)。我用 UNMETERED 导致了任务永远不执行。后来查了 Android 文档才明白:isActiveNetworkMetered 在漫游时可能会返回 true (取决于运营商)。所以如果你有国际场景,建议加一个自定义 NetworkObserver 或者用 CONNECTED 兜底。

好了,网络约束这部分就说到这。记住一句话:选 NetworkType 之前,先想想你的任务对流量和网络质量的敏感度。用对了,用户体验和手机电量都会感激你。用错了……等着被用户差评吧。