为什么 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 守住传输边界,服务端幂等则避免重试把数据写脏。