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 网络约束的知识图谱
下面这张图帮你理清各类型之间的关系和适用场景:
这张图里我特意加了一个 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 之前,先想想你的任务对流量和网络质量的敏感度。用对了,用户体验和手机电量都会感激你。用错了……等着被用户差评吧。