7、Preview 预览流程:SurfaceView/TextureView配置、PreviewRequest循环、PreviewCallback回调、帧率控制与丢帧处理

预览,说白了就是让相机把取景框里的画面实时显示到屏幕上。这是整个相机应用最基础、最核心的功能之一。我刚开始做相机开发时,觉得预览不就是把数据扔给Surface吗?后来踩了不少坑才发现,这里面的门道远比想象中多。

今天我们就来聊聊预览流程中的几个关键环节:Surface的选择与配置、PreviewRequest的循环机制、回调处理,以及最让人头疼的帧率控制和丢帧问题。

7.1 SurfaceView vs TextureView:选谁?

Android提供了两种主要的预览显示方案:SurfaceView和TextureView。很多新手会纠结选哪个,我个人的经验是——没有银弹,看场景。

特性 SurfaceView TextureView
渲染方式 独立Surface,独立窗口 与View共享Surface
性能 高,硬件加速友好 中等,需要额外合成
动画/变换 不支持(需额外处理) 支持,可做缩放旋转
生命周期 需监听surfaceCreated/Destroyed 随View生命周期
适用场景 全屏预览、高性能需求 需要动画、非全屏预览

我记得有一次做直播应用,用了TextureView来做美颜滤镜的预览。结果在低端机上帧率直接掉到15fps。后来换成SurfaceView配合GPU渲染,帧率稳定在30fps。说白了,如果你不需要对预览画面做复杂的变换或动画,SurfaceView永远是性能首选。

7.1.1 SurfaceView配置要点

配置SurfaceView时,有个坑我踩过好几次——Surface的创建时机。Camera的预览Session必须在Surface创建完成后才能启动。代码上要这样处理:

SurfaceView surfaceView = findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface已创建,可以开始预览
        startPreview(holder.getSurface());
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // 尺寸变化时,需要重新配置
        stopPreview();
        startPreview(holder.getSurface());
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface销毁,必须停止预览
        stopPreview();
    }
});
注意:千万不要在onCreate里直接拿Surface!我曾经犯过这个错,结果在部分机型上预览黑屏。SurfaceView的Surface是异步创建的,必须通过回调来获取。

7.1.2 TextureView配置要点

TextureView相对简单一些,因为它本身就是一个View,Surface随View生命周期走。但要注意的是,TextureView的SurfaceTexture也需要等待可用:

TextureView textureView = findViewById(R.id.texture_view);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture可用
        Surface previewSurface = new Surface(surface);
        startPreview(previewSurface);
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // 尺寸变化处理
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        stopPreview();
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // 每帧更新回调,慎用!性能开销大
    }
});
小技巧:onSurfaceTextureUpdated回调每帧都会触发,如果你在里面做耗时操作,帧率会直线下降。我一般只在调试时用它,线上版本绝对禁用。

7.2 PreviewRequest循环:谁在驱动预览?

预览的本质是什么?说白了就是相机硬件不断捕获图像,然后送给Surface去显示。在Camera2 API中,这个循环由PreviewRequest驱动。

每次预览循环大致是这样的:

  1. App创建CaptureRequest,指定目标Surface
  2. CameraSession通过CaptureSession.setRepeatingRequest()提交请求
  3. 相机硬件捕获一帧图像
  4. 图像数据写入Surface
  5. Surface显示或处理数据
  6. 重复步骤3-5

嗯,这里有个关键点——setRepeatingRequestcapture的区别。前者是持续循环,后者只拍一张。预览当然用setRepeatingRequest。

// 创建预览请求
CaptureRequest.Builder previewBuilder = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW);
previewBuilder.addTarget(previewSurface);

// 开始循环预览
cameraCaptureSession.setRepeatingRequest(
    previewBuilder.build(),
    previewCallback,  // 回调
    backgroundHandler
);

你想想看,这个循环一旦启动,相机就会以最高帧率持续输出。但问题来了——如果Surface处理不过来怎么办?这就引出了丢帧问题。

7.3 PreviewCallback回调:数据从哪来?

PreviewCallback是Camera2中获取预览帧数据的接口。它有两个回调方法:

  • onCaptureStarted:一帧开始捕获时触发
  • onCaptureCompleted:一帧捕获完成时触发

我习惯在onCaptureCompleted里获取帧数据,因为这时候数据已经写入Surface了。但要注意,这个回调的频率非常高——如果是30fps,每秒就会触发30次。

