背景
如图,RequestBody 有好几个 create 方法,可以满足不同场景下的内容上传,比如字符串、字节数组和文件。
![](https://img.haomeiwen.com/i2767475/0a384a4c63d6f15b.png)
显然,字符串和字节数组是不能上传大文件的,均可能 OOM。
- 大文件编码成字符串(比如 Bsase64)也会得到一个巨大的字符串
- 大文件存入字节数组也会是一个巨大的字节数组
那么,就只能使用 RequestBody create(MediaType contentType, File file)
方法了。正常情况下也是没什么问题的,但是在 Android Q 上,由于存储权限的变更,将导致无法直接访问从内容库所选择的文件。
- 拿到 uri, 得到 absolutePath, 生成 File,开始上传
class AndroidQUploadBigFileActivity : BaseActivity() {
private lateinit var getContentLauncher: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
thread {
LogUtils.d("content uri: $it")
val file = UriUtils.uri2File(it)
LogUtils.d("real path: ${file.absolutePath}")
// 直接访问原始文件
try {
val fis = FileInputStream(file)
LogUtils.d("file length: ${fis.available()}")
} catch (e: Exception) {
LogUtils.d("access failed: ${e.message}")
}
}
}
}
Button(this).apply {
text = "Select a file"
keepScreenOn = true
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setContentView(this)
setOnClickListener {
getContentLauncher.launch("image/*")
}
}
}
}
得到如下日志:
content uri: content://com.android.providers.media.documents/document/image%3A50163
real path: /storage/emulated/0/DCIM/Camera/IMG_20220510_072013.jpg
access failed: /storage/emulated/0/DCIM/Camera/IMG_20220510_072013.jpg: open failed: EACCES (Permission denied)
可见,即使通过 uri 得到了文件的真实路径,也是无法直接访问的。
解决
通过上面的实验可以看到,我们是无法直接通过 File 相关的 API
访问原始文件的,但是我们却可以通过 ContentResolver
得到原始文件的流。
- 对于小文件,可以通过该流将文件复制一份到内部空间,再通过
RequestBody create(MediaType contentType, File file)
进行上次。 - 但是对于大文件,再拷贝一份就真实费事费力了,直接对流进行处理即可,重点参考
MyRequestBody
。
class AndroidQUploadBigFileActivity : BaseActivity() {
private lateinit var getContentLauncher: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.call().request()
val requestBody = (request.body() as MultipartBody).part(0).body()
val name = request.header("FileName") ?: "Default.zip"
val file = File(externalCacheDir, name)
LogUtils.d("saved file path: ${file.absolutePath}")
val sink = Okio.buffer(Okio.sink(file))
sink.use {
requestBody.writeTo(sink)
}
LogUtils.d("saved file length: ${file.length()}")
val md5 = EncryptUtils.encryptMD5File2String(file)
LogUtils.d("saved file md5: $md5")
val responseBody = ResponseBody.create(MediaType.parse("application/json"), "OK")
return@addInterceptor Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(responseBody)
.message("OK")
.build()
}
.build()
getContentLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.let {
thread {
LogUtils.d("content uri: $it")
val file = UriUtils.uri2File(it)
LogUtils.d("real path: ${file.absolutePath}")
// 直接访问原始文件
try {
val fis = FileInputStream(file)
LogUtils.d("file length: ${fis.available()}")
} catch (e: Exception) {
LogUtils.d("access failed: ${e.message}")
}
// 通过流上传
val body = MultipartBody.Builder()
.addFormDataPart("name", "filename", MyRequestBody(contentResolver, it))
.build()
val request = Request.Builder()
.url("https://www.baidu.com")
.header("FileName", "Test.zip")
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
LogUtils.d("onFailure: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
LogUtils.d("onResponse: ${response.body()?.string()}")
}
})
}
}
}
Button(this).apply {
text = "Select a file"
keepScreenOn = true
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setContentView(this)
setOnClickListener {
getContentLauncher.launch("*/*")
}
}
}
}
private class MyRequestBody(val contentResolver: ContentResolver, val uri: Uri) : RequestBody() {
override fun contentType(): MediaType? {
return MediaType.get("multipart/form-data")
}
override fun writeTo(sink: BufferedSink) {
contentResolver.openFileDescriptor(uri, "r")?.let { fd ->
Okio.source((FileInputStream(fd.fileDescriptor))).use {
sink.writeAll(it)
}
}
}
}
引申
类似地,可以基于流或者 FileDescriptor 对图片解码成 Bitmap,参考:
- public static Bitmap decodeFileDescriptor(FileDescriptor fd)
- public static Bitmap decodeStream(InputStream is)
网友评论