9、Camera2+SurfaceView:Camera2 API架构、CameraManager与CameraDevice、CaptureRequest与CaptureSession、预览与拍照

好,咱们今天来聊聊 Camera2 和 SurfaceView 的配合。说实话,Camera2 这套 API 刚出来的时候,我也觉得挺头疼的。它不像 Camera1 那样简单粗暴,直接 open 一下就能用。但用久了你会发现,这种设计其实更合理——它把控制权真正交到了开发者手里。

我个人习惯把 Camera2 理解成一套「管道系统」。你想想看,每一帧数据就像水流,你需要先打开阀门(CameraDevice),然后铺设管道(CaptureSession),最后设定水流的方向和压力(CaptureRequest)。这套流程走通了,预览和拍照就水到渠成了。

核心要点:Camera2 的核心思想是「请求-响应」模型。你发出一个 CaptureRequest,系统就给你返回一帧数据。就这么简单,但背后的架构设计非常精妙。

9.1 Camera2 API 整体架构

先看一张图,把整个架构理清楚。我当年第一次接触 Camera2 时,就是靠这张图把各个组件的关系搞明白的。

Camera2 架构核心组件关系图 CameraManager CameraDevice CaptureSession CaptureRequest Surface 输出 CameraCharacteristics 流程:CameraManager 获取设备 → 打开 CameraDevice → 创建 CaptureSession → 提交 CaptureRequest

这张图里,CameraManager 是入口,负责枚举和打开摄像头。CameraDevice 代表物理摄像头。CaptureSession 是会话管道。CaptureRequest 是每一帧的配置指令。右侧的 Surface 就是咱们的 SurfaceView 提供的画布。

我的经验:刚开始接触时,最容易搞混的是 CaptureSession 和 CaptureRequest 的关系。记住一句话:Session 是长连接,Request 是短指令。Session 一旦创建,可以反复提交不同的 Request。

9.2 CameraManager 与 CameraDevice

先说说 CameraManager。它是系统服务,通过 getSystemService() 获取。它的主要工作是两件:

  • 枚举摄像头:调用 getCameraIdList() 获取所有摄像头 ID
  • 获取特性:通过 getCameraCharacteristics(id) 拿到每个摄像头的参数
  • 打开设备:调用 openCamera() 异步打开 CameraDevice

我记得有一次在项目中,客户反馈说某些手机上后置摄像头打不开。排查了半天,发现是 getCameraIdList() 返回的 ID 列表里,第一个 ID 竟然是前置摄像头。不同厂商的 ID 排序不一样,这个坑我踩过。

// 获取 CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

// 枚举摄像头
String[] cameraIdList = manager.getCameraIdList();
for (String id : cameraIdList) {
    CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
    Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
    if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
        // 找到后置摄像头
        break;
    }
}

// 打开摄像头
manager.openCamera(cameraId, new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        // CameraDevice 已就绪
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice camera) {
        camera.close();
    }

    @Override
    public void onError(@NonNull CameraDevice camera, int error) {
        camera.close();
    }
}, backgroundHandler);

这里要注意,openCamera() 是异步的。你调用后不会立刻拿到 CameraDevice,而是通过回调通知。为什么这么设计?因为打开摄像头涉及硬件初始化,可能耗时几十毫秒。如果在主线程同步等待,界面就卡住了。

避坑指南:我曾经在 onOpened 回调里直接创建 CaptureSession,结果崩溃了。原因是 SurfaceView 的 Surface 还没创建好。一定要等 SurfaceView 的 SurfaceHolder.Callback.surfaceCreated 触发后,再创建 Session。

9.3 CaptureRequest 与 CaptureSession

好,现在 CameraDevice 已经打开了。接下来要创建 CaptureSession。说白了,Session 就是一条「数据管道」,你把 Surface 塞进去,告诉系统:「我要把数据送到这里」。

CaptureRequest 则是「管道阀门」——控制每一帧的参数:曝光、对焦、白平衡、输出尺寸等等。每个 Request 可以绑定一个或多个 Surface 输出。

// 创建 CaptureSession
List<Surface> outputs = new ArrayList<>();
Surface surface = surfaceView.getHolder().getSurface();
outputs.add(surface);

cameraDevice.createCaptureSession(outputs, new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(@NonNull CameraCaptureSession session) {
        // Session 已就绪,可以提交 Request
        captureSession = session;
        startPreview();
    }

    @Override
    public void onConfigureFailed(@NonNull CameraCaptureSession session) {
        // 配置失败,通常是因为 Surface 不合法
    }
}, backgroundHandler);

// 提交预览 Request
private void startPreview() {
    CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
    builder.addTarget(surfaceView.getHolder().getSurface());
    // 设置自动对焦
    builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
    // 设置自动曝光
    builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);

    CaptureRequest request = builder.build();
    captureSession.setRepeatingRequest(request, null, backgroundHandler);
}

这里有个关键点:setRepeatingRequest 表示持续不断地发送请求,适合预览。capture 则只发一次,适合拍照。你想想看,预览需要每帧都更新,所以用重复模式;拍照只需要一帧,所以用单次模式。

9.4 预览与拍照实战

预览的流程我们已经走通了。拍照其实就是在预览的基础上,额外提交一个单次 CaptureRequest,并把输出指向另一个 Surface——通常是 ImageReader。

// 创建 ImageReader 用于拍照
ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);
imageReader.setOnImageAvailableListener(reader -> {
    Image image = reader.acquireLatestImage();
    // 处理图片数据
    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    // 保存或显示
    image.close();
}, backgroundHandler);

// 拍照
private void takePhoto() {
    try {
        CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.addTarget(imageReader.getSurface());
        // 可以设置闪光灯、对焦模式等
        builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_AUTO);

        captureSession.capture(builder.build(), new CameraCaptureSession.CaptureCallback() {
            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                           @NonNull CaptureRequest request,
                                           @NonNull TotalCaptureResult result) {
                // 拍照完成
            }
        }, backgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

我的建议:拍照时别忘了先锁定焦点和曝光。我习惯在拍照前先调用 captureSession.capture(lockRequest, callback, handler) 锁定 AE/AF,拍完后再解锁。否则拍出来的照片可能对焦不准或曝光异常。

最后,整理一下整个流程的时序:

步骤 操作 说明
1 获取 CameraManager 系统服务,全局唯一
2 打开 CameraDevice 异步操作,等待 onOpened
3 等待 Surface 就绪 SurfaceHolder.Callback.surfaceCreated
4 创建 CaptureSession 绑定 Surface 输出
5 提交预览 Request setRepeatingRequest 持续输出
6 拍照时提交单次 Request capture 方法,输出到 ImageReader
7 关闭资源 关闭 Session、Device、ImageReader

嗯,到这里 Camera2 + SurfaceView 的核心流程就讲完了。说白了就是三步:打开设备、建立管道、发送请求。每一步都有对应的回调,每一步都要注意异步时序。我在项目中见过太多因为时序问题导致的崩溃——比如 Surface 还没创建就提交 Request,或者 Session 还没配置完成就调用 capture。

记住一句话:Camera2 是异步的,你的代码也必须是异步的。别想着用 Thread.sleep 去等,那是给自己挖坑。


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