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

如何用Retrofit2上传仅持只读权限Uri的非自有大文件

直接通过只读Uri用Retrofit2上传大文件

当前常规实现流程

我现在的应用选择照片等文件时,用以下代码获取文件Uri:

private val imageFileFromGalleryPickResult =
    registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { imageUri ->
        imageUri?.let { }
    }

之后会把文件保存到应用私有缓存目录,方便后续修改(比如调整分辨率、修正方向):

suspend fun savePublicFileToAppCacheDir(context: Context, fileUri: Uri): File {
    val appCacheDir = ContextCompat.getExternalCacheDirs(context)
        .filterNotNull()
        .first()
        .also { it.mkdirs() }
    val file = File(appCacheDir, "temp_file.jpg")
    context.contentResolver.openInputStream(fileUri)!!.use { input ->
        FileOutputStream(file).use { output ->
            output.write(input.readBytes())
        }
    }
    return file
}

通过Retrofit2上传这个私有文件很简单,因为应用拥有文件所有权:

fun buildMultipartBodyPart(file: File, paramName: String): MultipartBody.Part {
    val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
    return MultipartBody.Part.createFormData(paramName, file.name, requestFile)
}

@Multipart
@POST("user/profile/photo")
suspend fun sendProfilePhoto(
    @Part photoFile: MultipartBody.Part,
)

问题

但对于无需复制到应用目录的文件(尤其是50MB以上的大文件),仅持有只读权限的Uri时,如何直接通过Retrofit2上传?是否可以通过InputStream实现?

更新
我猜测可以用input.readBytes()结合ByteArray.toRequestBody()实现,但大文件的内存占用问题还是没解决。

解决方案

直接用readBytes()会把整个文件加载到内存,大文件必然导致OOM,正确的做法是自定义RequestBody,基于InputStream分块读取:

1. 实现自定义InputStreamRequestBody

import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source
import java.io.InputStream

class InputStreamRequestBody(
    private val inputStream: InputStream,
    private val contentType: MediaType?,
    private val contentLength: Long? = null
) : RequestBody() {

    override fun contentType(): MediaType? = contentType

    override fun contentLength(): Long = contentLength ?: -1

    override fun writeTo(sink: BufferedSink) {
        inputStream.source().use { source ->
            sink.writeAll(source)
        }
    }
}

2. 构建MultipartBody.Part

从Uri获取InputStream,传入自定义RequestBody:

fun buildMultipartFromUri(
    context: Context,
    uri: Uri,
    paramName: String,
    fileName: String,
    mediaType: MediaType?
): MultipartBody.Part {
    val inputStream = context.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException("无法打开Uri输入流")
    // 可选:获取文件大小,避免contentLength返回-1
    val contentLength = context.contentResolver.openFileDescriptor(uri, "r")?.use { it.statSize }
    val requestBody = InputStreamRequestBody(inputStream, mediaType, contentLength)
    return MultipartBody.Part.createFormData(paramName, fileName, requestBody)
}

3. 调用Retrofit接口

和之前的调用方式一致:

// 示例:从Uri构建Part并上传
val imageUri = // 从系统选择器拿到的Uri
val photoPart = buildMultipartFromUri(
    context = this,
    uri = imageUri,
    paramName = "photo",
    fileName = "uploaded_image.jpg", // 可通过ContentResolver获取真实文件名,此处为简化示例
    mediaType = "image/jpeg".toMediaTypeOrNull()
)
api.sendProfilePhoto(photoPart)

关键说明

  • 自定义RequestBody通过Okio的source()和writeAll()实现分块读取,不会一次性加载整个文件到内存,彻底解决大文件OOM问题
  • 可通过ContentResolver获取文件大小,让服务器知晓文件总长度,提升上传交互体验
  • 输入流会在writeTo方法内通过use自动关闭,无需手动处理

内容的提问来源于stack exchange,提问作者user924

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 16:02:40