5. 预览流启动优化:SurfaceView vs TextureView选择、预览尺寸裁剪、降低初始帧延迟
相机预览流的启动速度,直接决定了用户按下快门后的第一印象。说白了,就是那个「黑屏转瞬即逝」的过程,到底要多久。
我做过一个项目,用户反馈相机启动慢,一测发现预览流从open到第一帧显示,花了将近800ms。嗯,这显然不行。今天我们就来拆解预览流启动的三个核心优化点。
5.1 SurfaceView vs TextureView:选对容器,少走弯路
这两个东西,都是用来承载相机预览画面的。但它们的底层机制完全不同。
| 对比维度 | SurfaceView | TextureView |
|---|---|---|
| 渲染方式 | 独立Surface,独立窗口 | 共享Surface,与View共用窗口 |
| 性能开销 | 低,直接走硬件合成 | 高,需要额外走GPU渲染 |
| 动画/变换 | 不支持(需额外处理) | 天然支持 |
| 启动延迟 | 低(约快30-50ms) | 高(需等待SurfaceTexture就绪) |
| 适用场景 | 纯预览、高帧率 | 需要动画、滤镜、截图 |
我个人习惯,如果只是做相机预览,没有复杂的动画需求,无脑选SurfaceView。为什么?
- SurfaceView的Surface创建更快。它直接向WMS申请一个独立窗口,不需要经过View的绘制流程。
- TextureView需要等View树测量、布局、绘制完成后,SurfaceTexture才能拿到第一帧。这个过程至少多出1-2个vsync周期。
核心结论: 纯预览场景,SurfaceView比TextureView快30-50ms。如果你不需要动画,别犹豫。
我在项目中遇到过一个问题:用TextureView做预览,每次启动都会闪一下黑屏。后来发现是SurfaceTexture的onFrameAvailable回调时机太晚。换成SurfaceView后,问题直接消失。
小技巧: 如果非要使用TextureView,可以在onSurfaceTextureAvailable回调中提前创建CameraSession,不要等View完全显示。
5.2 预览尺寸裁剪:别让相机输出无用像素
相机传感器输出的原始分辨率,通常远大于屏幕显示区域。比如后置4800万像素,屏幕只有1080p。如果你不裁剪,相机就要处理大量无用像素,白白浪费带宽和算力。
你想想看,每一帧多处理几百万像素,累积下来就是几百毫秒的延迟。
正确的做法是:选择最接近屏幕比例的预览尺寸,然后裁剪到屏幕分辨率。
// 选择最佳预览尺寸
private Size chooseOptimalSize(Size[] choices, int targetWidth, int targetHeight) {
// 1. 先过滤出比例接近的
List<Size> sameRatioSizes = new ArrayList<>();
for (Size size : choices) {
float ratio = (float) size.getWidth() / size.getHeight();
float targetRatio = (float) targetWidth / targetHeight;
if (Math.abs(ratio - targetRatio) < 0.01f) {
sameRatioSizes.add(size);
}
}
// 2. 选择最接近目标尺寸的
Size optimalSize = sameRatioSizes.get(0);
for (Size size : sameRatioSizes) {
if (size.getWidth() <= targetWidth && size.getHeight() <= targetHeight) {
if (size.getWidth() >= optimalSize.getWidth()) {
optimalSize = size;
}
}
}
return optimalSize;
}
这段代码的逻辑很简单:先找比例一致的,再找分辨率最接近但不超标的。我建议你把这个方法封装成工具类,每个相机项目都能复用。
注意: 不要直接使用最大分辨率。比如屏幕是1080x1920,你选一个4000x3000的预览尺寸,每一帧要多处理近3倍的数据。启动延迟直接翻倍。
我曾经在一个低端机上测试,不裁剪时预览启动耗时1.2秒,裁剪到1080p后降到0.6秒。效果立竿见影。
5.3 降低初始帧延迟:让第一帧来得更快
初始帧延迟,就是从Camera.open到第一帧显示到屏幕上的时间。这个时间通常由以下几个环节组成:
- Camera.open():打开硬件设备,约50-100ms
- configureStreams():配置数据流,约30-80ms
- createSession():创建会话,约50-150ms
- setRepeatingRequest():开始预览,约20-50ms
- 第一帧到达:等待硬件输出,约30-60ms
加起来,轻松超过300ms。怎么优化?
5.3.1 预热Camera设备
在应用启动阶段,提前调用CameraManager.getCameraCharacteristics(),把设备信息缓存起来。这样真正打开相机时,省去了一次查询时间。
// 提前预热
public void warmUpCamera(Context context) {
CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
String[] ids = manager.getCameraIdList();
for (String id : ids) {
CameraCharacteristics chars = manager.getCameraCharacteristics(id);
// 缓存到全局变量
cachedCharacteristics.put(id, chars);
}
} catch (CameraAccessException e) {
// 忽略
}
}
5.3.2 使用低延迟配置
在创建CameraCaptureSession时,使用CameraDevice.TEMPLATE_PREVIEW模板。这个模板专门为预览优化,延迟最低。
// 使用预览模板
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
5.3.3 减少第一帧的等待
默认情况下,setRepeatingRequest会等待AE/AWB收敛后才输出第一帧。我们可以设置CONTROL_AE_LOCK和CONTROL_AWB_LOCK为true,跳过自动收敛过程。
// 跳过自动收敛,加速第一帧
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.set(CaptureRequest.CONTROL_AE_LOCK, true);
builder.set(CaptureRequest.CONTROL_AWB_LOCK, true);
// 第一帧显示后,再解锁
session.setRepeatingRequest(builder.build(), callback, handler);
经验之谈: 第一帧显示后,立即解锁AE和AWB。这样既保证了启动速度,又不会影响后续的画质。
5.4 知识体系总览
下面这张图,把预览流启动优化的三个核心维度串起来了。你可以把它当作一个检查清单。
这三个优化点,每一个都能带来几十到上百毫秒的提升。组合起来,效果非常可观。我建议你在项目里按这个顺序排查:先选对容器,再裁剪尺寸,最后优化第一帧。
嗯,预览流启动优化就讲到这里。下一节我们会深入聊聊拍照流程的优化,包括连拍、HDR和夜景模式下的延迟控制。
公众号:蓝海资料掘金营,微信deep3321