Retrofit MultiPartBody无法上传大尺寸图片问题求助
Android大图片(2MB以上)上传失败问题排查与修复
我可以通过ANDROID_GET_CONTENT获取图片并加载,但仅2MB以上的图片无法上传,2MB及以下的图片可正常上传。以下是实现代码及上传日志:
API接口定义
@Multipart @POST("v1/product...") suspend fun productImage( @Part files: MultipartBody.Part, @Part("imageKey") imageKey: String ): ImageResponse
Uri转MultipartBody.Part扩展函数
fun Uri.progressRequestBody( context: Context?, percentageHandler: (Int) -> Unit ): MultipartBody.Part? { val name = getImageNameAndSize(context) val progressRequestBody = context?.contentResolver?.openInputStream(this) ?.use { val byte = it.readBytes() println("ImageUploadLog byte.size: ${byte.size}") val body = byte.toRequestBody( contentType = "multipart/form-data".toMediaTypeOrNull() ) ProgressRequestBody( requestBody = body, progressHandler = percentageHandler ) } return MultipartBody.Part .createFormData("files", name?.first, progressRequestBody ?: return null) }
ProgressRequestBody类
class ProgressRequestBody( private val requestBody: RequestBody, private val progressHandler: (Int) -> Unit ) : RequestBody() { companion object { private const val progressUpdatesToSkip = 2 } private var writeCount = 0 override fun contentType(): MediaType? = requestBody.contentType() override fun contentLength(): Long = requestBody.contentLength() override fun writeTo(sink: BufferedSink) { println("ImageUploadLog writeCount: $writeCount, progressUpdatesToSkip: $progressUpdatesToSkip") if (writeCount >= progressUpdatesToSkip) { val countingSink = CountingSink(sink).buffer() requestBody.writeTo(countingSink) countingSink.flush() } else { requestBody.writeTo(sink) } writeCount++ } private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) { private val handler = Handler(Looper.getMainLooper()) private val total = contentLength() private var uploaded = 0L init { println("ImageUploadLog CountingSink init") } override fun write(source: Buffer, byteCount: Long) { println("ImageUploadLog write") super.write(source, byteCount) println("ImageUploadLog previous uploaded: $uploaded") uploaded += byteCount println("ImageUploadLog new uploaded: $uploaded, byteCount: $byteCount") handler.post { val percentage = (100 * (uploaded.toFloat() / total.toFloat())).toInt() println("ImageUploadLog percentage: $percentage") progressHandler(percentage) } } } }
上传日志
1) 74KB图片上传(成功)
ImageUploadLog byte.size: 7417 ImageUploadLog writeCount: 0, progressUpdatesToSkip: 2 ImageUploadLog writeCount: 1, progressUpdatesToSkip: 2 ImageUploadLog writeCount: 2, progressUpdatesToSkip: 2 ImageUploadLog CountingSink init ImageUploadLog write ImageUploadLog previous uploaded: 0 ImageUploadLog new uploaded: 7417, byteCount: 7417 ImageUploadLog percentage: 100
2) 4.7MB图片上传(失败)
ImageUploadLog byte.size: 4788195 ImageUploadLog writeCount: 0, progressUpdatesToSkip: 2 ImageUploadLog writeCount: 1, progressUpdatesToSkip: 2 ImageUploadLog writeCount: 2, progressUpdatesToSkip: 2 ImageUploadLog CountingSink init ImageUploadLog write
问题分析
从日志和代码能看出几个核心问题:
- 内存占用过高:用
it.readBytes()一次性把整张大图片读进内存,4.7MB的图片会直接占用近5MB内存,后续上传时还会重复处理,容易触发内存不足导致上传中断。 - ProgressRequestBody逻辑错误:
writeCount是成员变量,上传一次后不会重置,而且前两次跳过进度监听的逻辑完全多余,大图片分块写入时会因为这个逻辑出现异常。 - 进度更新过于频繁:大图片分块写入时,每次都往主线程post任务,会挤占主线程资源,甚至导致上传流程卡顿中断。
修复方案
方案1:流式上传,避免一次性加载全图到内存
直接基于InputStream创建RequestBody,流式读取上传,大幅降低内存占用:
fun Uri.progressRequestBody( context: Context?, percentageHandler: (Int) -> Unit ): MultipartBody.Part? { val name = getImageNameAndSize(context) // 获取图片真实MIME类型,避免用固定的multipart/form-data val contentType = context?.contentResolver?.getType(this)?.toMediaTypeOrNull() ?: "image/*".toMediaTypeOrNull() val requestBody = object : RequestBody() { override fun contentType(): MediaType? = contentType override fun contentLength(): Long { return context?.contentResolver?.openInputStream(this@progressRequestBody)?.use { it.available().toLong() } ?: -1 } override fun writeTo(sink: BufferedSink) { context?.contentResolver?.openInputStream(this@progressRequestBody)?.use { inputStream -> val buffer = ByteArray(4096) // 4KB缓冲区,平衡效率和内存 var bytesRead: Int var totalBytesRead = 0L val totalLength = contentLength() while (inputStream.read(buffer).also { bytesRead = it } != -1) { sink.write(buffer, 0, bytesRead) totalBytesRead += bytesRead // 仅当总长度有效时更新进度,避免除以0 if (totalLength > 0) { val percentage = (100 * (totalBytesRead.toFloat() / totalLength.toFloat())).toInt() Handler(Looper.getMainLooper()).post { percentageHandler(percentage) } } } sink.flush() } } } return MultipartBody.Part.createFormData("files", name?.first, requestBody) }
方案2:修正ProgressRequestBody逻辑(保留进度监听类)
如果要继续使用ProgressRequestBody,需要重置计数器并优化进度更新:
class ProgressRequestBody( private val requestBody: RequestBody, private val progressHandler: (Int) -> Unit ) : RequestBody() { override fun contentType(): MediaType? = requestBody.contentType() override fun contentLength(): Long = requestBody.contentLength() override fun writeTo(sink: BufferedSink) { // 去掉多余的跳过逻辑,直接使用计数Sink val countingSink = CountingSink(sink).buffer() requestBody.writeTo(countingSink) countingSink.flush() } private inner class CountingSink(delegate: Sink) : ForwardingSink(delegate) { private val handler = Handler(Looper.getMainLooper()) private val total = contentLength() private var uploaded = 0L private var lastReportedPercentage = -1 // 避免重复发送相同进度 override fun write(source: Buffer, byteCount: Long) { super.write(source, byteCount) uploaded += byteCount if (total > 0) { val percentage = (100 * (uploaded.toFloat() / total.toFloat())).toInt() // 只有进度变化时才通知,减少主线程任务 if (percentage != lastReportedPercentage) { lastReportedPercentage = percentage handler.post { progressHandler(percentage) } } } } } }
同时修改扩展函数,不再一次性读取全图:
fun Uri.progressRequestBody( context: Context?, percentageHandler: (Int) -> Unit ): MultipartBody.Part? { val name = getImageNameAndSize(context) val contentType = context?.contentResolver?.getType(this)?.toMediaTypeOrNull() ?: "image/*".toMediaTypeOrNull() val requestBody = context?.contentResolver?.openInputStream(this)?.use { inputStream -> // 用ByteString流式读取,避免一次性加载全图 inputStream.source().buffer().readByteString().toRequestBody(contentType) } ?: return null return MultipartBody.Part.createFormData("files", name?.first, ProgressRequestBody(requestBody, percentageHandler) ) }
额外检查项
- 确认后端是否设置了文件大小限制,确保允许上传的最大尺寸大于2MB;
- 调整OkHttp超时时间,大图片上传需要更长时间,建议设置为30秒以上:
val okHttpClient = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build()
内容的提问来源于stack exchange,提问作者Ümit Bülbül
相关产品推荐
相关产品推荐

