将大型视频文件读取到字节数组时出现 OutOfMemoryException

OutOfMemoryException when reading large VideoFile to ByteArray

所以在搜索 Whosebug 数小时后,我尝试了很多方法和库,但没有一个是我问题的答案。

问题如下:

val videoPath = getVideoPathFromURI(getSelectedVideoUri(requestCode, data))
val videoBytes = FileInputStream(File(videoPath)).use { input ->
    input.readBytes()
}

这部分是我在发现错误之前在我的代码中的内容。

我试过的许多方法中有两个如下:

@Throws(IOException::class)
private fun InputStream.readAllBytes(): ByteArray {
    val bufLen = 4 * 0x400 // 4KB
    val buf = ByteArray(bufLen)
    var readLen: Int = 0

    ByteArrayOutputStream().use { o ->
        this.use { i ->
            while (i.read(buf, 0, bufLen).also { readLen = it } != -1)
                o.write(buf, 0, readLen)
        }

        return o.toByteArray()
    }
}

private fun getByteArray(videoPath: String): ByteArray {
    val inputStream = FileInputStream(File(videoPath))
    val buffer = ByteArrayOutputStream()

    var nRead: Int
    val byteData = ByteArray(16384)

    while (inputStream.read(byteData, 0, byteData.size).also { nRead = it } != -1) {
        buffer.write(byteData, 0, nRead)
    }
    return buffer.toByteArray()
}

我获取视频的方式是通过以下两种意图之一:

private fun selectMultipleVideos() {
    globalContext.checkPersmissions(Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA)){ response ->
        if(response){
            showCameraChoiceDialog(options, DialogInterface.OnClickListener{ dialog, which ->
                when(which){
                    0 -> {
                        val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
                        startActivityForResult(takeVideoIntent, ACTION_TAKE_VIDEO)
                    }
                    1 -> {
                        val intent = Intent(Intent.ACTION_PICK)
                        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
                        intent.setType("video/mp4")
                        startActivityForResult(intent, SELECT_VIDEOS)
                    }
                }
            })
        }
    }
}

我的 onActivityResult:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK) {
        if (requestCode == ACTION_TAKE_VIDEO) {
            val videoUri = data!!.data
            val filePath = getPath(globalContext, videoUri!!)
            amountOfVideosUploaded++
            val videoBytes = FileInputStream(File(filePath)).use { input -> input.readBytes() }
            videoByteArray.add(LocalFile(filePath, videoBytes))
            if (contentReminder.num_required_videos != amountOfVideosUploaded) {
                if (localVideoArray.size != contentReminder.num_required_videos) {
                    localVideoArray.add("")
                }
                localVideoArray[amountOfVideosUploaded] = filePath
            } else {
                localVideoArray[0] = filePath
            }

            setupList()
            videosDelivered.text = DataStorage.generalData.translations?.app__contentinbox__submit_video_label + " (" + amountOfVideosUploaded + "/" + contentReminder.num_required_videos + ")"
        } else {
            selectedVideos = getSelectedVideos(requestCode, data!!)
            selectedVideo = getSelectedVideo(requestCode, data)
            amountOfVideosUploaded++
            val videoPath = getVideoPathFromURI(getSelectedVideoUri(requestCode, data))
            val videoBytes = FileInputStream(File(videoPath)).use { input -> input.readBytes() }
            videoByteArray.add(LocalFile(selectedVideo, videoBytes))
            if (contentReminder.num_required_videos != amountOfVideosUploaded) {
                if (localVideoArray.size != contentReminder.num_required_videos) {
                    localVideoArray.add("")
                }
                localVideoArray[amountOfVideosUploaded] = selectedVideos[i]
            } else {
                localVideoArray[0] = selectedVideos[i]
            }

            setupList()
            videosDelivered.text = DataStorage.generalData.translations?.app__contentinbox__submit_video_label + " (" + amountOfVideosUploaded + "/" + contentReminder.num_required_videos + ")"
        }
    }
}

我的一些辅助方法:

private fun getSelectedVideoUri(requestCode: Int, data: Intent): Uri {
    return data.data!!
}

private fun getSelectedVideo(requestCode: Int, data:Intent): String {
    var result : String = ""
    val videoURI = data.data!!
    val filePath = getPath(globalContext, videoURI)
    result = filePath
    return result
}

