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 缓存的核心流程:
避坑指南
我曾经在缓存上栽过跟头,分享几个经验:
- 不要缓存 CameraManager 本身——它内部有状态,不同进程拿到的实例可能不同。只缓存 CameraCharacteristics。
- 注意 cameraId 的变化——有些设备在系统 OTA 后 cameraId 会变。虽然概率极低,但缓存 key 最好用
cameraId + build.fingerprint拼接。 - 不要缓存所有元数据——CameraCharacteristics 里有些字段是动态的,比如
CONTROL_AE_STATE。只缓存静态元数据,动态数据每次实时读取。 - 多摄像头场景——现在很多手机有 3-5 个摄像头。建议在应用启动时一次性预加载所有摄像头的元数据,不要等用户切换摄像头时才去读。
总结一下:CameraCharacteristics 缓存是相机启动优化里性价比最高的手段之一。代码改动量小,收益却非常稳定。你想想看,一个 ConcurrentHashMap + 几行逻辑,就能省掉每次 50-80ms 的读取时间,这笔账怎么算都划算。