20. 连拍与高速连拍:配置连拍CaptureRequest、处理连拍回调、连拍性能优化
连拍这个功能,说白了就是让摄像头在短时间内连续输出多帧图像。你按下一次快门,系统帮你拍下一串照片。嗯,听起来简单,但真正落地的时候,坑还是挺多的。
我个人习惯把连拍分成两种场景:一种是普通连拍,每秒几张到十几张,主要用于抓拍日常瞬间;另一种是高速连拍,每秒几十帧甚至上百帧,常用于运动分析、慢动作合成或者科研场景。两者的实现思路其实差不多,但性能要求天差地别。
连拍的核心思路
Android Camera2 的连拍,本质上就是重复发送 CaptureRequest。你想想看,每次拍照就是一个 request,连拍就是一口气发一堆 request 出去。但这里有个关键点:你不能等上一张拍完再发下一张,那样太慢了。正确的做法是批量提交,让底层 pipeline 自己去排队处理。
核心要点:连拍的速度取决于三个因素——传感器的帧率、ISP 的处理能力、以及你 App 侧的回调处理速度。任何一个环节卡住,连拍就快不起来。
配置连拍 CaptureRequest
先看代码。我一般会这样构建连拍的 request:
// 构建连拍用的 CaptureRequest
val requestBuilder = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
// 设置对焦模式为连续自动对焦
requestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
// 设置曝光模式
requestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
// 如果你需要固定曝光,可以锁住 AE
// requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true)
// 设置输出 Surface
requestBuilder.addTarget(imageReader.surface)
// 批量提交
for (i in 0 until burstCount) {
// 注意:每个 request 可以微调参数
if (i % 2 == 0) {
requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 0)
} else {
requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, -1)
}
captureSession.capture(requestBuilder.build(), callback, handler)
}
这里有个细节:TEMPLATE_STILL_CAPTURE 和 TEMPLATE_PREVIEW 的区别。我建议连拍用 TEMPLATE_STILL_CAPTURE,因为它会优先保证画质。但如果你需要极高速连拍,可以试试 TEMPLATE_ZERO_SHUTTER_LAG,不过这个模板不是所有设备都支持。
小技巧:我曾经在某个项目里发现,连续提交相同的 request 时,部分手机会出现曝光抖动。后来我在每个 request 里微调了曝光补偿,让它们交替变化,反而更稳定。你可以试试看。
处理连拍回调
连拍的回调比单拍要复杂一些。你需要区分每一帧,并且要处理好回调的并发问题。看这个例子:
private val burstCallback = object : CameraCaptureSession.CaptureCallback() {
private var frameCount = 0
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
// 这一帧开始拍了
Log.d("Burst", "Frame $frameCount started at $timestamp")
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
// 这一帧拍完了,可以取数据了
val frameNumber = result.frameNumber
val timestamp = result.timestamp
// 注意:这里是在 camera 线程回调的
// 不要做耗时操作
frameCount++
// 通知 UI 线程更新进度
runOnUiThread {
updateProgress(frameCount, totalCount)
}
}
override fun onCaptureFailed(
session: CameraCaptureSession,
request: CaptureRequest,
failure: CaptureFailure
) {
super.onCaptureFailed(session, request, failure)
// 这一帧失败了
Log.e("Burst", "Frame failed: ${failure.reason}")
}
}
为什么要注意线程?因为 onCaptureCompleted 是在 Camera2 的内部线程池里回调的。如果你在这里直接操作 UI 或者做大量计算,会阻塞后续的回调,导致连拍速度下降。我建议回调里只做轻量级的状态记录,真正的图像处理放到工作线程去。
注意:连拍时 onCaptureCompleted 的调用频率非常高。如果你在回调里打印日志,记得控制频率,否则 logcat 会被刷爆。我曾经吃过这个亏,调试时手机直接卡死。
ImageReader 的配置
连拍的数据是通过 ImageReader 拿到的。它的配置直接影响连拍性能:
// 配置 ImageReader
val imageReader = ImageReader.newInstance(
width, height,
ImageFormat.JPEG, // 用 JPEG 格式,体积小
maxImages = 10 // 缓冲区大小,很重要
)
imageReader.setOnImageAvailableListener({ reader ->
// 这里要尽快消费掉 Image
val image = reader.acquireLatestImage()
image?.let {
// 处理图像数据
processImage(it)
// 一定要 close,否则缓冲区会满
it.close()
}
}, backgroundHandler)
maxImages 这个参数很关键。它决定了 ImageReader 内部能缓存多少帧。设得太小,底层会等你消费完才给新帧,连拍速度受限。设得太大,内存占用高,而且处理不及时的话,旧帧会被丢弃。
我个人习惯设成 10 左右。如果你的连拍速度特别高,可以适当增大,但不要超过 20。嗯,这里要提醒一下:一定要及时 close Image,否则缓冲区满了之后,新的帧就进不来了。
连拍性能优化
这部分是我踩坑最多的。直接上干货:
| 优化方向 | 具体做法 | 效果 |
|---|---|---|
| 减少 JPEG 压缩 | 使用 YUV_420_888 格式,自己编码 | 减少 ISP 负担,提升帧率 |
| 降低分辨率 | 使用较小的输出尺寸 | 显著提升连拍速度 |
| 批量提交 | 用 captureBurst 代替循环 capture |
减少 IPC 开销 |
| 异步处理 | 回调中只做轻量操作 | 避免阻塞 pipeline |
| 减少 AE/AF 计算 | 锁定曝光和对焦 | 帧率更稳定 |
其中 captureBurst 这个方法值得单独说一下:
// 批量提交,比循环 capture 效率高
val requests = List(burstCount) { index ->
val builder = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
builder.addTarget(imageReader.surface)
// 可以给每个 request 设置不同的参数
builder.set(CaptureRequest.JPEG_QUALITY, 95)
builder.build()
}
captureSession.captureBurst(requests, callback, handler)
为什么 captureBurst 更快?因为它把多个 request 打包成一次 IPC 调用,减少了内核态和用户态的切换次数。你想想看,循环调用 capture 每次都要跨进程通信,而 captureBurst 一次搞定,效率自然高。
性能数据参考:在我测试过的某款骁龙 8 Gen2 设备上,使用 captureBurst 配合 YUV 格式,1080p 分辨率下可以达到 30fps 的连拍速度。而用 JPEG 格式循环 capture,只能跑到 12fps 左右。差距很明显。
高速连拍的特殊处理
如果你需要 60fps 以上的高速连拍,普通的套路就不够用了。这时候需要一些特殊手段:
- 使用 PRIVATE 格式:不走 JPEG 编码,直接从 sensor 拿原始数据
- 降低输出分辨率:720p 甚至更小,减少数据量
- 关闭所有后处理:关掉美颜、HDR、降噪等
- 使用 SurfaceView 直接消费:绕过 ImageReader 的拷贝开销
我曾经在一个运动检测项目里,需要 120fps 的连拍。折腾了很久,最后发现瓶颈不在 Camera2 API,而在 App 侧的处理速度。后来我用了双缓冲机制:一个线程负责接收帧,另一个线程负责处理,才勉强跑通。
经验之谈:高速连拍时,建议把图像处理放到 Native 层去做。用 C++ 处理 YUV 数据,比 Java/Kotlin 快一个数量级。如果你用 NDK,还可以直接操作 Surface 的 buffer,省掉一次内存拷贝。
连拍流程总览
下面这张图是我自己总结的连拍流程,你可以对照着看:
从流程图可以看到,整个连拍链路其实不复杂。但每个环节都有优化空间。我个人觉得,最容易出问题的是回调处理和数据消费这两个环节。很多人只关注怎么发 request,忽略了消费端的处理能力,结果连拍速度上不去。
避坑指南
最后分享几个我踩过的坑:
- 不要在主线程处理图像:我曾经把 JPEG 解码放在主线程,结果连拍时 UI 直接卡死。后来改成工作线程,问题解决。
- 注意内存抖动:连拍时频繁创建对象会导致 GC 频繁触发,影响帧率。建议复用对象,或者用对象池。
- 不同设备差异很大:同一套代码,在 Pixel 上跑 30fps,在低端机上可能只有 5fps。建议根据设备能力动态调整连拍参数。
- 别忘了释放资源:连拍结束后,记得关闭 ImageReader 和 CaptureSession,否则下次连拍可能失败。
嗯,连拍这块的内容差不多就这些。核心就是:配置要合理,回调要轻量,消费要快速。把这三点做好,连拍功能基本就稳了。