22、Camera 性能优化:Camera启动优化、预览延迟优化、拍照延迟优化、内存占用优化与OOM预防。
做Camera开发这么多年,我踩过最多的坑,就是性能问题。启动慢、预览卡、拍照延迟、内存爆掉……这些几乎是每个Camera工程师的噩梦。今天咱们就好好聊聊,怎么把这些硬骨头啃下来。
说白了,Camera性能优化就是一场与时间、内存的博弈。你想想看,用户按下快门那一瞬间,背后是几十个模块在协同工作。任何一个环节慢了,体验就崩了。
Camera启动优化:从点击到预览的第一帧
启动优化,核心目标就一个:缩短从点击图标到预览画面出现的时间。我见过有些App启动要3秒多,用户早跑了。
启动流程大致分这几步:
- 进程创建与初始化
- CameraService绑定
- Camera HAL加载
- Stream配置与创建
- 第一帧预览数据回调
这里有个关键点——并行化。很多开发者习惯串行做事情,其实没必要。比如UI初始化可以和CameraService绑定同时进行。
核心优化策略:
- 预加载CameraService:在Application启动时就提前绑定CameraService,减少首次调用时的等待。我在项目中遇到过,冷启动时CameraService还没起来,硬等了几百毫秒。
- 延迟非必要初始化:像闪光灯控制、对焦模式切换这些,别在启动时一股脑全配好。先出画面,再慢慢配。
- 使用CameraProvider:Android 10+推荐用CameraX或Camera2的扩展,底层做了很多预热优化。
// 预加载CameraService示例
public class CameraApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// 提前绑定CameraService
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
// 这里只是触发Service启动,不实际打开Camera
manager.registerAvailabilityCallback(executor, new CameraManager.AvailabilityCallback() {
@Override
public void onCameraAvailable(String cameraId) {
// CameraService已就绪
}
});
}
}
嗯,这里要注意:预加载别做得太重。我曾经试过在Application里直接打开Camera,结果导致ANR——因为主线程被占住了。预加载只是触发Service绑定,不是真的打开Camera。
预览延迟优化:让画面丝滑起来
预览延迟,说白了就是画面跟不上手指移动的速度。用户转个身,画面还在半秒前的位置,这体验太糟糕了。
预览延迟的根源,通常是这几个:
- SurfaceView/TextureView的缓冲区交换不及时
- HAL层处理帧数据太慢
- 应用层做了太多图像处理
我的经验:预览延迟优化,优先检查Surface的缓冲区大小。默认的缓冲区可能只有3个,如果处理不过来就会丢帧。我建议至少配置4个缓冲区。
// 配置预览缓冲区大小
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map != null) {
// 获取支持的输出尺寸
Size[] outputSizes = map.getOutputSizes(ImageFormat.YUV_420_888);
// 选择合适的分辨率,不要一味追求最大
Size previewSize = chooseOptimalSize(outputSizes, targetWidth, targetHeight);
// 创建CaptureRequest时设置
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
new Range<Integer>(30, 30)); // 锁定30fps
}
还有一个容易被忽略的点——预览分辨率。很多App直接取最大分辨率,结果帧率掉到15fps。你想想看,1080p和4K的预览,对带宽要求差了多少?我一般建议预览分辨率不超过1080p,除非有特殊需求。
拍照延迟优化:按下快门到出片
拍照延迟,是用户感知最明显的痛点。按下快门,等半天才听到快门声,用户心里肯定在骂娘。
拍照延迟的构成:
| 阶段 | 耗时占比 | 优化方向 |
|---|---|---|
| Capture Request提交 | 10% | 减少Request构建开销 |
| HAL处理与ISP | 40% | 使用ZSL模式 |
| 图像编码 | 30% | 硬件编码器/JPEG压缩优化 |
| 文件写入 | 20% | 异步写入/内存映射 |
我个人习惯,拍照延迟优化优先考虑ZSL(Zero Shutter Lag)模式。ZSL的原理很简单:预览时一直在循环缓冲区里存着帧数据,按下快门时直接从缓冲区取,而不是重新Capture。这样能省掉几十到几百毫秒。
注意:ZSL模式会持续占用内存,因为要维护一个帧缓冲区。我见过有App开了ZSL后内存涨了200MB,结果在低端机上直接OOM。所以ZSL的缓冲区大小要控制好,一般3-5帧就够了。
// 开启ZSL模式
captureRequestBuilder.set(CaptureRequest.CONTROL_ENABLE_ZSL, 1);
// 设置缓冲区大小(部分厂商支持)
captureRequestBuilder.set(CaptureRequest.CONTROL_ZSL_CAPTURE_TRIGGER_ID, triggerId);
还有一个技巧——预对焦。用户打开Camera时,先做一次自动对焦。等用户按下快门时,对焦已经完成了,省掉对焦时间。我做过测试,预对焦能省掉约150ms。
内存占用优化与OOM预防
内存问题,是Camera开发中最头疼的。一张照片可能就10MB+,如果处理不当,内存分分钟爆掉。
内存占用的主要来源:
- 预览帧数据(YUV/RGB)
- 拍照帧数据(RAW/YUV/JPEG)
- 图像处理中间结果
- 编码器/解码器缓冲区
我遇到过最惨的一次,是在某款低端机上,拍照时直接OOM崩溃。后来发现是Bitmap没有及时回收。Camera的回调里拿到的Image对象,如果不及时close,底层的内存就不会释放。
OOM预防三板斧:
- 及时释放Image对象:在ImageReader的回调中,用完就close,别等GC。
- 控制并发帧数:不要同时处理太多帧,设置合理的maxImages。
- 使用内存池:复用ByteBuffer和Bitmap,减少频繁分配。
// 正确的Image处理方式
ImageReader.OnImageAvailableListener listener = reader -> {
Image image = null;
try {
image = reader.acquireLatestImage(); // 只取最新帧
if (image != null) {
// 处理图像...
processImage(image);
}
} finally {
if (image != null) {
image.close(); // 必须释放
}
}
};
还有一个容易被忽视的点——YUV转Bitmap。很多开发者习惯把YUV数据转成Bitmap再处理,但Bitmap在Java堆上分配,很容易触发GC。我建议直接用ImageReader的getPlanes()操作YUV数据,或者用RenderScript做GPU加速转换。
避坑指南:我曾经在某个项目里,把预览帧全部转成Bitmap存到ArrayList里做回放。结果用户拍了10张照片后,内存涨了300MB,直接OOM。后来改成只存YUV数据,内存降了70%。
知识体系总览
下面这张图,是我整理的Camera性能优化核心逻辑。你可以看到,启动、预览、拍照、内存这四个维度是相互关联的。比如启动优化做得好,预览延迟也会降低;内存优化做得好,拍照延迟也会改善。
最后说一句,性能优化没有银弹。每个项目、每款机型的情况都不一样。我建议你在做优化时,先用adb shell dumpsys camera和Profiler工具把瓶颈定位清楚,再针对性地优化。别一上来就改代码,那样容易白费功夫。
公众号:蓝海资料掘金营,微信deep3321