7. 实现Camera预览:启动预览循环、处理预览帧数据、预览性能优化

Camera预览,说白了就是让摄像头把看到的东西实时显示在屏幕上。这步搞不定,后面所有视频处理都是空谈。我刚开始做Camera开发时,以为预览就是调个API完事,结果被各种黑屏、卡顿、OOM折磨得够呛。今天咱们就把这块彻底讲透。

7.1 预览循环的核心机制

Android Camera的预览,本质上是一个生产者-消费者模型。Camera硬件是生产者,不断产生帧数据;你的App是消费者,需要及时处理这些帧。中间有个缓冲区队列在协调双方节奏。

关键点:预览循环不是你自己写while(true)去轮询,而是通过回调机制被动接收帧数据。你注册一个回调,CameraService每采集到一帧,就通知你一次。

在Camera2 API中,预览循环通过CaptureRequestCaptureSession配合实现。大致流程是:

  1. 创建CaptureRequest.Builder,绑定到Surface(比如TextureView的Surface)
  2. 调用CaptureSession.setRepeatingRequest()启动持续捕获
  3. 系统自动循环发送请求,你只需在回调里处理结果
// 启动预览循环的核心代码
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预览核心流程与优化 启动预览循环 setRepeatingRequest 接收帧数据 onCaptureCompleted / ImageReader 处理与显示 GPU合成 / CPU处理 分辨率选择 StreamConfigurationMap 缓冲区管理 复用ByteBuffer / 降帧 处理链路优化 TextureView / 双路输出 目标:稳定25fps+,内存不暴涨,CPU不过热 提示:用Choreographer或计时器叠加帧率显示,方便调试

嗯,预览这块内容其实不少。但核心就三件事:启动循环、处理数据、优化性能。把这三件事理顺了,Camera预览就稳了。我在实际项目中,通常先把基础预览跑通,然后逐步加上帧处理逻辑,最后根据性能数据做针对性优化。千万别一上来就想搞个大而全的方案,容易翻车。

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