You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题分析

从日志和代码能看出几个核心问题:

  1. 内存占用过高:用it.readBytes()一次性把整张大图片读进内存,4.7MB的图片会直接占用近5MB内存,后续上传时还会重复处理,容易触发内存不足导致上传中断。
  2. ProgressRequestBody逻辑错误:writeCount是成员变量,上传一次后不会重置,而且前两次跳过进度监听的逻辑完全多余,大图片分块写入时会因为这个逻辑出现异常。
  3. 进度更新过于频繁:大图片分块写入时,每次都往主线程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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 04:25:00