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到第一帧显示到屏幕上的时间。这个时间通常由以下几个环节组成:

  1. Camera.open():打开硬件设备,约50-100ms
  2. configureStreams():配置数据流,约30-80ms
  3. createSession():创建会话,约50-150ms
  4. setRepeatingRequest():开始预览,约20-50ms
  5. 第一帧到达:等待硬件输出,约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_LOCKCONTROL_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 知识体系总览

下面这张图,把预览流启动优化的三个核心维度串起来了。你可以把它当作一个检查清单。

预览流启动优化知识体系 预览流启动优化 SurfaceView vs TextureView SurfaceView:独立窗口,延迟低 TextureView:共享窗口,延迟高 纯预览场景选SurfaceView 预览尺寸裁剪 选择最接近屏幕比例 裁剪到屏幕分辨率 减少无用像素处理 降低初始帧延迟 预热Camera设备 使用低延迟配置 跳过AE/AWB收敛 综合优化后,预览启动延迟可降低40%-60% 从800ms优化到300ms以内

这三个优化点,每一个都能带来几十到上百毫秒的提升。组合起来,效果非常可观。我建议你在项目里按这个顺序排查:先选对容器,再裁剪尺寸,最后优化第一帧。

嗯,预览流启动优化就讲到这里。下一节我们会深入聊聊拍照流程的优化,包括连拍、HDR和夜景模式下的延迟控制。


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