CameraCaptureSession.CaptureCallback previewCallback = 
    new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureStarted(@NonNull CameraCaptureSession session,
                                  @NonNull CaptureRequest request,
                                  long timestamp, long frameNumber) {
        // 帧开始,可以做些轻量级统计
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                    @NonNull CaptureRequest request,
                                    @NonNull TotalCaptureResult result) {
        // 帧完成,可以获取元数据
        Long exposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);
        Integer iso = result.get(CaptureResult.SENSOR_SENSITIVITY);
        
        // 注意:这里不要做耗时操作!
        // 如果需要处理图像数据,建议用ImageReader
    }
};
核心原则:回调里不要做任何耗时操作!我曾经在onCaptureCompleted里做图像格式转换,结果帧率直接掉到10fps以下。正确的做法是——用ImageReader异步获取帧数据,或者把数据扔到工作线程处理。

7.4 帧率控制:别让相机跑太快

帧率控制,说白了就是告诉相机「你慢点拍」。为什么要控制?两个原因:

  • 性能考虑:高帧率意味着高功耗、高发热
  • 画面稳定:帧率波动比低帧率更影响体验

在Camera2中,帧率控制主要通过CONTROL_AE_TARGET_FPS_RANGE来实现:

// 设置目标帧率范围
Range<Integer> fpsRange = new Range<>(15, 30);
previewBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);

这里有个坑——你设置的范围不一定能精确达到。比如你设了30fps,但光线不足时,相机可能会自动降到15fps。这是正常的,因为AE(自动曝光)会优先保证画面亮度。

我记得有一次做扫码应用,需要稳定的30fps。结果在暗光环境下帧率掉到10fps,扫码成功率直线下降。后来我加了一个策略:当帧率低于阈值时,主动降低分辨率来换取帧率。

7.5 丢帧处理:数据去哪了?

丢帧,是预览中最常见也最头疼的问题。为什么会丢帧?说白了就是「生产者太快,消费者太慢」。

相机硬件以固定帧率生产图像数据,但App的处理速度跟不上,导致部分帧被丢弃。丢帧的表现就是画面卡顿、不流畅。

我总结了几种常见的丢帧原因和解决方案:

原因 表现 解决方案
Surface处理延迟 画面撕裂、卡顿 使用双缓冲或三缓冲
回调中耗时操作 帧率下降 异步处理,回调只做轻量操作
GPU渲染瓶颈 帧率波动 降低分辨率或使用硬件加速
内存抖动 偶发卡顿 对象复用,减少GC
避坑指南:我曾经在低端机上遇到一个诡异的问题——预览偶尔会卡住几秒钟。排查了半天,发现是ImageReader的OnImageAvailableListener里做了Bitmap转换,导致GC频繁触发。后来改用ByteBuffer直接操作,问题解决。

7.6 知识体系总览

为了让你更直观地理解预览流程的整体架构,我画了一张图:

Preview 预览流程架构图 Surface配置 SurfaceView / TextureView PreviewRequest循环 setRepeatingRequest PreviewCallback onCaptureCompleted 帧率控制 CONTROL_AE_TARGET_FPS_RANGE 丢帧处理 生产者 > 消费者 → 丢帧 关键要点 • Surface必须在创建完成后才能启动预览 • 回调中禁止耗时操作,使用ImageReader异步处理 • 帧率控制需平衡性能与画面质量

这张图把预览流程分成了三个主要阶段:Surface配置、PreviewRequest循环、回调处理。下方是帧率控制和丢帧处理这两个贯穿始终的优化点。你想想看,任何一个环节出问题,预览都会受影响。

7.7 实战建议

最后,我分享几个实战中的小建议:

  • 优先用SurfaceView:除非你需要动画或变换,否则SurfaceView性能更好
  • 帧率不要设太高:30fps对大多数场景已经足够,60fps带来的功耗和发热不值得
  • 监控丢帧率:通过onCaptureStarted和onCaptureCompleted的时间戳可以计算实际帧率
  • 预留缓冲:使用双缓冲或三缓冲机制,避免Surface写入冲突
个人经验:我习惯在调试版本里加一个帧率监控器,实时显示当前帧率和丢帧数。这样在真机测试时,一眼就能看出性能瓶颈在哪。线上版本记得关掉,不然会影响性能。

预览流程看似简单,但要做好却不容易。从Surface的选择到帧率的控制,每一步都需要仔细权衡。希望今天的分享能帮你少踩一些坑。


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