将大型视频文件读取到字节数组时出现 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)
所以在搜索 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)