背景痛点:85 ms 红线是怎么来的?
做相机应用最怕什么?不是对焦失败,不是预览花屏,而是“咔”一下卡顿。把系统日志拉到最底下,常常能看到一行不起眼的小字:
CameraLatencyHistogram(1171): processCaptureRequest latency histogram (85)这行日志的意思是:过去一段时间里,processCaptureRequest的耗时分布里,85 ms出现的次数最多。
85 ms 听起来不多,但换算成帧率就是 11.7 FPS——远低于人眼可感知的 20 FPS 红线,用户会明显觉得“快门延迟”+“取景卡顿”。
对新手来说,第一反应往往是“是不是手机太老?”其实多数情况下,应用层对 Pipeline 的不当配置才是罪魁祸首。
理解这条直方图,就等于抓住了相机性能优化的“脉搏”。
技术对比:SurfaceView vs TextureView vs CameraX
先快速扫一眼三种常见方案在“延迟”维度的差异:
| 方案 | 渲染路径 | 延迟来源 | 官方优化程度 | 适合场景 |
|---|---|---|---|---|
| SurfaceView | 直接叠加到系统层,不走 UI 线程 | 最低,几乎零拷贝 | 低,需要自己做帧率对齐 | 全屏预览、AR 识别 |
| TextureView | 把图像当 GL 纹理,需要 GPU 一次拷贝 | 多一次 GPU→CPU→GPU | 中,有 BufferQueue 缓冲 | 需要旋转、缩放动画 |
| CameraX | 内部封装了 Surface/Texture,自动选路 | 依赖内部实现,默认 30 FPS | 高,自带线程池与缓存 | 快速上线、生命周期托管 |
结论:
想压延迟,优先 SurfaceView;想要“生命周期+易用”,CameraX 1.3 以上也能做到 60 FPS,但得手动把setSessionFeature里的REQUEST_AVAILABLE_CAPABILITIES_HIGH_SPEED_VIDEO打开,否则默认 Pipeline 深度就是 3 帧,85 ms 轻松出现。
实现方案:Camera2 低延迟采集最小可跑通代码
下面给一份 Kotlin 最小可运行片段,目标:单路 720p/60 FPS,延迟 < 40 ms。
重点看注释,理解Buffer 管理与帧率控制即可。
class FastCamera(private val activity: Activity, private val surface: Surface) : CameraDevice.StateCallback() { private lateinit var camera: CameraDevice private lateinit var session: CameraCaptureSession private val handler = HandlerThread("Cam2Thread").apply { start() }.looper.let(::Handler) // 1. 打开相机 fun open(cameraId: String) { val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager manager.openCamera(cameraId, this, Handler(handler)) } override fun onOpened(device: CameraDevice) { camera = device createSession() } // 2. 创建会话,关键:TEMPLATE_RECORD 降低3A延迟 private fun createSession() { val list = listOf(surface) camera.createCaptureSession(list, object : CameraCaptureSession.StateCallback() { override fun onConfigured(s: CameraCaptureSession) { session = s val req = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { addTarget(surface) set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(60, 60)) set(CaptureRequest.CONTROL_AE_LOCK, true) // 锁曝光,减帧间抖动 set(CaptureRequest.LENS_FOCUS_DISTANCE, 0f) // 固定焦距,省掉对焦马达 }.build() // 3. 设置重复请求,注意:null 回调减少 Binder 往返 session.setRepeatingRequest(req, null, null) } override fun onConfigureFailed(s: CameraCaptureSession) = Unit }, Handler(handler)) oganizeBufferQueue() } // 4. BufferQueue 深度控制:把 Surface 的 maxBufferCount 降到 2 private fun organizeBufferQueue() { val surfaceClass = Class.forName("android.view.Surface") val setMaxBufferCount = surfaceClass.getDeclaredMethod("setMaxBufferCount", Int::class.java) setMaxBufferCount.isAccessible = true setMaxBufferCount.invoke(surface, 2) // 双缓冲,降低 Pipeline Stall 概率 } fun close() { session.close() camera.close() handler.looper.quit() } }要点拆解:
- TEMPLATE_RECORD:官方注释就写了“optimized for low latency”,比 PREVIEW 模板少一次 3A 收敛。
- AE_LOCK + 固定焦距:把每帧节省的 3~5 ms 累加起来,60 FPS 下就是 300 ms/秒级收益。
- maxBufferCount=2:Pipeline Stall 最常见原因就是“Buffer 堆积”,把队列深度砍到 2,GPU 消费不过来时立即抛帧,而不是堆到 85 ms 才爆掉。
- null Callback:setRepeatingRequest 的第二个参数传 null,避免每次帧完成都走 Binder 回主线程,实测可削 2~3 ms。
性能优化:Trace 定位 + 线程模型
1. 用 Trace 找到真凶
在怀疑卡顿的地方插两行:
import android.os.Trace Trace.beginSection("processCaptureRequest") // 真正的 capture 逻辑 Trace.endSection()然后执行:
adb shell atrace -c -o /sdcard/trace.html cam2把 trace.html 拖进 Perfetto 一看,哪段超过 16.6 ms 一目了然。
经验:多数情况下,>50% 的 85 ms 都卡在 JPEG/YUV→RGB 转换,如果预览只是用来显示,不要傻乎乎把 ImageReader 的格式设成 JPEG,直接YUV_420_888丢给 Surface,GPU 会自己转颜色。
2. 线程模型怎么搭
- HandlerThread:轻量、顺序执行,适合串行下发请求。
- Executor + 队列:并发能力强,但容易把 CaptureSession 搞成多线程乱入,触发IllegalStateException: Session has been closed。
推荐做法:
CaptureSession 的所有操作(createCaptureRequest、setRepeatingRequest、close)都放到单线程 Handler;
ImageReader 的回调用Executor无所谓,因为它只读 Buffer,不会改会话状态。
这样既能并行做 AI 识别,又不会把 Session 锁搞炸。
避坑指南:三个原则 + 厂商差异
- 原则一:永远保持“生产≤消费”
把 BufferQueue 深度、请求频率、处理线程池长度对齐,宁可丢帧,也不要让 HAL 堆积。 - 原则二:禁用隐藏“后处理”
很多 OEM 把降噪/锐化默认打开,processCaptureRequest会等算法返回才结束。用CaptureRequest.NOISE_REDUCTION_MODE_OFF和EDGE_MODE_OFF关掉,延迟立降 10~15 ms。 - 原则三:版本降级测试
同一台手机,Android 11→12 的 HAL 接口实现可能大变。发版前,用 Google 提供的 CTS Camera Performance Test跑一遍,防止厂商“负优化”。
厂商差异速查表:
| 厂商 | 特殊限制 | 快速绕过 |
|---|---|---|
| 某族 | 强制 EIS,开 60 FPS 会降 720p | 用 CamcorderProfile 先 query,再设 30 FPS |
| 某米 | 开 AE_LOCK 后帧率锁 30 | 用 CONTROL_AE_MODE_ON_ALWAYS_FLASH 曲线救国 |
| 某为 | 后台线程优先级低,会丢帧 | 把线程提至 THREAD_PRIORITY_URGENT_DISPLAY |
延伸思考:把优化搬到视频录制
预览延迟压下来后,视频录制就是天然延伸。
Camera2 的REPEATING_BURST模式可以复用同一套 Pipeline,只需额外打开MediaRecorder的surface,并给createCaptureSession多加一个Surface即可。
注意三点:
- 录制时把CONTROL_AE_TARGET_FPS_RANGE设成 (30,30) 或 (60,60),与MediaRecorder.setVideoFrameRate保持一致,否则 HAL 会重新协商,首帧延迟又回到 85 ms 级别。
- 音频轨道会引入AudioTrack latency,可以用getTimestamp与视频首帧对齐,防止 A/V 不同步。
- 高码率 4K/60 场景下,GPU 后处理 + 编码容易把 Buffer 吃满,记得把ImageReader的maxImages调到 6 以上,避免onImageAvailable阻塞。
小结:从一条日志到完整闭环
- 看到CameraLatencyHistogram(1171)先别慌,用 Trace 找到processCaptureRequest真耗时。
- 优先SurfaceView + TEMPLATE_RECORD + AE_LOCK,把 85 ms 砍到 40 ms 以内。
- 线程模型保持“单线程序列化会话 + 并行化算法”,既不炸 Session,也不丢性能。
- 上线前跑CTS 性能用例,把厂商差异提前扫平。
如果你想亲手搭一个“能说话、能听话”的实时交互 Demo,又正好缺一套低延迟采集框架,不妨把上面代码直接搬过去用;再往后,给每一帧喂给 AI 做识别、做对话,就能做出一个“边说边拍”的魔法应用。
我就是在 从0打造个人豆包实时通话AI 实验里,把这套 Camera 模板直接套进去,语音+视觉双通道延迟都压在 200 ms 以内,小白也能顺利跑通,推荐你也试试。