8. Camera2拍照功能:从按下快门到照片落盘

拍照功能,说白了就是三个步骤:发起请求 → 捕获数据 → 保存文件。但Camera2把这套流程拆得特别细,刚接触时容易懵。我刚开始做的时候也绕了不少弯路,今天咱们就把这条链路彻底捋清楚。

核心流程速览

创建CaptureRequest → 提交到CaptureSession → ImageReader接收数据 → 回调中处理图片 → 保存到相册

创建CaptureRequest 设置TEMPLATE_STILL_CAPTURE 提交到CaptureSession capture() 单次捕获 ImageReader回调 onImageAvailable() 配置ImageReader 宽/高/格式/最大图片数 获取Image对象 reader.acquireLatestImage() 转Bitmap/字节 YUV_420_888 → JPEG 保存到相册 MediaStore / File API 通知系统扫描 sendBroadcast(Intent) 拍照全流程:请求构建 → 捕获触发 → 数据回调 → 图片落地

8.1 创建拍照专用的CaptureRequest

拍照和预览用的CaptureRequest不一样。预览追求流畅,拍照追求画质。我个人习惯把拍照请求单独构建,不和预览混在一起。

// 构建拍照请求
CaptureRequest.Builder photoBuilder = cameraDevice
    .createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

// 设置对焦模式——拍照时用连续自动对焦
photoBuilder.set(CaptureRequest.CONTROL_AF_MODE,
    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

// 设置闪光灯——根据场景自动决定
photoBuilder.set(CaptureRequest.CONTROL_AE_MODE,
    CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

// 设置输出目标——把ImageReader的Surface加进去
photoBuilder.addTarget(imageReader.getSurface());

// 生成最终的CaptureRequest
CaptureRequest photoRequest = photoBuilder.build();

小技巧:TEMPLATE_STILL_CAPTURE 和 TEMPLATE_PREVIEW 的默认参数不同。拍照模板会优先保证画质,预览模板优先保证帧率。别混用,否则照片可能偏暗或偏亮。

8.2 捕获静态图片——capture() vs captureBurst()

提交拍照请求有两种方式:

  • capture():单次捕获,拍一张照片。日常拍照够用了。
  • captureBurst():连拍模式,适合抓拍运动场景。

我一般用capture(),简单直接。提交代码长这样:

// 在拍照按钮点击时触发
captureSession.capture(photoRequest, new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
        // 拍照完成回调——此时ImageReader那边已经在处理图片了
        Log.d("Camera", "拍照完成,图片数据已进入ImageReader");
    }

    @Override
    public void onCaptureFailed(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull CaptureFailure failure) {
        // 拍照失败——常见原因:快门速度太慢、对焦超时
        Log.e("Camera", "拍照失败: " + failure.getReason());
    }
}, null);

注意:capture() 是异步的。调用后立刻返回,图片数据通过ImageReader回调回来。不要在onCaptureCompleted里直接拿图片数据——那里只有元数据,没有像素数据。

8.3 ImageReader回调处理——核心中的核心

ImageReader是Camera2里专门用来接收图片数据的组件。它内部维护了一个队列,相机捕获到的每一帧都会放进这个队列。我们通过回调把数据取出来。

初始化ImageReader的代码:

// 创建ImageReader——宽高、格式、最大图片数
ImageReader imageReader = ImageReader.newInstance(
    width, height,
    ImageFormat.JPEG,  // 直接输出JPEG格式
    2                  // 队列最大深度,2就够了
);

// 设置监听器——图片数据到达时触发
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {
        // 这里是在Camera线程回调的,别做耗时操作
        Image image = reader.acquireLatestImage();
        if (image != null) {
            // 处理图片——保存到文件
            saveImageToFile(image);
            // 用完必须关闭,否则内存泄漏
            image.close();
        }
    }
}, backgroundHandler);

为什么会用 acquireLatestImage() 而不是 acquireNextImage()

嗯,这里有个坑。如果相机帧率很快,而你的保存逻辑比较慢,队列里会堆积多张图片。acquireNextImage() 会按顺序一张张取,容易导致OOM。acquireLatestImage() 则直接丢弃旧帧,只拿最新的那张。我建议你始终用 acquireLatestImage()

8.4 保存图片到相册——两种主流方式

图片数据拿到了,接下来就是存盘。Android有两种方式:

方式 适用场景 优点 缺点
MediaStore API Android 10+ 推荐 无需存储权限,系统自动管理 API较新,低版本需兼容
File API + 广播通知 所有版本通用 简单直接,控制力强 需要WRITE_EXTERNAL_STORAGE权限

我个人更推荐MediaStore方式,毕竟Android 10以后强制分区存储,File API越来越受限。下面给出MediaStore的示例:

private void saveImageToFile(Image image) {
    // 从Image中取出JPEG字节数据
    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);

    // 生成文件名——用时间戳避免重名
    String filename = "IMG_" + System.currentTimeMillis() + ".jpg";

    // 使用MediaStore保存到相册
    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.Images.Media.RELATIVE_PATH,
        Environment.DIRECTORY_PICTURES + "/Camera2Demo");

    Uri uri = getContentResolver().insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

    try (OutputStream out = getContentResolver().openOutputStream(uri)) {
        out.write(bytes);
        out.flush();
        Log.d("Camera", "图片已保存: " + uri.toString());
    } catch (IOException e) {
        Log.e("Camera", "保存失败", e);
    }
}

避坑指南:我曾经在保存图片时忘记关闭Image对象,结果相机预览卡死、拍照越来越慢。后来排查发现是ImageReader队列满了,新数据进不来。记住:acquireLatestImage() 拿到的Image,用完必须 close(),否则就是给自己挖坑。

8.5 完整调用链——把一切串起来

上面讲了各个零件,现在把它们组装起来。拍照按钮的完整逻辑:

  1. 用户点击拍照按钮
  2. 调用 captureSession.capture(photoRequest, callback, handler)
  3. 相机硬件捕获一帧数据,写入ImageReader的Surface
  4. ImageReader回调 onImageAvailable() 被触发
  5. 在回调中调用 acquireLatestImage() 拿到Image对象
  6. 从Image中提取JPEG字节数据
  7. 通过MediaStore写入相册
  8. 关闭Image对象,释放资源

你看,链路其实很清晰。但每个环节都有细节要注意。比如第5步和第8步,漏掉任何一个都会出问题。

核心要点总结

  • 拍照用TEMPLATE_STILL_CAPTURE,别用预览模板
  • capture()是异步的,图片数据走ImageReader回调
  • 始终用acquireLatestImage()避免OOM
  • Image用完必须close()
  • Android 10+推荐MediaStore保存,兼容性好

好了,拍照功能的核心逻辑就这些。你把这个流程跑通,Camera2拍照这块就算拿下了。下一章咱们聊聊更进阶的话题——不过那是后话了。


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