美文网首页
Android Q 上基于 OkHttp 上传(大)文件的实现

Android Q 上基于 OkHttp 上传(大)文件的实现

作者: 雁过留声_泪落无痕 | 来源:发表于2022-05-10 14:51 被阅读0次

背景

如图,RequestBody 有好几个 create 方法,可以满足不同场景下的内容上传,比如字符串、字节数组和文件。


RequestBody#create() 方法

显然,字符串和字节数组是不能上传大文件的,均可能 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)

参考

相关文章

网友评论

      本文标题:Android Q 上基于 OkHttp 上传(大)文件的实现

      本文链接:https://www.haomeiwen.com/subject/qaklurtx.html