7. 实现Camera预览:启动预览循环、处理预览帧数据、预览性能优化
Camera预览,说白了就是让摄像头把看到的东西实时显示在屏幕上。这步搞不定,后面所有视频处理都是空谈。我刚开始做Camera开发时,以为预览就是调个API完事,结果被各种黑屏、卡顿、OOM折磨得够呛。今天咱们就把这块彻底讲透。
7.1 预览循环的核心机制
Android Camera的预览,本质上是一个生产者-消费者模型。Camera硬件是生产者,不断产生帧数据;你的App是消费者,需要及时处理这些帧。中间有个缓冲区队列在协调双方节奏。
关键点:预览循环不是你自己写while(true)去轮询,而是通过回调机制被动接收帧数据。你注册一个回调,CameraService每采集到一帧,就通知你一次。
在Camera2 API中,预览循环通过CaptureRequest和CaptureSession配合实现。大致流程是:
- 创建
CaptureRequest.Builder,绑定到Surface(比如TextureView的Surface) - 调用
CaptureSession.setRepeatingRequest()启动持续捕获 - 系统自动循环发送请求,你只需在回调里处理结果
// 启动预览循环的核心代码
private void startPreviewLoop() {
try {
// 创建预览请求
CaptureRequest.Builder previewBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewBuilder.addTarget(surfaceView.getHolder().getSurface());
// 设置连续自动对焦
previewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 启动重复请求
captureSession.setRepeatingRequest(
previewBuilder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(...) {
// 每帧完成后的回调
// 这里可以做帧率统计、日志等轻量操作
}
},
backgroundHandler
);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
我的经验:setRepeatingRequest的第三个参数handler很重要。如果你传null,回调会跑在CameraService的binder线程上,容易阻塞。我习惯单独开一个HandlerThread来处理这些回调,避免影响UI线程。
7.2 处理预览帧数据
很多时候我们不满足于只看画面,还要分析每一帧的内容。比如做扫码、人脸检测、滤镜效果等。这时候就需要拿到帧的原始数据。
获取帧数据有两种方式:
| 方式 | 适用场景 | 性能影响 |
|---|---|---|
| ImageReader | 需要逐帧处理YUV/RGB数据 | 中等,需控制帧率 |
| PreviewCallback(Camera1) | 兼容旧设备 | 较高,已废弃 |
| SurfaceTexture + 自定义GL | 需要GPU加速处理 | 较低,推荐 |
我个人最常用的是ImageReader方式。它可以直接拿到YUV_420_888格式的帧数据,而且支持设置最大图片数来控制内存。
// 使用ImageReader获取帧数据
ImageReader imageReader = ImageReader.newInstance(
width, height,
ImageFormat.YUV_420_888,
2 // 最大图片数,建议2-3
);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) {
// 处理帧数据
processFrame(image);
// 必须关闭,否则会卡死
image.close();
}
}
}, backgroundHandler);
// 将ImageReader的Surface添加到CaptureRequest
previewBuilder.addTarget(imageReader.getSurface());
注意:ImageReader的回调频率和预览帧率一致。如果你做实时人脸检测,30fps的数据量可能让CPU直接爆掉。我曾经在一个项目里没控制帧率,结果手机烫得能煎鸡蛋。解决方案是:在回调里做降帧处理,比如每3帧只处理1帧。
7.3 预览性能优化
预览性能优化是个系统工程。我把它总结为三个维度:
- 分辨率选择:不是越高越好。1080p预览在低端机上可能只有15fps,720p反而能跑满30fps
- 缓冲区管理:避免频繁创建和销毁对象,复用缓冲区
- 处理链路:帧数据从Camera到屏幕的路径越短越好
先看分辨率。Camera2 API提供了StreamConfigurationMap来查询支持的预览尺寸。我建议的策略是:
// 选择最优预览尺寸
private Size chooseOptimalSize(Size[] choices, int targetWidth, int targetHeight) {
// 先找完全匹配的
for (Size size : choices) {
if (size.getWidth() == targetWidth &&
size.getHeight() == targetHeight) {
return size;
}
}
// 没有就找最接近的
Size optimalSize = choices[0];
long minDiff = Long.MAX_VALUE;
for (Size size : choices) {
long diff = Math.abs(size.getWidth() - targetWidth) +
Math.abs(size.getHeight() - targetHeight);
if (diff < minDiff) {
minDiff = diff;
optimalSize = size;
}
}
return optimalSize;
}
避坑指南:我曾经在小米某款机型上遇到预览黑屏问题。查了两天才发现是预览尺寸选了设备不支持的格式。记住:一定要从getOutputSizes()返回的列表里选,别自己瞎猜尺寸。
缓冲区复用这块,我有个小技巧:
// 复用ByteBuffer,避免GC抖动
private ByteBuffer reusableBuffer = ByteBuffer.allocateDirect(width * height * 3 / 2);
private void processFrame(Image image) {
Image.Plane[] planes = image.getPlanes();
// 直接拷贝到复用缓冲区
reusableBuffer.clear();
for (Image.Plane plane : planes) {
ByteBuffer buffer = plane.getBuffer();
reusableBuffer.put(buffer);
}
reusableBuffer.flip();
// 用reusableBuffer做后续处理
}
最后说处理链路。如果你只是显示预览,用TextureView配合SurfaceTexture是最优解。它直接走GPU硬件合成,不经过CPU。如果你需要同时显示和处理帧数据,那就用ImageReader + TextureView双路输出。
// 双路输出示例
previewBuilder.addTarget(textureView.getSurfaceTexture()); // 显示用
previewBuilder.addTarget(imageReader.getSurface()); // 处理用
我的习惯:在调试预览性能时,我会在屏幕上叠加一个帧率计数器。用Choreographer或者简单的计时器都能实现。看到帧率稳定在25fps以上,心里才踏实。
7.4 知识体系总览
下面这张图总结了预览模块的核心逻辑和优化方向:
嗯,预览这块内容其实不少。但核心就三件事:启动循环、处理数据、优化性能。把这三件事理顺了,Camera预览就稳了。我在实际项目中,通常先把基础预览跑通,然后逐步加上帧处理逻辑,最后根据性能数据做针对性优化。千万别一上来就想搞个大而全的方案,容易翻车。