WorkManager实战:图片上传任务
图片上传,几乎是每个App都会遇到的需求。但说实话,很多开发者对它的处理方式,就是简单丢个OkHttp请求完事。用户切到后台,上传断了;网络波动,上传失败;App被杀,一切重来……
我在项目中就踩过这个坑。当时做一个社交App,用户拍完照片,后台一压,上传队列全崩了。产品经理追着我问:「为什么用户发个图这么难?」嗯,从那以后,我开始认真研究WorkManager来做图片上传。
创建图片上传Worker
先看最基础的部分——如何定义一个图片上传的Worker。我习惯把Worker设计成带状态的,而不是一股脑往里塞逻辑。
class ImageUploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// 1. 获取输入数据
val imageUri = inputData.getString("image_uri") ?: return Result.failure()
val imageId = inputData.getString("image_id") ?: return Result.failure()
// 2. 读取文件
val file = try {
UriUtils.getFileFromUri(applicationContext, Uri.parse(imageUri))
} catch (e: Exception) {
return Result.retry()
}
// 3. 执行上传
return uploadImage(imageId, file)
}
private suspend fun uploadImage(imageId: String, file: File): Result {
return withContext(Dispatchers.IO) {
try {
val response = ApiService.upload(imageId, file)
if (response.isSuccessful) {
Result.success()
} else {
Result.retry()
}
} catch (e: IOException) {
Result.retry()
}
}
}
}
这里有个细节:CoroutineWorker 默认跑在 Dispatchers.Default 上。但网络请求我建议切到 Dispatchers.IO,毕竟IO密集型操作,别占着CPU线程不放。
处理网络请求
网络请求这块,我推荐用Retrofit + OkHttp。但要注意,WorkManager的Worker生命周期和Activity不同,你不能指望用户一直在前台。
核心原则:Worker里的网络请求,必须能独立运行,不依赖任何UI组件。
object ApiService {
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.build()
private val api = retrofit.create(UploadApi::class.java)
suspend fun upload(imageId: String, file: File): Response<UploadResponse> {
val requestBody = file.asRequestBody("image/jpeg".toMediaType())
val part = MultipartBody.Part.createFormData("image", file.name, requestBody)
return api.uploadImage(imageId, part)
}
}
interface UploadApi {
@Multipart
@POST("upload/image")
suspend fun uploadImage(
@Part("image_id") imageId: RequestBody,
@Part image: MultipartBody.Part
): Response<UploadResponse>
}
我曾经犯过一个错——把超时时间设得太短。用户用4G网络上传大图,动不动就超时重试。后来我把writeTimeout调到60秒,配合分片上传,问题就解决了。
上传进度反馈
用户最关心什么?进度。但WorkManager本身不提供进度回调给UI。怎么办?用 setProgress() 方法。
class ImageUploadWorker(...) : CoroutineWorker(...) {
override suspend fun doWork(): Result {
// ... 省略前面的代码
// 创建进度监听器
val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(bytesWritten: Long, contentLength: Long) {
val progress = (bytesWritten * 100 / contentLength).toInt()
// 更新进度
setProgress(workDataOf("progress" to progress))
}
}
// 包装RequestBody
val requestBody = file.asRequestBody("image/jpeg".toMediaType())
val progressBody = ProgressRequestBody(requestBody, progressListener)
// ... 上传逻辑
}
}
UI层怎么获取进度?通过 WorkManager.getWorkInfoByIdLiveData() 观察即可。
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(workId)
.observe(viewLifecycleOwner) { workInfo ->
if (workInfo != null && workInfo.progress != null) {
val progress = workInfo.progress.getInt("progress", 0)
binding.progressBar.progress = progress
}
}
小技巧:进度更新不要太频繁。我一般控制在每5%更新一次,或者每秒最多更新10次。否则频繁的跨进程通信反而会影响性能。
失败重试策略
上传失败是常态。网络波动、服务器限流、Token过期……你想想看,哪个没遇到过?
WorkManager提供了非常灵活的重试策略。我一般这样配置:
val uploadRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>()
.setInputData(inputData)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.addTag("image_upload")
.build()
这里我用了指数退避策略。第一次失败等30秒,第二次等60秒,第三次等120秒……最大延迟由系统控制。为什么要用指数退避?说白了,如果服务器正在重启,你每秒重试一次,只会加重负担。
注意:重试次数不是无限的。默认情况下,WorkManager会重试3次(加上首次尝试,共4次)。如果需要更多,可以在Worker里手动返回 Result.retry(),但建议不要超过10次。
断点续传实现
这才是真正的硬骨头。大图上传,断了重传整个文件?太浪费了。我分享一个我在生产环境用过的方案。
核心思路:把文件切成固定大小的分片,记录每个分片的上传状态。
class ChunkedImageUploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
private val chunkSize = 512 * 1024 // 512KB每片
override suspend fun doWork(): Result {
val imageUri = inputData.getString("image_uri") ?: return Result.failure()
val imageId = inputData.getString("image_id") ?: return Result.failure()
val file = UriUtils.getFileFromUri(applicationContext, Uri.parse(imageUri))
val totalChunks = (file.length() + chunkSize - 1) / chunkSize
// 获取已上传的分片列表
val uploadedChunks = getUploadedChunks(imageId)
// 遍历所有分片
for (chunkIndex in 0 until totalChunks) {
if (chunkIndex in uploadedChunks) continue // 已上传的跳过
val start = chunkIndex * chunkSize
val end = minOf(start + chunkSize, file.length())
val chunkData = file.readBytes().copyOfRange(start.toInt(), end.toInt())
// 上传单个分片
val result = uploadChunk(imageId, chunkIndex, totalChunks, chunkData)
if (!result) {
// 记录当前进度,下次从断点继续
saveUploadProgress(imageId, chunkIndex)
return Result.retry()
}
}
// 所有分片上传完成,通知服务端合并
return mergeChunks(imageId, totalChunks)
}
private suspend fun uploadChunk(
imageId: String,
chunkIndex: Long,
totalChunks: Long,
data: ByteArray
): Boolean {
return withContext(Dispatchers.IO) {
try {
val response = ApiService.uploadChunk(
imageId, chunkIndex, totalChunks, data
)
response.isSuccessful
} catch (e: Exception) {
false
}
}
}
private fun getUploadedChunks(imageId: String): Set<Long> {
// 从本地数据库或SharedPreferences读取已上传的分片索引
val prefs = applicationContext.getSharedPreferences("upload_progress", 0)
val uploaded = prefs.getStringSet(imageId, emptySet()) ?: emptySet()
return uploaded.map { it.toLong() }.toSet()
}
private fun saveUploadProgress(imageId: String, chunkIndex: Long) {
val prefs = applicationContext.getSharedPreferences("upload_progress", 0)
val uploaded = prefs.getStringSet(imageId, mutableSetOf()) ?: mutableSetOf()
uploaded.add(chunkIndex.toString())
prefs.edit().putStringSet(imageId, uploaded).apply()
}
}
关键点:断点续传的核心是「记录已上传的分片」。我用SharedPreferences做演示,生产环境建议用Room数据库,更可靠。
为什么不用文件来记录?我曾经试过,结果App被系统杀掉时,文件写入可能不完整,导致记录损坏。数据库有事务保证,安全得多。
整体架构图
下面这张图,是我整理出来的图片上传任务整体流程。你看一眼就能明白各个模块怎么配合。
从这张图你能看到,整个流程分四层。UI层只管观察进度,不参与实际上传。WorkManager负责调度和约束条件。Worker层是核心,处理分片、进度、重试。网络层只做一件事——发请求。
这种分层设计的好处是:每一层都可以独立测试、独立修改。我在重构旧项目时,就把原来耦合在Activity里的上传逻辑,按这个架构拆开,代码清晰了很多。
好了,图片上传Worker的核心内容就这些。从创建Worker、处理网络请求,到进度反馈、失败重试,再到断点续传,每一步都有坑,但也都有解法。你动手试试,遇到问题再回来翻翻这篇文章。
最后提醒一句:生产环境别忘了处理权限问题。Android 11以上,读取图片需要 READ_MEDIA_IMAGES 权限,WorkManager不会帮你处理权限申请,你得在调度任务前确保权限已授予。