为什么 Android 图片上传经常线上翻车
测试机上能成功,不代表真实用户也稳定。弱网、切后台、系统杀进程、网络切换,都会让“页面里直接发请求”的写法暴露问题:上传丢失、重复上传、后台中断。
稳妥做法是:UI 只发起任务,WorkManager 负责调度与重试,长任务切到前台通知,网络层默认 HTTPS,服务端用幂等键防止重复写入。
第一步:用 WorkManager 托管上传
Android 官方把 WorkManager 作为持久化后台任务的推荐方案。图片上传只要可能超过几十秒、需要切后台继续,或者失败后要自动重试,就不该只绑在页面生命周期里。
给每次逻辑上传生成稳定 uploadId,再用唯一任务入队,避免连点和页面重建造成重复排队:
val request = OneTimeWorkRequestBuilder<ImageUploadWorker>()
.setInputData(workDataOf("uploadId" to uploadId, "filePath" to filePath))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"image-upload-$uploadId",
ExistingWorkPolicy.KEEP,
request
)
KEEP 表示同一个上传任务只保留一个进行中的实例。
第二步:长上传必须给前台通知
长时间 Worker 可以调用 setForeground()。弱网上传适合这样做:
override suspend fun doWork(): Result {
setForeground(createForegroundInfo("正在上传图片"))
return runCatching { uploadOnce() }
.fold(
onSuccess = { Result.success() },
onFailure = { if (runAttemptCount < 3) Result.retry() else Result.failure() }
)
}
如果应用目标版本是 Android 14(API 34)及以上,长时间 Worker 还必须声明前台服务类型。图片上传一般可使用 dataSync:
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
第三步:把执行条件写进约束
头像、单张凭证照通常 CONNECTED 就够;相册备份、批量同步更适合 UNMETERED + RequiresCharging:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.build()
约束变化时,WorkManager 可能中断当前任务并稍后重排,因此上传代码必须可重入。
第四步:正式环境默认 HTTPS
线上图片上传不该继续依赖 http://。可以用 Network Security Config 把“默认拒绝明文、仅对调试域名单独放行”写成显式配置:
<application
android:usesCleartextTraffic="false"
android:networkSecurityConfig="@xml/network_security_config" />
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
第五步:客户端重试,服务端一定要幂等
只做 Result.retry() 还不够。超时场景里,客户端可能认为失败了,但服务端已经保存成功;此时再传一次,就会生成重复图片或重复记录。
解决办法是把同一个 uploadId 带到服务端,例如放在 X-Upload-Id 请求头中。服务端按这个键做幂等。OkHttp 组装 multipart 也直接:
val body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("uploadId", uploadId)
.addFormDataPart("image", file.name, file.asRequestBody("image/jpeg".toMediaType()))
.build()
常见避坑点
- 上传逻辑绑死在页面生命周期里,界面一销毁任务就跟着丢。
- 长任务没有前台通知,大文件上传时最容易被系统限制。
- 有重试,没有幂等,最终把数据写脏。
总结
可靠上传的关键,不是“接口能不能通”,而是它能否穿过真实设备上的中断和波动。WorkManager 负责让任务跨重启继续存在,前台通知提高长任务存活率,HTTPS 与 Network Security Config 守住传输边界,服务端幂等则避免重试把数据写脏。