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();
}
});
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) {
// 每帧更新回调,慎用!性能开销大
}
});
7.2 PreviewRequest循环:谁在驱动预览?
预览的本质是什么?说白了就是相机硬件不断捕获图像,然后送给Surface去显示。在Camera2 API中,这个循环由PreviewRequest驱动。
每次预览循环大致是这样的:
- App创建CaptureRequest,指定目标Surface
- CameraSession通过CaptureSession.setRepeatingRequest()提交请求
- 相机硬件捕获一帧图像
- 图像数据写入Surface
- Surface显示或处理数据
- 重复步骤3-5
嗯,这里有个关键点——setRepeatingRequest和capture的区别。前者是持续循环,后者只拍一张。预览当然用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
}
};
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 |
7.6 知识体系总览
为了让你更直观地理解预览流程的整体架构,我画了一张图:
这张图把预览流程分成了三个主要阶段:Surface配置、PreviewRequest循环、回调处理。下方是帧率控制和丢帧处理这两个贯穿始终的优化点。你想想看,任何一个环节出问题,预览都会受影响。
7.7 实战建议
最后,我分享几个实战中的小建议:
- 优先用SurfaceView:除非你需要动画或变换,否则SurfaceView性能更好
- 帧率不要设太高:30fps对大多数场景已经足够,60fps带来的功耗和发热不值得
- 监控丢帧率:通过onCaptureStarted和onCaptureCompleted的时间戳可以计算实际帧率
- 预留缓冲:使用双缓冲或三缓冲机制,避免Surface写入冲突
预览流程看似简单,但要做好却不容易。从Surface的选择到帧率的控制,每一步都需要仔细权衡。希望今天的分享能帮你少踩一些坑。
公众号:蓝海资料掘金营,微信deep3321