28. Camera性能优化:帧率监控、内存管理、功耗优化、多线程处理

做Camera开发,说白了就是在跟资源较劲。CPU、内存、带宽、功耗,哪一块没伺候好,整个采集链路就会出问题。我这些年踩过的坑,十有八九都跟性能有关。今天咱们就把这块硬骨头啃下来。

28.1 帧率监控:别让数据骗了你

帧率是Camera最直观的性能指标。但很多人只看logcat里打印的fps,那玩意儿其实不太靠谱。为什么?因为系统打印的往往是平均帧率,中间卡了几帧你根本看不出来。

我个人习惯用两种方式监控帧率:

  • 逐帧时间戳分析:记录每一帧的到达时间,计算相邻帧的时间差
  • 滑动窗口统计:取最近30帧或60帧,计算实时帧率

来看一段我常用的帧率监控代码:

public class FpsMonitor {
    private static final int WINDOW_SIZE = 30;
    private final long[] frameTimestamps = new long[WINDOW_SIZE];
    private int index = 0;
    private int count = 0;

    public void onFrameCaptured() {
        long now = System.nanoTime();
        frameTimestamps[index] = now;
        index = (index + 1) % WINDOW_SIZE;
        if (count < WINDOW_SIZE) count++;

        if (count == WINDOW_SIZE) {
            long oldest = frameTimestamps[index];
            long elapsed = now - oldest; // 纳秒
            double fps = (double) WINDOW_SIZE / (elapsed / 1_000_000_000.0);
            Log.d("FpsMonitor", "实时帧率: " + String.format("%.1f", fps));
        }
    }
}
小技巧:把帧率数据用GraphView画成折线图,卡顿点一目了然。我在调试某款低端机时,就是靠这个发现了驱动层的周期性丢帧。

28.2 内存管理:ImageReader是个双刃剑

Camera采集最吃内存的地方,就是ImageReader。你想想看,1080p的YUV420_888图像,一帧就要3MB左右。如果开了3帧的缓冲池,那就是9MB。要是再开个预览和拍照双路流,内存直接起飞。

我遇到过最离谱的一次,是某款手机在拍照时OOM了。查了半天,发现是ImageReader的OnImageAvailableListener里没有及时close Image对象。嗯,这个坑我帮你们踩过了。

内存优化的几个关键点:

  • 控制缓冲池大小:预览场景2-3帧足够,拍照场景可以适当增加
  • 及时释放Image:用完必须close,最好用try-finally保证
  • 复用Surface:预览和拍照尽量用同一个Surface,减少内存拷贝
  • 降低分辨率:不是所有场景都需要4K,720p在很多场景下完全够用
注意:千万不要在OnImageAvailableListener里做耗时操作!这个回调是在Camera的binder线程里执行的,你卡住了它,整个Camera服务都会受影响。我曾经见过有人在这里做Bitmap压缩,结果预览直接掉到15fps。

28.3 功耗优化:别让手机变成暖手宝

Camera是功耗大户,这个大家都知道。但具体怎么优化,很多人就懵了。我总结了几条实战经验:

优化手段 效果 适用场景
降低帧率 显著降低功耗 非实时场景(扫码、文档扫描)
使用低分辨率 减少带宽和计算量 预览、人脸检测
关闭不必要的特性 减少ISP负载 所有场景
使用硬件编码 比软编省电50%以上 视频录制
动态调整帧率 按需分配资源 场景切换频繁时

这里重点说一下动态帧率调整。说白了就是:场景简单时跑低帧率,场景复杂时跑高帧率。比如人脸检测,没人脸时15fps就够了,检测到人脸再切到30fps。我在做门禁项目时就用这个方案,功耗降低了40%。

28.4 多线程处理:别让主线程背锅

Camera的整个采集链路,涉及多个线程。我见过最糟糕的写法,是在主线程里直接操作Camera。结果呢?UI卡成PPT,ANR警告满天飞。

正确的做法是这样的:

  • Camera操作线程:专门用来open、configure、start/stop preview
  • 帧处理线程:处理ImageReader回调,做图像算法
  • UI线程:只负责更新界面,不做任何耗时操作
  • 编码线程:视频录制时,把编码放到独立线程

来看一个典型的多线程架构:

public class CameraManager {
    private HandlerThread cameraThread;
    private HandlerThread frameThread;
    private Handler cameraHandler;
    private Handler frameHandler;

    public void init() {
        cameraThread = new HandlerThread("CameraThread");
        cameraThread.start();
        cameraHandler = new Handler(cameraThread.getLooper());

        frameThread = new HandlerThread("FrameThread");
        frameThread.start();
        frameHandler = new Handler(frameThread.getLooper());
    }

    public void startPreview() {
        cameraHandler.post(() -> {
            // 在这里操作Camera
            camera.open();
            camera.configure();
            camera.startPreview();
        });
    }

    private ImageReader.OnImageAvailableListener frameCallback = reader -> {
        Image image = reader.acquireLatestImage();
        frameHandler.post(() -> {
            // 在这里处理帧数据
            processFrame(image);
            image.close();
        });
    };
}
核心原则:谁的孩子谁抱走。Camera的回调在Camera线程,帧数据在帧处理线程,UI更新在主线程。各司其职,互不干扰。

28.5 知识体系总览

说了这么多,咱们用一张图把整个性能优化的知识体系串起来:

Camera性能优化 帧率监控 逐帧时间戳分析 滑动窗口统计 实时折线图展示 内存管理 控制缓冲池大小 及时释放Image 复用Surface 功耗优化 降低帧率/分辨率 关闭不必要特性 动态帧率调整 多线程处理 Camera操作线程 帧处理线程 UI线程分离

这张图把四个优化方向的关系讲清楚了。帧率监控是眼睛,帮你发现问题;内存管理和功耗优化是两条腿,保证系统跑得稳、跑得久;多线程处理是骨架,把整个架构撑起来。缺了哪一个,你的Camera应用都走不远。

好了,这一章的内容就到这里。性能优化是个持续的过程,没有银弹。我的建议是:先跑起来,再优化。别一开始就想着完美,那样反而容易陷入过度设计的泥潭。

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