将3GB大文件转为字节数组遇OOM问题,求最优解决方案
解决大文件处理的OutOfMemoryError问题
你的核心问题在于试图将3GB的完整文件加载到内存中(不管是MappedByteBuffer还是byte[]数组),这显然超出了Android应用的内存限制,必然会触发OOM。另外MappedByteBuffer.array()抛出ReadOnlyBufferException是因为只读映射的ByteBuffer不允许获取底层可修改的数组,这也是API的预期行为。
最优方案完全不需要把整个文件转成字节数组,而是采用流式分块处理:分块读取文件、分块加密、分块上传,每一步只处理一小段数据,全程不占用大量内存。
步骤1:移除全文件内存加载的逻辑
首先抛弃原来用MappedByteBuffer加载整个文件的代码,改用FileInputStream分块读取文件内容,每读取一块就加密一块,然后直接传递给上传流,不需要把所有加密后的数据存成byte[]。
步骤2:修改ProgressRequestBody,支持流式处理
重新实现ProgressRequestBody,让它直接读取原文件、分块加密并上传,全程流式操作:
import android.os.Looper; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; public class ProgressRequestBody extends RequestBody { private static final int DEFAULT_BUFFER_SIZE = 4096; // 4KB缓冲区,和磁盘IO块大小匹配更高效 private final File mFile; private final String mFileType; private final int mItemIndex; private final UploadCallbacks mListener; // 假设你有一个加密工具类,负责分块加密 private final EncryptionUtil mEncryptionUtil; public ProgressRequestBody(String fileType, File file, int itemIndex, UploadCallbacks listener, EncryptionUtil encryptionUtil) { this.mFile = file; this.mFileType = fileType; this.mItemIndex = itemIndex; this.mListener = listener; this.mEncryptionUtil = encryptionUtil; } @Override public MediaType contentType() { try { return MediaType.parse(mFileType); } catch (Exception ex) { return MediaType.parse("application/octet-stream"); } } @Override public long contentLength() throws IOException { // 如果加密会改变文件长度,这里需要提前计算加密后的总长度;对称加密(如AES)通常会增加少量长度 return mFile.length(); } @Override public void writeTo(BufferedSink sink) throws IOException { long fileLength = mFile.length(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long uploaded = 0; FileInputStream fis = new FileInputStream(mFile); android.os.Handler handler = new android.os.Handler(Looper.getMainLooper()); try { int read; while ((read = fis.read(buffer)) != -1) { // 分块加密当前读取的内容 byte[] encryptedBlock = mEncryptionUtil.encrypt(buffer, 0, read); // 写入加密后的块到上传流 sink.write(encryptedBlock); // 更新已上传长度(注意:如果加密后长度变化,这里要加encryptedBlock.length而非read) uploaded += encryptedBlock.length; // 在UI线程更新进度 handler.post(new ProgressUpdater(uploaded, fileLength)); } } finally { fis.close(); } } public interface UploadCallbacks { void onProgressUpdate(int itemIndex, int percentage); } private class ProgressUpdater implements Runnable { private final long mUploaded; private final long mTotal; public ProgressUpdater(long uploaded, long total) { mUploaded = uploaded; mTotal = total; } @Override public void run() { if (mListener != null) { int percentage = (int) (100 * mUploaded / mTotal); // 确保百分比不超过100 mListener.onProgressUpdate(mItemIndex, Math.min(percentage, 100)); } } } }
步骤3:修改上传请求代码
现在不需要提前生成整个文件的byte[],直接传入文件和加密工具即可:
File sourceLocation = new File("你的文件路径"); // 初始化你的加密工具实例 EncryptionUtil encryptionUtil = new EncryptionUtil(); ProgressRequestBody requestFile = new ProgressRequestBody(fileType, sourceLocation, itemIndex, this, encryptionUtil); MultipartBody.Part body = MultipartBody.Part.createFormData("file", sourceLocation.getName(), requestFile); Call<String> mediaCall = getRetrofit().addMedia("SOME STRING DATA", body);
关键注意事项
- 加密块对齐:如果你的加密算法需要固定块大小(比如AES的128位块),要确保分块读取时对齐加密块大小,或者处理最后一块的填充问题,避免加密失败。
- 进度准确性:如果加密后文件长度和原文件不同,
contentLength()需要返回加密后的总长度,否则进度计算会有偏差。 - 缓冲区大小:选择4KB~8KB的缓冲区大小是最优的,和磁盘IO的块大小匹配,能减少IO次数同时避免占用过多内存。
这种流式处理方式的内存占用仅取决于缓冲区大小(几KB),完全不会因为文件大小而触发OOM,是处理大文件的标准方案。
内容的提问来源于stack exchange,提问作者Ashish Pandya
相关产品推荐
相关产品推荐

