相机内存优化:内存泄漏检测、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内存压力测试:别让线上事故打你脸

内存优化做没做到位,不是靠“我觉得没问题”来判断的。你得用压力测试来验证。

我常用的压力测试方案:

  1. 反复打开关闭相机——模拟用户频繁切换场景,监控内存是否持续增长
  2. 连续拍照100次——检查Bitmap复用是否生效,GC频率是否正常
  3. 切换前后置摄像头——看Session重建时是否有泄漏
  4. 低内存场景模拟——用adb shell am send-trim-memory模拟系统回收,看相机是否崩溃
  5. 长时间预览——跑30分钟以上,监控Native Heap和Java Heap趋势

我记得有一次,压力测试跑了20分钟,发现Native Heap缓慢增长。用malloc debug一查,是某个HAL层的Buffer没释放。这种问题,你不跑压力测试根本发现不了。

我的建议:把压力测试写进CI/CD流程。每次代码合入前,自动跑一轮内存压力测试,内存增长超过阈值就告警。别等到用户反馈了才去修。

知识体系总览

下面这张图,是我自己总结的相机内存优化全景。你可以把它当成一个检查清单,做优化时对着看,不容易漏项。

相机内存优化 内存泄漏检测 • LeakCanary 集成 • MAT / Heap Dump 分析 • 常见泄漏点:Callback、Session、Handler • 自动化检测流程 Bitmap复用 • inBitmap 复用池 • 尺寸兼容性(4.4前后差异) • 减少GC触发频率 • 连续拍照场景优化 YUV Buffer池 • 环形Buffer池设计 • 预分配 vs 按需分配 • 池大小调优(3~5块) • 阻塞与丢帧策略 内存压力测试 • 反复开关相机 • 连续拍照100次 • 低内存模拟 • CI/CD 自动化监控 目标:每一块内存的生与死都在掌控之中

好了,以上就是相机内存优化的四个核心方向。说实话,这块没有太多花哨的技巧,就是扎扎实实地做好检测、复用、池化和测试。但正是这些“笨功夫”,决定了你的相机模块能不能稳定跑上几个小时不崩溃。

下次你遇到相机OOM,别慌。按这个框架排查一遍,大概率能找到问题。


公众号:蓝海资料掘金营,微信deep3321