[英]How to write byte array without OOM?
I have an android app which handles some large byte array but I am getting some OOM crash in my Firebase Crashlytics reports for devices with low memory while handling byte array whose size may go from 10 mb to 50mb. 下面是我用过的方法。 那么任何人都可以帮助我改进它以避免OOM。
byte[] decrypt(File files) {
try {
FileInputStream fis = new FileInputStream(files);
SecretKeySpec sks = new SecretKeySpec(getResources().getString(R.string.encryptPassword).getBytes(),
"AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, sks);
CipherInputStream cis = new CipherInputStream(fis, cipher);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int b;
byte[] d = new byte[1024];
while ((b = cis.read(d)) != -1) {
buffer.write(d, 0, b); //this is one of the line which is being referred for the OOM in firebase
}
byte[] decryptedData = buffer.toByteArray();//this is the line which is being referred for the OOM in firebase
buffer.flush();
fis.close();
cis.close();
return decryptedData;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
编辑
实际上,我正在使用上述方法来解密在下载过程中加密的下载音频文件。 上述方法将加密文件的内容返回给exoplayer播放其内容,我通过以下方式调用上述方法
ByteArrayDataSource src= new ByteArrayDataSource(decrypt(some_file));
Uri uri = new UriByteDataHelper().getUri(decrypt(some_file));
DataSpec dataSpec = new DataSpec(uri);
src.open(dataSpec);
DataSource.Factory factory = new DataSource.Factory()
{
@Override public DataSource createDataSource()
{
return src;
}
};
audioSource = new ProgressiveMediaSource.Factory(factory).createMediaSource(uri);
我正在写第二个答案,不要编辑我的第一个答案,因为这是解决问题的完全不同的方法。
当您发布部分代码时,我可以看到您有一个字节数组,其中包含由 exoplayer 播放的完整和解密的内容:
output:
byte[] decrypt(File files)
as input for
ByteArrayDataSource src= new ByteArrayDataSource(decrypt(some_file));
因此,为了避免在播放大文件(大约 50mb)时使用 memory 消耗更多时间,我的方法是下载完整的加密文件并将其保存在字节数组中。
在具有良好 memory 设备的设备上,您可以在一次运行中将加密的字节数组解密为另一个字节数组,并从这个解密的字节数组播放音乐(我的示例程序中的步骤 6 + 8)。
使用低 memory 设备解密字节数组(在我的程序中为 16 字节长的块)并将解密的块保存在加密字节数组的同一位置。 当所有块都得到处理后,(以前的)加密数据现在被解密,并且您使用了只有一个字节数组长度的 memory。 现在您可以播放这个字节数组中的音乐(步骤 7 + 8)。
只是为了解释,步骤 1-3 在服务器端,在步骤 3+4 中进行传输。
此示例使用AES CTR 模式,因为它为输入和 output 数据提供相同的长度。
最后,我比较了字节 arrays 以证明步骤 6(直接解密)和步骤 7(块解密)的解密成功:
output:
Decrypting 50mb data in chunks to avoid out of memory error
https://stackoverflow.com/questions/62412705/how-to-write-byte-array-without-oom
plaindata length: 52428810
cipherdata length: 52428810
decrypteddata length: 52428810
cipherdata parts in 16 byte long parts: 3276800 = rounds for decryption
cipherdata moduluo 16 byte long parts: 10 + 1 round for rest/modulus
cipherdata length: 52428810 (after decryption)
plaindata equals decrypteddata: true
plaindata equals cipherdata: true
代码:
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
public class EncryptionCtrSo4 {
public static void main(String[] args) throws GeneralSecurityException {
System.out.println("Decrypting 50mb data in chunks to avoid out of memory error");
System.out.println("https://stackoverflow.com/questions/62412705/how-to-write-byte-array-without-oom");
/*
* author michael fehr, http://javacrypto.bplaced.net
* no licence applies, no warranty
*/
// === server side ===
// 1 create a 50 mb byte array (unencrypted)
byte[] plaindata = new byte[(50 * 1024 * 1024 + 10)];
// fill array with (random) data
Random random = new Random();
random.nextBytes(plaindata);
// 2 encrypt the data with aes ctr mode, create random keys
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[32]; // 32 byte = 256 bit aes key
secureRandom.nextBytes(key);
byte[] iv = new byte[16]; // 16 byte = 128 bit
secureRandom.nextBytes(iv);
SecretKeySpec keySpecEnc = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpecEnc = new IvParameterSpec(iv);
Cipher cipherEnc = Cipher.getInstance("AES/CTR/NoPadding");
cipherEnc.init(Cipher.ENCRYPT_MODE, keySpecEnc, ivParameterSpecEnc);
byte[] cipherdata = cipherEnc.doFinal(plaindata);
System.out.println("plaindata length: " + plaindata.length);
System.out.println("cipherdata length: " + cipherdata.length);
// 3 transfer the cipherdata to app
// ...
// === app side ===
// 4 receive encrypted data from server
// ...
// 5 decryption setup
SecretKeySpec keySpecDec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpecDec = new IvParameterSpec(iv);
Cipher cipherDec = Cipher.getInstance("AES/CTR/NoPadding");
cipherDec.init(Cipher.DECRYPT_MODE, keySpecDec, ivParameterSpecDec);
// 6 decryption in one run on high memory devices
byte[] decrypteddata = cipherDec.doFinal(cipherdata);
System.out.println("decrypteddata length: " + decrypteddata.length);
// 7 decryption in chunks using the cipherdata byte array
int cipherdataLength = cipherdata.length;
int chunksize = 16; // should be a multiple of 16, minimum 16
byte[] decryptedPart = new byte[chunksize];
int parts16byteDiv = cipherdataLength / chunksize;
int parts16byteMod = cipherdataLength % chunksize;
System.out.println("cipherdata parts in " + chunksize + " byte long parts: " + parts16byteDiv + " = rounds for decryption");
System.out.println("cipherdata moduluo " + chunksize + " byte long parts: " + parts16byteMod + " + 1 round for rest/modulus");
for (int i = 0; i < parts16byteDiv; i++) {
cipherDec.update(cipherdata, (i * chunksize), chunksize, decryptedPart);
System.arraycopy(decryptedPart, 0, cipherdata, (i * chunksize), decryptedPart.length);
}
if (parts16byteMod > 0) {
decryptedPart = new byte[parts16byteMod];
cipherDec.update(cipherdata, (parts16byteDiv * chunksize), parts16byteMod, decryptedPart);
System.arraycopy(decryptedPart, 0, cipherdata, (parts16byteDiv * chunksize), decryptedPart.length);
}
System.out.println("cipherdata length: " + cipherdata.length + " (after decryption)");
// the cipherdata byte array is now decrypted !
// 8 use cipherdata (encrypted) or decrypteddata as input for exoplayer
// compare ciphertext with decrypteddata in step 6
System.out.println("plaindata equals decrypteddata: " + Arrays.equals(plaindata, decrypteddata));
// check that (decrypted) cipherdata equals plaindata of step 7
System.out.println("plaindata equals cipherdata: " + Arrays.equals(plaindata, cipherdata));
}
}
首先,我会确保正在运行它的设备有足够的堆 memory 来运行它,这可能只是软件已经分配了很多空间并且堆上可能没有更多空间了提供软件。 此操作不需要太多 memory,而且我没有看到任何明显的迹象表明试图分配和意外大量的 memory。
不过,如果您想进行快速测试,我建议您实际上只是降低字节数组的大小,您使用 1024 的任何特殊原因是什么? 如果可能的话,也许尝试:
byte[] d = new byte[8];
另外,如果那是我,我会暂时将读取的数据存储在一个数组上,并且只有在 cypher 的读取完成后,我才会调用
buffer.write()
根据我的经验,不建议同时尝试读写数十个,这可能会导致一些问题,至少你应该确保你拥有整个 cypher 并且它是有效的(如果你有一些验证要求),然后才发送。
同样,这不应该是核心问题,设备似乎缺少足够的可用 memory 来分配,也许为其他进程预留了太多的 memory?
您应该考虑将解密的数据写入临时文件,然后重新加载数据以供使用。
Out of memory 错误的主要原因是 ByteArrayOutputStream AND byte[] decryptedData = buffer.toByteArray(),因为它们都保存了完整的(解密的)数据,并且使您的解密方法的 memory 消耗加倍。
您可以通过在第一步将数据解密到临时文件并稍后从临时文件加载数据来避免这种情况。 我修改了解密方法来处理解密后的 output stream ,后来有一种方法可以重新加载解密的数据(没有适当的异常处理,为了我的测试,我设置了一个 ZA81259CEF8E959C624DF1Dencryptword-E959C624DF1Dencryptword-Evariable...329....
你只剩下一个部分了——你需要为临时文件找到一个好地方,而我不是 Android 专家。
只有两个注意事项:您使用的是不安全的 AES ECB 模式,并且密码的字符串到字节 [] 转换应更改为
.getBytes(StandardCharsets.UTF_8)
在加密和解密端,避免不同平台不同编码导致的错误。
public static void decryptNew(File files, File tempfiles) {
try (FileInputStream fis = new FileInputStream(files);
BufferedInputStream in = new BufferedInputStream(fis);
FileOutputStream out = new FileOutputStream(tempfiles);
BufferedOutputStream bos = new BufferedOutputStream(out)) {
byte[] ibuf = new byte[1024];
int len;
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec sks = new SecretKeySpec(encryptPassword.getBytes(),"AES"); // static password
// SecretKeySpec sks = new SecretKeySpec(getResources().getString(R.string.encryptPassword).getBytes(),"AES");
cipher.init(Cipher.DECRYPT_MODE, sks);
while ((len = in.read(ibuf)) != -1) {
byte[] obuf = cipher.update(ibuf, 0, len);
if (obuf != null)
bos.write(obuf);
}
byte[] obuf = cipher.doFinal();
if (obuf != null)
bos.write(obuf);
} catch (BadPaddingException | IllegalBlockSizeException | InvalidKeyException | IOException | NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
}
}
public static byte[] loadFile(File filename) throws IOException {
byte[] filecontent = new byte[0];
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(filename);
// int byteLength = fff.length();
// In android the result of file.length() is long
long byteLength = filename.length(); // byte count of the file-content
filecontent = new byte[(int) byteLength];
fileInputStream.read(filecontent, 0, (int) byteLength);
} catch (IOException e) {
e.printStackTrace();
fileInputStream.close();
return filecontent;
}
fileInputStream.close();
return filecontent;
}
将临时文件内容加载到字节数组后,您可以使用单行删除文件(同样没有异常处理):
Files.deleteIfExists(tempFile.toPath());
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.