CaptureRequest详解:从构建到队列的完整旅程

大家好,我是你们的老朋友。今天我们来聊聊CaptureRequest——这个在Android相机系统中看似简单、实则暗藏玄机的核心对象。

说实话,我刚接触Camera2时,觉得CaptureRequest不就是个配置参数嘛,有什么好研究的?直到有一次,我在项目中遇到了预览画面卡顿、拍照延迟的问题,排查了整整两天,最后发现是Request构建方式不对。嗯,从那以后,我再也不敢小看它了。

1. Request构建:一切从这里开始

CaptureRequest的构建,说白了就是通过Builder模式一步步搭积木。我个人习惯把构建过程分为三步:

  1. 获取Builder对象:从CameraDevice创建
  2. 配置参数:设置各种控制项
  3. 添加Target:指定输出目标

来看一段最基础的代码:

// 从CameraDevice创建Builder
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW);

// 配置参数
builder.set(CaptureRequest.CONTROL_AE_MODE, 
    CaptureRequest.CONTROL_AE_MODE_ON);
builder.set(CaptureRequest.CONTROL_AF_MODE, 
    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

// 添加Target
builder.addTarget(surfaceView.getSurface());

// 构建Request
CaptureRequest request = builder.build();

这里有个坑,我曾经踩过:build()方法调用后,Builder对象就不能再修改了。如果你需要不同的配置,必须重新创建Builder。所以,我建议把构建逻辑封装成一个方法,方便复用。

小技巧:TEMPLATE_PREVIEW、TEMPLATE_STILL_CAPTURE这些模板,其实只是预设了常用参数。你完全可以在它们基础上做调整,不必从头开始。

2. Target配置:输出目标的选择

Target,就是你的照片或视频最终要去哪里。可以是SurfaceView、ImageReader、MediaRecorder等等。

我记得有一次,同事问我:“为什么我加了两个Target,预览和拍照却不同步?”

问题出在——不同Target的帧率可能不一致。比如预览Surface是30fps,但拍照的ImageReader可能只捕获关键帧。这时候,你需要用addTargetremoveTarget动态管理。

常见的Target类型:

Target类型 用途 注意事项
SurfaceView 预览显示 需要与Camera的尺寸匹配
ImageReader 拍照/帧处理 注意设置合适的ImageFormat
MediaRecorder 视频录制 需要配置音频和视频轨道
SurfaceTexture OpenGL处理 常用于滤镜或特效
注意:不要同时添加太多Target,否则会加重ISP负担。我曾经在一个项目里加了3个Target,结果帧率直接掉到15fps。后来只保留必要的Target,问题就解决了。

3. 参数设置:控制相机的每一个细节

CaptureRequest的参数设置,是控制相机行为的核心。说白了,就是告诉相机:“我要这样拍,不要那样拍。”

常用的参数分组:

  • 曝光控制:AE模式、曝光补偿、ISO、快门速度
  • 对焦控制:AF模式、对焦区域、对焦距离
  • 白平衡:AWB模式、色温
  • 图像处理:锐度、对比度、饱和度
  • 输出控制:输出尺寸、格式、旋转角度

举个例子,如果你想实现“点击屏幕对焦”的功能:

// 设置对焦区域
MeteringRectangle focusRect = new MeteringRectangle(
    x - 50, y - 50, 100, 100, 
    MeteringRectangle.METERING_WEIGHT_MAX);

builder.set(CaptureRequest.CONTROL_AF_REGIONS, 
    new MeteringRectangle[]{focusRect});
builder.set(CaptureRequest.CONTROL_AF_MODE, 
    CaptureRequest.CONTROL_AF_MODE_AUTO);
builder.set(CaptureRequest.CONTROL_AE_REGIONS, 
    new MeteringRectangle[]{focusRect});

// 触发对焦
builder.set(CaptureRequest.CONTROL_AF_TRIGGER, 
    CaptureRequest.CONTROL_AF_TRIGGER_START);

你想想看,为什么对焦和测光区域要一起设置?因为大部分场景下,用户点击的位置既是焦点也是测光点。当然,你也可以分开设置,实现“对焦在A点,测光在B点”的效果。

4. Request队列管理:有序执行的关键

CaptureRequest不是发出去就完事了。它会进入一个队列,由相机HAL层按顺序处理。这个队列的管理,直接影响到拍照的响应速度。

我画了一张图,帮你理解Request的流转过程:

CaptureRequest 队列流转图 应用层 createCaptureRequest() Request 队列 排队等待处理 Camera HAL 逐帧处理 CaptureResult 回调 onCaptureCompleted() 应用回调处理 更新UI/保存数据 重复请求循环 应用层 队列 HAL层 回调

从图中可以看到,Request从应用层出发,进入队列,然后被HAL层逐帧处理。处理完成后,通过回调返回结果。

这里有个关键点:队列是先进先出的。如果你先发了一个拍照Request,又发了一个预览Request,拍照Request会先被处理。但如果你连续发了多个拍照Request,它们会排队等待,可能导致延迟。

核心原则:尽量保持队列简洁。不必要的Request不要入队,否则会影响响应速度。

5. 重复请求与单次请求:两种模式的抉择

这是CaptureRequest最核心的概念之一。说白了:

  • 重复请求:一直发,直到你叫停。适合预览、视频录制。
  • 单次请求:只发一次,拍完就完。适合拍照、对焦触发。

来看代码对比:

// 重复请求 - 预览
captureSession.setRepeatingRequest(previewRequest, 
    callback, handler);

// 单次请求 - 拍照
captureSession.capture(stillCaptureRequest, 
    callback, handler);

你可能会问:“为什么不用重复请求来拍照?”

嗯,这个问题我当年也想过。原因在于:重复请求会持续占用ISP资源。如果你用重复请求来拍照,意味着相机一直在全功率运行,功耗和发热都会增加。而单次请求只在需要时触发,更加高效。

我曾经在一个项目中,为了简化代码,用重复请求代替单次请求来拍照。结果手机发烫严重,用户反馈很差。后来改成单次请求,问题就解决了。

还有一个常见场景:先对焦,再拍照。流程是这样的:

  1. 发送一个单次请求,触发AF
  2. 等待AF完成回调
  3. 发送拍照请求
  4. 恢复预览的重复请求

这个流程看似简单,但实现起来有不少细节。我建议你把对焦和拍照的逻辑封装成状态机,避免回调嵌套过深。

经验之谈:重复请求的帧率可以通过CONTROL_AE_TARGET_FPS_RANGE来控制。比如设置成[15, 30],相机就会在15-30fps之间动态调整,平衡画质和功耗。

6. 避坑指南:我踩过的那些坑

最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路:

  • Builder复用问题:build()之后不能修改,必须重新创建。我建议用工厂模式管理Builder。
  • Target尺寸不匹配:Surface的尺寸必须和Camera支持的输出尺寸一致,否则会报错。记得用StreamConfigurationMap.getOutputSizes()查询。
  • 队列溢出:如果连续发送大量Request,队列可能溢出。HAL层会丢弃多余的Request。所以,控制好发送频率很重要。
  • 回调线程:CaptureCallback默认在Camera的线程中执行,不要在里面做耗时操作。如果需要更新UI,记得切换到主线程。

好了,关于CaptureRequest的内容就讲到这里。希望这些经验对你有帮助。记住,理解Request的本质,是掌握Camera2 API的关键一步。


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