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
相关产品推荐
相关产品推荐

