如何使用Retrofit Multipart上传500MB以上大文件并解决OOM问题
根因分析
你遇到的所有OOM问题的核心原因是将整个大文件的内容一次性加载到了应用堆内存中:inputStream.readBytes()、ByteArray.copyOfRange这类操作都会把500MB级的文件全量/多份拷贝到Java堆中,而安卓应用的堆上限通常只有几百MB,即便开启largeHeap也无法承载500MB级的全量文件加载。Postman可以正常上传大文件的原因是它采用流式传输逻辑,不会将整个文件加载到内存,只会在传输时读取当前需要的缓冲区内容。
解决方案
无需获取文件本地路径,也无需创建临时文件,直接基于文件选择返回的Uri实现流式RequestBody即可,OkHttp原生支持流式传输,不会加载全量文件到内存。
第一步:实现流式UriRequestBody
import android.content.ContentResolver import android.net.Uri import android.provider.OpenableColumns import okhttp3.MediaType import okhttp3.RequestBody import okio.BufferedSink import okio.source import java.io.IOException class UriRequestBody( private val contentResolver: ContentResolver, private val uri: Uri, private val fileName: String ) : RequestBody() { override fun contentType(): MediaType? { // 从Uri直接获取文件MIME类型 return contentResolver.getType(uri)?.toMediaTypeOrNull() ?: "application/octet-stream".toMediaTypeOrNull() } override fun contentLength(): Long { // 从ContentResolver查询文件大小,无需读取文件内容 return try { contentResolver.query(uri, null, null, null, null)?.use { cursor -> val sizeIndex = cursor.getColumnIndexOrThrow(OpenableColumns.SIZE) cursor.moveToFirst() cursor.getLong(sizeIndex) } ?: super.contentLength() } catch (e: Exception) { super.contentLength() } } @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { // 流式读写,每次只读取缓冲区大小的内容,不会加载全量文件到内存 contentResolver.openInputStream(uri)?.use { inputStream -> inputStream.source().use { source -> sink.writeAll(source) } } ?: throw IOException("无法打开指定Uri对应的文件") } fun getFileName(): String = fileName }
第二步:改造上传逻辑
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.CoroutineScope import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import java.util.concurrent.TimeUnit import okhttp3.Protocol private fun uploadLargeFile(uri: Uri, fileName: String) { // 不要用GlobalScope,建议替换为页面绑定的ViewModel的viewModelScope CoroutineScope(Dispatchers.IO).launch { val client = OkHttpClient.Builder() .protocols(listOf(Protocol.HTTP_1_1)) .connectTimeout(10, TimeUnit.MINUTES) .readTimeout(10, TimeUnit.MINUTES) .writeTimeout(10, TimeUnit.MINUTES) // 大文件上传必须配置写超时 .build() val multipartBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( "data", fileName, UriRequestBody(contentResolver, uri, fileName) ) .build() val request = Request.Builder() .url("你的上传接口地址") .post(multipartBody) .build() runCatching { client.newCall(request).execute() }.onSuccess { response -> // 处理上传成功逻辑 println("上传响应:${response.body?.string()}") }.onFailure { e -> // 处理上传失败逻辑 e.printStackTrace() } } }
第三步:文件选择回调中直接调用
private val filesReceiver = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { val fileUri = result.data?.data ?: return@registerForActivityResult // 查询文件名 val fileName = contentResolver.query(fileUri, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) cursor.moveToFirst() cursor.getString(nameIndex) } ?: "unknown_file" // 直接调用上传,无需处理临时文件、路径转换 uploadLargeFile(fileUri, fileName) } }
优化建议
如果后端支持分片上传,建议进一步改造为分片上传逻辑:每次只读4MB左右的文件内容上传,传完即释放内存,中途断网可续传,无需重传整个文件,稳定性更高。
内容的提问来源于stack exchange,提问作者Andrew
相关产品推荐
相关产品推荐

