4. Preview性能优化:SurfaceView vs TextureView vs GLSurfaceView选择、预览帧率控制、预览分辨率与比例适配、预览延迟优化
预览性能,说白了就是摄像头画面从传感器到你眼睛的这段旅程,到底顺不顺、快不快。我做了这么多年Camera HAL,见过太多项目在预览这块翻车——画面卡顿、比例变形、延迟高得离谱。今天咱们就把这几个核心问题掰开揉碎,一个一个讲清楚。
4.1 SurfaceView vs TextureView vs GLSurfaceView:到底选哪个?
这个问题,几乎每个做Camera开发的同事都会问我。我的回答很简单:没有银弹,只有最适合你场景的选择。
核心结论:
- SurfaceView:性能最优,延迟最低,适合纯预览、录像场景
- TextureView:灵活性高,支持动画、变换,但性能有损耗
- GLSurfaceView:需要OpenGL渲染时使用,比如滤镜、特效
我习惯用一张表来对比,这样最直观:
| 特性 | SurfaceView | TextureView | GLSurfaceView |
|---|---|---|---|
| 渲染方式 | 独立Surface,独立窗口 | 与View共享Surface | OpenGL ES渲染 |
| 性能 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
| 延迟 | 最低(约1-2帧) | 较高(约2-3帧) | 中等(约1-2帧) |
| 动画/变换支持 | 差(需额外处理) | 好(原生支持) | 好(需自己实现) |
| 适用场景 | 预览、录像、扫码 | 需要动画的预览 | 滤镜、特效、AR |
嗯,这里要注意:SurfaceView在Android 7.0之后有了重大改进。以前它有个痛点——不能做平移、缩放、旋转等动画。但从API 24开始,Google给SurfaceView加了一个setTransitionVisibility方法,配合SurfaceControl,可以实现类似TextureView的变换效果。我在一个项目中就用这个方案替代了TextureView,预览延迟从3帧降到了1帧。
我的建议:
如果你只是做普通预览,别犹豫,直接上SurfaceView。如果你需要做动画或者变换,先试试SurfaceView能不能满足,实在不行再换TextureView。至于GLSurfaceView,除非你确定要用OpenGL,否则别碰——它带来的复杂度远比你想象的高。
4.2 预览帧率控制:别让CPU白忙活
帧率控制,说白了就是告诉摄像头:你每秒给我多少帧就够了。我见过不少项目,明明只需要15fps的预览,却让摄像头跑30fps,结果CPU和GPU都在做无用功,手机还发烫。
Android Camera2 API提供了两种控制帧率的方式:
- 通过
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE:设置目标帧率范围 - 通过
CaptureRequest.CONTROL_AE_MODE:配合帧率控制策略
我个人习惯这样设置:
// 设置帧率范围,比如15-30fps
Range<Integer> fpsRange = new Range<>(15, 30);
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
// 如果对帧率稳定性要求高,可以锁定帧率
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
为什么会这样?因为CONTROL_AE_TARGET_FPS_RANGE告诉HAL层:你可以在15到30fps之间自由调整。如果场景光线充足,它会尽量跑30fps;如果光线暗,它会降到15fps来保证曝光时间。这样既保证了流畅度,又避免了暗光下的噪点问题。
避坑指南:
我曾经在一个项目中,把帧率范围设成了new Range<>(30, 30),以为这样就能锁定30fps。结果在暗光环境下,预览画面变得非常暗,而且帧率反而掉到了10fps以下。后来才发现,锁定帧率不等于锁定性能——HAL层在暗光下会优先保证曝光,而不是帧率。
所以我的建议是:不要锁定帧率,而是给一个合理的范围。比如预览用15-30fps,录像用24-30fps,扫码用10-15fps就够了。
4.3 预览分辨率与比例适配:别让画面变形
这个问题,说白了就是:摄像头拍出来的画面,怎么完美地显示在屏幕上?你想想看,如果摄像头输出的是4:3的画面,但屏幕是16:9的,直接拉伸就会变形。
我总结了一套适配流程,你可以参考:
- 获取屏幕尺寸:拿到屏幕的宽高
- 获取摄像头支持的分辨率列表:通过
StreamConfigurationMap.getOutputSizes() - 选择最佳分辨率:根据屏幕比例,选择最接近的摄像头分辨率
- 计算显示区域:用
Matrix做缩放和裁剪
代码实现大概是这样的:
// 选择最佳预览分辨率
private Size chooseOptimalSize(Size[] choices, int width, int height) {
// 先按比例筛选
float targetRatio = (float) width / height;
List<Size> sameRatioSizes = new ArrayList<>();
for (Size size : choices) {
float ratio = (float) size.getWidth() / size.getHeight();
if (Math.abs(ratio - targetRatio) < 0.01f) {
sameRatioSizes.add(size);
}
}
// 再选分辨率最大的
if (!sameRatioSizes.isEmpty()) {
return Collections.max(sameRatioSizes,
(a, b) -> Long.compare(a.getWidth() * a.getHeight(),
b.getWidth() * b.getHeight()));
}
// 没有完全匹配比例的,选最接近的
return choices[0];
}
我的经验:
别选最高分辨率!我见过有人直接选摄像头支持的最大分辨率,结果预览卡成PPT。一般来说,1080p对于大多数手机预览已经足够了。如果你做扫码或者人脸识别,720p甚至480p都够用。分辨率越高,带宽和功耗就越大,没必要。
另外,比例适配还有一个坑:摄像头方向。手机摄像头默认是横屏的,如果你竖屏预览,需要旋转画面。我习惯在onConfigure时设置CameraCharacteristics.SENSOR_ORIENTATION,然后通过TextureView.setTransform()或者SurfaceView的SurfaceControl来做旋转。
4.4 预览延迟优化:从传感器到屏幕的每一毫秒
预览延迟,就是摄像头拍到画面到你看到画面的时间差。这个值如果超过100ms,用户就能明显感觉到卡顿。我做过一个项目,延迟高达200ms,用户拍照时总是抓不到想要的瞬间。
优化预览延迟,我总结了四个方向:
| 优化方向 | 具体措施 | 预期效果 |
|---|---|---|
| 1. 减少缓冲区数量 | 使用SurfaceView,减少BufferQueue深度 |
降低1-2帧延迟 |
| 2. 降低分辨率 | 选择合适的分辨率,不要盲目追求高分辨率 | 降低处理时间 |
| 3. 关闭不必要的处理 | 关闭3A算法中的某些模块(如AF、AWB) | 降低每帧处理时间 |
| 4. 使用低延迟模式 | 设置CONTROL_AE_MODE_ON配合低延迟策略 |
降低AE收敛时间 |
嗯,这里我要重点说一下缓冲区优化。Android Camera的预览流程是这样的:
传感器 → ISP处理 → 缓冲区1 → 缓冲区2 → SurfaceFlinger → 屏幕显示
默认情况下,BufferQueue有3个缓冲区(triple buffering)。如果你用SurfaceView,可以尝试减少到2个(double buffering),这样能降低1帧的延迟。但要注意,减少缓冲区可能会引起丢帧,需要根据实际场景权衡。
我的实战经验:
在一个低延迟预览项目中,我做了三件事:
- 用SurfaceView替代TextureView(降低1帧延迟)
- 将预览分辨率从1080p降到720p(降低处理时间)
- 关闭了自动对焦和自动白平衡(降低每帧计算量)
最终,预览延迟从120ms降到了45ms。用户反馈说「终于能抓拍到瞬间了」。
另外,别忘了检查HAL层的实现。有些HAL在预览时做了额外的处理,比如降噪、锐化,这些都会增加延迟。如果你对画质要求不高,可以尝试关闭这些后处理。
避坑指南:
我曾经在一个项目中,为了降低延迟,把预览分辨率降到了480p。结果发现画面模糊得不行,用户投诉说「连人脸都看不清」。后来才意识到,分辨率不是越低越好——太低了,画面细节丢失,用户体验反而更差。建议至少保持720p,除非你的应用场景对画质完全没要求(比如扫码)。
知识体系总览
最后,我用一张图来总结本章的核心内容。这张图展示了预览性能优化的四个维度,以及它们之间的关系:
这张图把本章的四个核心知识点串在了一起。你想想看,视图选择决定了渲染路径,帧率控制决定了CPU负载,分辨率适配决定了画面质量,延迟优化决定了用户体验——它们之间是相互影响的。比如你选了TextureView,延迟就会高一些,那你就得在帧率和分辨率上做补偿。
好了,预览性能优化就讲到这里。记住一句话:没有最好的方案,只有最适合你场景的方案。动手试试吧,有问题随时交流。