class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
companion object {
private const val PRESIGNED_URL = "PRESIGNED_URL"
private const val FILE_PATH = "FILE_PATH"
private const val ATTEMPT_COUNT = "ATTEMPT_COUNT"
private const val MAX_ATTEMPT = 3
fun addUploadTask(
context: Context,
preUrl: String,
filePath: String,
attemptCount: Int = 1
) {
LogUtils.d("UploadWorker addUploadTask $filePath")
// 构建输入参数
val inputData = Data.Builder()
.putString(PRESIGNED_URL, preUrl)
.putString(FILE_PATH, filePath)
.putInt(ATTEMPT_COUNT, attemptCount)
.build()
// 创建 WorkRequest
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(inputData)
.setConstraints(constraints)
.build()
// 提交任务
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
}
}
override suspend fun doWork(): Result {
// 从输入参数中获取预签名 URL 和文件路径
val presignedUrl = inputData.getString(PRESIGNED_URL) ?: return Result.failure()
val filePath = inputData.getString(FILE_PATH) ?: return Result.failure()
val attempt = inputData.getInt(ATTEMPT_COUNT, 1) // 默认从第一次开始
LogUtils.d("UploadWorker addUploadTask doWork attempt=$attempt")
return try {
val file = File(filePath)
val client = OkHttpClient()
val request = Request.Builder()
.url(presignedUrl)
.put(file.asRequestBody("text/plain".toMediaType()))
.build()
val response = client.newCall(request).execute()
LogUtils.d("UploadWorker addUploadTask doWork response=$response")
if (response.isSuccessful) {
file.delete()
Result.success()
} else {
deleteOrRetry(attempt,filePath,presignedUrl)
Result.failure()
}
} catch (e: Exception) {
e.printStackTrace()
deleteOrRetry(attempt,filePath,presignedUrl)
Result.failure()
}
}
private fun deleteOrRetry(attemptCount: Int,filePath: String,preUrl: String,){
if (attemptCount < MAX_ATTEMPT) {
addUploadTask(applicationContext, preUrl, filePath, attemptCount + 1)
} else {
deleteFile(filePath)
}
}
private fun deleteFile(filePath:String){
try {
val file = File(filePath)
if (file.exists()) file.delete()
}catch (e:Exception){
e.printStackTrace()
}
}
}
如果文件很大,大到一个什么范围需要分段上传呢?
-
内存压力:大文件在上传时如果全部加载进内存,容易造成 OOM(Out of Memory)。
-
上传稳定性:网络中断时,如果是整个文件上传,一旦失败就要重头再来,耗时耗流量。
-
可控性更强:可以做断点续传、上传进度监控、更好地用户体验。
-
<10MB:直接上传通常没问题
-
10MB - 50MB:需要视设备内存情况和网络稳定性决定
-
>50MB:推荐强烈使用分段上传
class StreamingFileRequestBody(
private val file: File,
private val contentType: MediaType,
private val bufferSize: Int = DEFAULT_BUFFER_SIZE // 可自定义缓存大小
) : RequestBody() {
override fun contentType(): MediaType = contentType
override fun contentLength(): Long = file.length()
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { input ->
val buffer = ByteArray(bufferSize)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } >= 0) {
sink.write(buffer, 0, bytesRead)
}
}
}
}
val request = Request.Builder()
.url(presignedUrl)
.put(
StreamingFileRequestBody(
file = file,
contentType = "application/octet-stream".toMediaType(),
bufferSize = 8192 // 8KB buffer,默认就很合理
)
)
.build()
val response = OkHttpClient().newCall(request).execute()