16、性能优化:缓冲区大小设置、避免内存抖动、异步提取
性能优化这个话题,说实话,是区分「能用」和「好用」的分水岭。
我自己刚做音视频那会儿,写出来的提取器也能跑,但一放到低端机上就卡顿、掉帧,甚至直接 OOM。后来踩了不少坑,才慢慢摸清楚门道。今天咱们就把缓冲区大小、内存抖动、异步提取这三个核心点彻底聊透。
16.1 缓冲区大小:不是越大越好
很多人一上来就问:「缓冲区设多大合适?」
我的回答是:看场景,看设备,看帧率。
先看一个最基础的设置方式:
// 获取轨道格式
MediaFormat format = extractor.getTrackFormat(trackIndex);
int sampleSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(sampleSize);
嗯,这里要注意:KEY_MAX_INPUT_SIZE 这个值,不同厂商的 ROM 给得不一样。有的给 2MB,有的给 8MB。你直接用它分配,在低端机上很容易触发大块内存分配,造成卡顿。
我个人习惯的做法是:动态计算,按需分配。
// 根据视频分辨率估算缓冲区大小
int width = format.getInteger(MediaFormat.KEY_WIDTH);
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
// 一般一帧压缩数据不会超过 width * height * 1.5
int estimatedSize = (int)(width * height * 1.5f);
// 再给个上限,防止极端情况
int bufferSize = Math.min(estimatedSize, 2 * 1024 * 1024); // 最大2MB
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
为什么是 1.5 倍?我在项目中遇到过一些 H.264 的 I 帧,压缩比低的时候确实会接近这个值。你想想看,如果缓冲区设小了,readSampleData 返回 -1,你还得重新分配,这不是更亏?
16.2 避免内存抖动:复用是关键
内存抖动,说白了就是频繁创建和销毁对象,导致 GC 频繁触发。
我记得有一次在项目里做实时提取,每读一帧就 new 一个 ByteBuffer,结果在 30fps 的视频上,GC 日志刷屏了。帧率直接从 30 掉到 15,画面肉眼可见地卡。
解决方案其实很简单:复用缓冲区。
// 在循环外部只分配一次
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
// 处理数据...
// 注意:处理完后要清空缓冲区,或者用 position/limit 控制
buffer.clear();
extractor.advance();
}
这里有个小坑:buffer.clear() 只是重置了 position 和 limit,并没有真正清空数据。如果你在处理时依赖 buffer 的剩余数据量,记得用 buffer.limit(sampleSize) 来限定有效数据范围。
16.3 异步提取:别让主线程卡死
提取音视频数据,本质上是 I/O 操作。如果在主线程里同步读取,UI 会直接卡住。用户滑动列表时突然卡顿,体验极差。
我建议的做法是:用 HandlerThread 或者协程来做异步提取。
先看一个经典的 HandlerThread 方案:
public class AsyncExtractor {
private HandlerThread extractThread;
private Handler extractHandler;
private MediaExtractor extractor;
public void startAsyncExtract(String filePath) {
extractThread = new HandlerThread("ExtractThread");
extractThread.start();
extractHandler = new Handler(extractThread.getLooper());
extractHandler.post(() -> {
extractor = new MediaExtractor();
extractor.setDataSource(filePath);
// ... 选择轨道、分配缓冲区
while (true) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
// 处理数据,注意线程安全
processData(buffer, sampleSize);
extractor.advance();
}
// 提取完成,回调主线程
new Handler(Looper.getMainLooper()).post(() -> {
onExtractComplete();
});
});
}
public void stopExtract() {
extractHandler.removeCallbacksAndMessages(null);
extractThread.quitSafely();
}
}
为什么用 HandlerThread 而不是直接 new Thread?因为 HandlerThread 自带 Looper,方便你后续发消息、取消任务。说白了,管理起来更顺手。
如果你用 Kotlin,协程方案会更简洁:
// 用 Dispatchers.IO 做异步提取
viewModelScope.launch(Dispatchers.IO) {
val extractor = MediaExtractor()
extractor.setDataSource(filePath)
// ... 提取逻辑
withContext(Dispatchers.Main) {
// 更新 UI
}
}
16.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
16.5 综合实战:一个完整的优化示例
把上面三个点串起来,写一个完整的优化版提取器:
public class OptimizedExtractor {
private MediaExtractor extractor;
private ByteBuffer buffer;
private MediaCodec.BufferInfo info;
private HandlerThread workerThread;
private Handler workerHandler;
public void start(String filePath) {
// 1. 异步线程
workerThread = new HandlerThread("ExtractWorker");
workerThread.start();
workerHandler = new Handler(workerThread.getLooper());
workerHandler.post(() -> {
extractor = new MediaExtractor();
try {
extractor.setDataSource(filePath);
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) return;
extractor.selectTrack(trackIndex);
// 2. 动态计算缓冲区
MediaFormat format = extractor.getTrackFormat(trackIndex);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
int bufferSize = (int)(width * height * 1.5f);
bufferSize = Math.min(bufferSize, 2 * 1024 * 1024);
// 3. 复用对象
buffer = ByteBuffer.allocate(bufferSize);
info = new MediaCodec.BufferInfo();
while (!Thread.currentThread().isInterrupted()) {
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
buffer.limit(sampleSize);
// 处理数据...
processFrame(buffer, info);
buffer.clear();
extractor.advance();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
release();
}
});
}
private void release() {
if (extractor != null) {
extractor.release();
extractor = null;
}
workerThread.quitSafely();
}
}
好了,关于性能优化,核心就是这三板斧:缓冲区大小要合理、内存对象要复用、提取操作要异步。你想想看,只要把这三点做到位,大部分性能问题都能迎刃而解。
公众号:蓝海资料掘金营,微信deep3321