private fun getSelectedVideos(requestCode: Int, data:Intent): MutableList<String> {
    val result : MutableList<String> = mutableListOf()
    val videoURI = data.data!!
    val filePath = getPath(globalContext, videoURI)
    result.add(filePath)
    return result
}

如果没有出现错误,我会使用以下方法将其上传到服务器:

fun uploadVideoFiles(module: String, file: LocalFile, completionHandler: (path: String) -> Unit){
    val params = HashMap<String, String>()
    params["module"] = module
    params["action"] = "uploadFiles"

    val request = object : MultipartRequest(Method.POST, Router.getUploadUrl(), Response.Listener { response ->
        try {
            completionHandler(Gson().fromJson(String(response.data), FileResponse::class.java).data.file_path)
        } catch (ignore: Exception){
            showSnackbar("Er is iets fout gegaan", ERROR)
        }})
    {
        override fun getParams(): MutableMap<String, String> {
            return params
        }

        override fun getByteData(): MutableMap<String, DataPart> {
            val attachment = HashMap<String, DataPart>()
            val extension = ".mp4"
            attachment["files"] = DataPart(generateCustomString() + extension, file.byteArray!!, MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.replace(".", ""))!!)
            return attachment
        }
    }

    request.retryPolicy = DefaultRetryPolicy(600000, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
    volleyHelper.addToRequestQueue(request)
}

现在我得到的错误(对于显示的每个方法+我自己的/以及我尝试过的所有其他方法):

2021-08-13 11:57:19.726 31911-31911/nl.mycontent E/AndroidRuntime: FATAL EXCEPTION: main
Process: nl.mycontent, PID: 31911
java.lang.OutOfMemoryError: Failed to allocate a 268435472 byte allocation with 25165824 free bytes and 124MB until OOM, target footprint 163389616, growth limit 268435456
    at java.util.Arrays.copyOf(Arrays.java:3161)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118)
    at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:153)
    at nl.mycontent.controller.video.VideoDetail.readAllBytes(VideoDetail.kt:199)
    at nl.mycontent.controller.video.VideoDetail.onActivityResult(VideoDetail.kt:169)
    at androidx.fragment.app.FragmentActivity.onActivityResult(FragmentActivity.java:170)
    at android.app.Activity.dispatchActivityResult(Activity.java:8300)
    at android.app.ActivityThread.deliverResults(ActivityThread.java:5353)
    at android.app.ActivityThread.handleSendResult(ActivityThread.java:5401)
    at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:51)
    at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2267)
    at android.os.Handler.dispatchMessage(Handler.java:107)
    at android.os.Looper.loop(Looper.java:237)
    at android.app.ActivityThread.main(ActivityThread.java:8167)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)

现在我希望有人能告诉我所有 Android 版本(因为现在每个版本对外部存储和东西都有不同的访问要求)如何在没有 OOM 的情况下读取文件例外,但最重要的是为什么解决方案有效以及我在哪里出错了。

如果您只想上传视频文件,为什么不使用自定义 RequestBody,这意味着您不会将整个文件读入内存?像这样:

class InputStreamRequestBody(
    private val contentResolver: ContentResolver,
    private val uri: Uri) : RequestBody()
{
    override fun contentType(): MediaType? {
        val contentType = contentResolver.getType(uri)
        return contentType?.toMediaTypeOrNull()
    }

    override fun writeTo(sink : BufferedSink)
    {
        val input = contentResolver.openInputStream(uri)
        input?.use { sink.writeAll(it.source()) }
            ?: throw IOException("Could not open URI")
    }
}

然后在onActivityResult():

val reqBody = InputStreamRequestBody(requireContext().contentResolver, videoFileUri)
viewModel.uploadVideo(reqBody)

然后,根据设计模式,上传您的视频 ViewModel:

fun uploadVideo(video: RequestBody) {
    viewModelScope.launch(Dispatchers.IO + someExceptionHandler) {
        val videoRequestBody = MultipartBody.Part.createFormData("video", "some_video", video)
        val response = someRetrofitService.uploadVideo(videoRequestBody)
    }
}

Retrofit 服务方法看起来像这样:

@Multipart
@POST("api/v1/files/videos") suspend fun uploadVideo(
    @Part video: MultipartBody.Part? = null)