You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台AES大文件加密出现OutOfMemoryError问题求助

Android AES加密大文件触发OutOfMemoryError问题解决

问题描述

在Android平台使用AES加密大文件时,触发了OutOfMemoryError错误。

所用代码

import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import timber.log.Timber
import java.io.IOException
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

suspend fun encryptFile(
    context: Context,
    file: DocumentFile,
    secretByte: ByteArray,
    iv: ByteArray
) {
    try {
        val bufferSize = 1024 * 1024
        val secretKeySpec = SecretKeySpec(secretByte, "AES")

        val encryptedFileUri = createEncryptedFileUri(file)
            ?: throw IOException("Failed to create URI for encrypted file")
        
        context.contentResolver.openOutputStream(encryptedFileUri)?.use { outputStream ->


            context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
                val buffer = ByteArray(bufferSize)
                var bytesRead: Int

                val cipher = Cipher.getInstance("AES/GCM/NoPadding")
                val gcmSpec = GCMParameterSpec(128, iv)
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec)

                CipherOutputStream(outputStream, cipher).use { cipherOutputStream ->
                    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        cipherOutputStream.write(buffer, 0, bytesRead)
                    }
                    cipherOutputStream.flush()
                }

            } ?: throw IOException("Failed to open input stream for file: ${file.uri}")
        } ?: throw IOException("Failed to open output stream for file: $encryptedFileUri")

        if (!file.delete()) {
            Timber.e("Failed to delete original file: ${file.uri}")
        }
    } catch (e: Exception) {
        e.printStackTrace()
        Timber.e("Error during file encryption: ${e.message}")
    }
}

private fun createEncryptedFileUri(file: DocumentFile): Uri? {
    val parentDirectory = file.parentFile ?: throw IllegalArgumentException("No parent directory")
    return parentDirectory.createFile("*/*", file.name + ".aesEncr")?.uri
}

错误日志

java.lang.OutOfMemoryError: Failed to allocate a 266338320 byte allocation with 25165824 free bytes and 121MB until OOM, target footprint 166187728, growth limit 268435456
    at com.android.org.conscrypt.OpenSSLAeadCipher.expand(OpenSSLAeadCipher.java:127)
    at com.android.org.conscrypt.OpenSSLAeadCipher.updateInternal(OpenSSLAeadCipher.java:300)
    at com.android.org.conscrypt.OpenSSLCipher.engineUpdate(OpenSSLCipher.java:332)
    at javax.crypto.Cipher.update(Cipher.java:1741)
    at javax.crypto.CipherOutputStream.write(CipherOutputStream.java:158)                                                                                                   
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
    at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: java.lang.OutOfMemoryError: Failed to allocate a 132120608 byte allocation with 25165824 free bytes and 121MB until OOM, target footprint 166188144, growth limit 268435456
    at com.android.org.conscrypt.OpenSSLCipher.engineDoFinal(OpenSSLCipher.java:359)
    at javax.crypto.Cipher.doFinal(Cipher.java:1957)

原因分析

错误根源在于Android默认的Conscrypt加密库中,CipherOutputStream在AES-GCM模式下会大量缓存未加密的数据,直到调用doFinal才会一次性处理输出,导致大文件加密时内存占用急剧飙升,触发OOM。1MB的缓冲区进一步加剧了内存压力。

解决方案

修改代码实现,避免CipherOutputStream缓存

手动调用Cipher的update方法分段处理数据,直接写入输出流,同时缩小缓冲区大小,减少内存占用:

suspend fun encryptFile(
    context: Context,
    file: DocumentFile,
    secretByte: ByteArray,
    iv: ByteArray
) {
    try {
        val bufferSize = 64 * 1024 // 缩小缓冲区到64KB(AES块大小的倍数)
        val secretKeySpec = SecretKeySpec(secretByte, "AES")

        val encryptedFileUri = createEncryptedFileUri(file)
            ?: throw IOException("Failed to create URI for encrypted file")
        
        context.contentResolver.openOutputStream(encryptedFileUri)?.use { outputStream ->
            context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
                val buffer = ByteArray(bufferSize)
                var bytesRead: Int

                val cipher = Cipher.getInstance("AES/GCM/NoPadding")
                val gcmSpec = GCMParameterSpec(128, iv)
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec)

                // 手动分段加密并写入,避免CipherOutputStream的缓存问题
                while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                    val encryptedBytes = cipher.update(buffer, 0, bytesRead)
                    encryptedBytes?.let {
                        outputStream.write(it)
                        outputStream.flush() // 立即写入磁盘,释放内存
                    }
                }
                // 处理最后一段数据和GCM认证标签
                val finalBytes = cipher.doFinal()
                finalBytes?.let {
                    outputStream.write(it)
                    outputStream.flush()
                }

            } ?: throw IOException("Failed to open input stream for file: ${file.uri}")
        } ?: throw IOException("Failed to open output stream for file: $encryptedFileUri")

        if (!file.delete()) {
            Timber.e("Failed to delete original file: ${file.uri}")
        }
    } catch (e: Exception) {
        e.printStackTrace()
        Timber.e("Error during file encryption: ${e.message}")
    }
}

private fun createEncryptedFileUri(file: DocumentFile): Uri? {
    val parentDirectory = file.parentFile ?: throw IllegalArgumentException("No parent directory")
    return parentDirectory.createFile("*/*", file.name + ".aesEncr")?.uri
}

额外优化建议

  • 确保加密操作在IO线程执行:在suspend函数内用withContext(Dispatchers.IO)包裹核心逻辑,避免阻塞主线程
  • 加密期间释放不必要的内存对象,减少内存占用
  • 针对Android 10+,确保Scoped Storage权限配置正确,避免文件操作异常

内容的提问来源于stack exchange,提问作者Muhammad Ammar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:44:51