Retrofit上传大视频触发OutOfMemoryError内存溢出问题
问题根源
触发OOM的核心原因是你在构造上传请求体时,通过IOUtils.toByteArray(inputStream)将整个视频文件一次性全部读取到内存中。
Android应用的堆内存存在上限(从你的报错看当前应用堆增长上限是256MB),当上传上百MB的大体积视频时,申请连续大内存块的操作会直接超出堆限制,抛出内存溢出错误。实际上Retrofit/OkHttp原生支持流式上传,完全不需要提前把整个文件加载进内存。
修复方案
替换原有prepareFilePart的实现,构造支持分段读取的流式RequestBody,上传过程中始终只占用极小的缓冲区内存,不会因为文件体积过大触发OOM:
public class ApiHelper { @NonNull public static MultipartBody.Part prepareFilePart(Context context, String partName, Uri fileUri) { ContentResolver contentResolver = context.getContentResolver(); String mimeType = contentResolver.getType(fileUri); if (mimeType == null) { mimeType = "video/*"; } MediaType mediaType = MediaType.parse(mimeType); RequestBody requestFile = new RequestBody() { @Override public MediaType contentType() { return mediaType; } @Override public long contentLength() throws IOException { Cursor cursor = contentResolver.query(fileUri, new String[]{OpenableColumns.SIZE}, null, null, null); if (cursor != null) { try { int sizeIndex = cursor.getColumnIndexOrThrow(OpenableColumns.SIZE); if (cursor.moveToFirst()) { return cursor.getLong(sizeIndex); } } finally { cursor.close(); } } return -1; } @Override public void writeTo(@NonNull BufferedSink sink) throws IOException { byte[] buffer = new byte[8192]; try (InputStream inputStream = contentResolver.openInputStream(fileUri)) { if (inputStream == null) { throw new IOException("Open input stream failed for uri: " + fileUri); } int readLen; while ((readLen = inputStream.read(buffer)) != -1) { sink.write(buffer, 0, readLen); } sink.flush(); } } }; String fileName = resolveFileName(context, fileUri); return MultipartBody.Part.createFormData(partName, fileName, requestFile); } private static String resolveFileName(Context context, Uri uri) { String fileName = null; if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME); fileName = cursor.getString(nameIndex); } } } return fileName != null ? fileName : uri.getLastPathSegment(); } }
额外说明
- 上述实现使用8KB的缓冲区分段读取文件,无论上传多大体积的视频,内存中仅保留缓冲区大小的文件片段,从根源上避免大文件导致的OOM。
- 原有实现直接将Uri对象转为字符串作为上传文件名,服务端收到的文件名会是Uri的路径字符串而非真实文件名,修复后的实现会从ContentResolver查询文件真实展示名。
- 如果需要实现上传进度监听,只需要在
writeTo方法中统计累计写入的字节数,结合contentLength()返回的总文件长度做进度回调即可,不需要调整整体流式读取逻辑。 - 后续处理大文件读写场景时,禁止使用
IOUtils.toByteArray这类将全量内容一次性读入内存的方法。
内容的提问来源于stack exchange,提问作者user1695123
相关产品推荐
相关产品推荐

