相机内存优化:内存泄漏检测、Bitmap复用、YUV Buffer池、Camera内存压力测试
内存优化这个话题,说实话,是相机性能优化里最容易被忽视、但一旦出事就最头疼的一块。我见过太多项目,帧率调好了,功耗也压下去了,结果跑个半小时直接OOM崩溃——那种感觉,就像考试最后一道大题写完了,结果发现名字忘写了。
今天咱们就把这块掰开揉碎了讲。核心就四个方向:内存泄漏怎么抓、Bitmap怎么复用、YUV Buffer池怎么设计、压力测试怎么做。每个点我都会结合我踩过的坑来讲。
核心观点:相机内存优化的本质,是让每一块内存的“生”和“死”都在你的掌控之中。失控的内存,就是崩溃的种子。
一、内存泄漏检测:别等用户骂你才想起来
内存泄漏这东西,说白了就是“借了不还”。你申请了一块内存,用完了没释放,系统就永远拿不回来了。一次两次没事,但相机是高频调用的模块,泄漏累积起来,迟早崩给你看。
我个人习惯,在开发阶段就做两件事:
- LeakCanary 集成到测试包——别只在debug包跑,有些泄漏只在特定场景触发
- 手动dump heap + MAT分析——LeakCanary能告诉你“泄漏了”,但MAT能告诉你“为什么泄漏”
我记得有一次,项目上线前发现相机切后台再切回来,内存涨了20MB不回落。用MAT一查,发现是CameraMetadata对象被一个静态变量持有了。嗯,这种低级错误,其实挺常见的。
我的经验:相机模块最容易泄漏的地方,往往是Callback注册了没反注册。比如CameraCaptureSession.StateCallback,你在onResume里注册,onPause里忘了清理,泄漏就来了。
常见泄漏场景速查表
| 泄漏类型 | 典型表现 | 检测手段 |
|---|---|---|
| Surface持有Activity引用 | 旋转屏幕后内存不降 | LeakCanary + ActivityRefWatcher |
| CameraSession未关闭 | 反复打开相机内存递增 | MAT查看CameraDevice引用链 |
| Handler持有外部类 | 匿名内部类泄漏 | 静态内部类 + WeakReference |
| YUV Buffer未回收 | ByteBuffer.allocateDirect暴涨 | 手动跟踪Buffer生命周期 |
二、Bitmap复用:别让GC成为你的性能瓶颈
相机预览和拍照,Bitmap是内存大户。你想想看,一张1200万像素的照片,加载成Bitmap就是几十MB。如果每次拍照都new一个新Bitmap,GC会频繁触发,卡顿就来了。
我建议的做法是:建立Bitmap复用池。
说白了,就是提前申请好一批Bitmap,用的时候从池子里拿,用完还回去。Android的BitmapFactory.Options.inBitmap就是干这个的。
// 复用池示例
BitmapPool pool = new BitmapPool(maxSize: 5, width: 1920, height: 1080, config: ARGB_8888);
// 拍照时从池中取
Bitmap reused = pool.acquire();
if (reused != null) {
options.inBitmap = reused;
}
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
// 使用完后归还
pool.release(bitmap);
注意:inBitmap复用时,新Bitmap的尺寸不能大于原Bitmap的尺寸。而且Android 4.4之前只支持相同尺寸的复用,4.4之后放宽了限制。如果你还在适配老设备,这块要小心。
我曾经在一个项目里,因为没做Bitmap复用,连续拍10张照片后GC时间飙到200ms,用户直接投诉“相机卡死了”。后来加了复用池,GC时间降到20ms以内。说白了,复用池就是拿空间换时间,值。
三、YUV Buffer池:预览流的内存管理艺术
YUV数据是相机最原始的输出格式。每一帧预览,都是一块YUV Buffer。如果每帧都申请新Buffer,内存分配和释放的开销会非常大。
我的做法是:实现一个环形Buffer池。
核心思路:
- 预分配N块固定大小的YUV Buffer(比如4块)
- 每一帧从池中取一块,填充数据
- 处理完后归还到池中
- 如果池满,阻塞等待或丢弃旧帧
// YUV Buffer池伪代码
class YuvBufferPool {
private final BlockingQueue<ByteBuffer> pool;
private final int bufferSize; // 例如 1920*1080*3/2
public YuvBufferPool(int poolSize, int width, int height) {
this.bufferSize = width * height * 3 / 2;
this.pool = new LinkedBlockingQueue<>(poolSize);
for (int i = 0; i < poolSize; i++) {
pool.offer(ByteBuffer.allocateDirect(bufferSize));
}
}
public ByteBuffer acquire() throws InterruptedException {
return pool.take(); // 阻塞直到有可用Buffer
}
public void release(ByteBuffer buffer) {
buffer.clear();
pool.offer(buffer);
}
}
避坑指南:我曾经把Buffer池大小设成2,结果在高帧率模式下,消费者线程处理不过来,生产者线程一直阻塞,导致预览卡顿。后来改成4块,问题解决。池的大小要根据你的处理链路耗时来定,一般3~5块比较稳妥。
四、Camera内存压力测试:别让线上事故打你脸
内存优化做没做到位,不是靠“我觉得没问题”来判断的。你得用压力测试来验证。
我常用的压力测试方案:
- 反复打开关闭相机——模拟用户频繁切换场景,监控内存是否持续增长
- 连续拍照100次——检查Bitmap复用是否生效,GC频率是否正常
- 切换前后置摄像头——看Session重建时是否有泄漏
- 低内存场景模拟——用
adb shell am send-trim-memory模拟系统回收,看相机是否崩溃 - 长时间预览——跑30分钟以上,监控Native Heap和Java Heap趋势
我记得有一次,压力测试跑了20分钟,发现Native Heap缓慢增长。用malloc debug一查,是某个HAL层的Buffer没释放。这种问题,你不跑压力测试根本发现不了。
我的建议:把压力测试写进CI/CD流程。每次代码合入前,自动跑一轮内存压力测试,内存增长超过阈值就告警。别等到用户反馈了才去修。
知识体系总览
下面这张图,是我自己总结的相机内存优化全景。你可以把它当成一个检查清单,做优化时对着看,不容易漏项。
好了,以上就是相机内存优化的四个核心方向。说实话,这块没有太多花哨的技巧,就是扎扎实实地做好检测、复用、池化和测试。但正是这些“笨功夫”,决定了你的相机模块能不能稳定跑上几个小时不崩溃。
下次你遇到相机OOM,别慌。按这个框架排查一遍,大概率能找到问题。