7. CameraCharacteristics缓存:避免重复读取元数据、预加载支持的流配置

做相机启动优化,有个坑我踩过好几次——就是反复去读 CameraCharacteristics。你想想看,每次打开相机都要去查一遍传感器尺寸、支持的流配置、AE/AWB 信息……这些数据其实从设备开机到关机都不会变。重复读取,说白了就是在浪费宝贵的启动时间。

为什么需要缓存 CameraCharacteristics?

CameraCharacteristics 是 HAL 层上报的静态元数据。它描述的是相机传感器的“天生属性”,比如:

  • 传感器物理尺寸
  • 像素阵列大小
  • 支持的输出格式和分辨率
  • AE/AWB/AF 能力
  • 闪光灯信息
  • 镜头朝向

这些信息在设备运行期间不会改变。但很多开发者每次打开相机都去调用 cameraManager.getCameraCharacteristics(cameraId),这就多了一次跨进程调用和 HAL 层的查询开销。

核心观点:CameraCharacteristics 是只读的、不变的、可缓存的。一次读取,全局复用。

我遇到过的真实案例

之前优化一款中端机的相机启动,发现每次冷启动都要花 80ms 左右去读 CameraCharacteristics。我一开始以为是 HAL 层慢,后来用 systrace 一抓,发现是上层反复调用了三次 getCameraCharacteristics。嗯,这里要注意——同一个 cameraId,你调一次和调三次,耗时是线性增长的。

我当时就做了个全局缓存,把 CameraCharacteristics 存在 Application 级别的单例里。启动时间直接砍掉了 70ms。效果立竿见影。

缓存策略怎么设计?

我个人习惯用 ConcurrentHashMap 来做缓存容器。key 是 cameraId,value 是 CameraCharacteristics 对象。这样多线程访问也不会出问题。

public class CameraCharacteristicsCache {
    private static final String TAG = "CameraCache";
    private final ConcurrentHashMap<String, CameraCharacteristics> cache = new ConcurrentHashMap<>();
    private final CameraManager cameraManager;

    public CameraCharacteristicsCache(CameraManager manager) {
        this.cameraManager = manager;
    }

    public CameraCharacteristics get(String cameraId) {
        CameraCharacteristics cached = cache.get(cameraId);
        if (cached != null) {
            return cached;
        }
        try {
            CameraCharacteristics chars = cameraManager.getCameraCharacteristics(cameraId);
            cache.put(cameraId, chars);
            return chars;
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to get camera characteristics for " + cameraId, e);
            return null;
        }
    }

    public void preload(String[] cameraIds) {
        for (String id : cameraIds) {
            get(id); // 触发缓存填充
        }
    }

    public void clear() {
        cache.clear();
    }
}

小技巧:可以在 Application.onCreate() 里就调用 preload(),把前后摄像头的元数据都加载好。用户打开相机时,直接命中缓存,零等待。

预加载支持的流配置

流配置(StreamConfigurationMap)是 CameraCharacteristics 里最常用的子项。它包含了所有支持的输出格式、分辨率、帧率等信息。每次创建预览或拍照时,都要查一遍这个 map。

我曾经见过一个项目,每次创建预览 Surface 时都去遍历所有支持的分辨率,找到最合适的那个。遍历一次倒不慢,但问题是它每次创建都遍历,而且遍历过程中还做了很多字符串拼接和日志打印。嗯,这就是典型的“积少成多”型性能问题。

我的做法是:把 StreamConfigurationMap 单独抽出来缓存,并且把常用的分辨率列表也一并缓存好。

public class StreamConfigCache {
    private final CameraCharacteristics characteristics;
    private StreamConfigurationMap configMap;
    private List<Size> previewSizes;
    private List<Size> photoSizes;

    public StreamConfigCache(CameraCharacteristics chars) {
        this.characteristics = chars;
        this.configMap = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        // 预解析常用分辨率
        this.previewSizes = Arrays.asList(configMap.getOutputSizes(ImageFormat.YUV_420_888));
        this.photoSizes = Arrays.asList(configMap.getOutputSizes(ImageFormat.JPEG));
    }

    public Size getOptimalPreviewSize(int targetWidth, int targetHeight) {
        // 这里直接用缓存的 previewSizes 做匹配,不用每次都去 getOutputSizes()
        // 省略具体匹配逻辑...
        return bestSize;
    }
}

缓存的生命周期管理

缓存不是越多越好。CameraCharacteristics 缓存的生命周期应该和 Application 保持一致。因为设备信息不会变,所以缓存可以一直保留。

但有一点要注意:如果设备支持热插拔摄像头(比如 USB Camera),那缓存就需要在插拔事件发生时清空。监听 CameraManager.AvailabilityCallback 即可。

警告:不要用 WeakReference 来缓存 CameraCharacteristics。因为 GC 随时可能回收它,导致你又得重新读取。用强引用,配合 Application 生命周期,最稳妥。

性能收益有多大?

我拿一台骁龙 8 Gen 2 的设备做过测试,数据如下:

场景 优化前 优化后 提升
冷启动(首次) 85ms 85ms(首次不可避免) 0%
冷启动(第二次) 82ms 0ms(命中缓存) 100%
热启动 45ms 0ms 100%
切换摄像头 60ms 0ms 100%

你看,除了第一次读取,后续所有操作都是零耗时。这个优化几乎没有成本,收益却非常可观。

核心逻辑流程图

下面这张图展示了 CameraCharacteristics 缓存的核心流程:

CameraCharacteristics 缓存核心流程 应用启动 / 相机打开 检查缓存中是否有数据? 调用 getCameraCharacteristics() 直接返回缓存数据 写入 ConcurrentHashMap 上层使用缓存数据 启动流程 缓存判断 HAL读取 缓存命中

避坑指南

我曾经在缓存上栽过跟头,分享几个经验:

  • 不要缓存 CameraManager 本身——它内部有状态,不同进程拿到的实例可能不同。只缓存 CameraCharacteristics。
  • 注意 cameraId 的变化——有些设备在系统 OTA 后 cameraId 会变。虽然概率极低,但缓存 key 最好用 cameraId + build.fingerprint 拼接。
  • 不要缓存所有元数据——CameraCharacteristics 里有些字段是动态的,比如 CONTROL_AE_STATE。只缓存静态元数据,动态数据每次实时读取。
  • 多摄像头场景——现在很多手机有 3-5 个摄像头。建议在应用启动时一次性预加载所有摄像头的元数据,不要等用户切换摄像头时才去读。

总结一下:CameraCharacteristics 缓存是相机启动优化里性价比最高的手段之一。代码改动量小,收益却非常稳定。你想想看,一个 ConcurrentHashMap + 几行逻辑,就能省掉每次 50-80ms 的读取时间,这笔账怎么算都划算。

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