如何用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
相关产品推荐
相关产品推荐

