第 4 章讲了 Camera2 API 的五个核心类。这章把它们串起来——写一个能跑的相机 App。
不过不要以为"能跑"就够了。这章真正要教你的不是 App 代码本身,而是每行 App 代码在 HAL 层触发了什么。理解了这层映射关系,你在 HAL 层写代码时才知道"App 那边到底要什么"。
5.1 预览——最基础也最重要的循环
📊 对照这张图看本节:
预览的本质:App 发起一个持续重复的 CaptureRequest,HAL 收到后每帧处理,处理完的帧通过 BufferQueue 送到 Surface 显示。
5.1.1 时序分解
对照 05_preview_timing.puml,整个预览流程分 5 个阶段:
阶段 1:打开相机App: CameraManager.openCamera(cameraId, callback, handler)HAL: camera_module_t.open() → camera3_device_t.initialize()耗时: ~200ms(ChiContext 创建 + Static Metadata 构建)阶段 2:创建 SessionApp: CameraDevice.createCaptureSession(surfaces, callback)HAL: configure_streams() → UsecaseSelector 选择 Usecase → 创建 Pipeline → 分配 Buffer耗时: ~100ms阶段 3:开始预览App: CaptureSession.setRepeatingRequest(request, callback)HAL: process_capture_request() → Session → Pipeline → Node 链第一帧延迟: ~50ms(Sensor 曝光 + ISP 处理)阶段 4:每帧循环HAL 每处理完一帧 → process_capture_result() 回调→ BufferQueue 把数据送到 Surface → Surface 更新显示→ App 收到 onCaptureCompleted()循环周期: ~33ms@30fps 或 ~16ms@60fps阶段 5:停止预览App: CaptureSession.close() 或 CameraDevice.close()HAL: flush() → Pipeline StreamOff → 释放 Buffer
5.1.2 每一步的代码和 HAL 层对应
步骤 1:TextureView 就绪后打开相机
TextureView 的 SurfaceTexture 就绪后,才能创建 Surface 传给 HAL。
previewView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surface, int w, int h) {openCamera(); // Surface 准备好了,可以打开相机了}// ... 其他回调省略});
步骤 2:打开相机
private void openCamera() {cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {@Overridepublic void onOpened(CameraDevice device) {// 对应 HAL: camera_module_t.open() 完成cameraDevice = device;startPreview();}@Overridepublic void onDisconnected(CameraDevice device) {// HAL 层 Provider 进程崩溃时会触发这个回调device.close();}@Overridepublic void onError(CameraDevice device, int error) {// HAL 返回了错误(如 configure_streams 失败)device.close();}}, null);}
💡 openCamera 的异步特性
openCamera()不是同步的。你在onOpened()回调中才能拿到CameraDevice对象。在此之前,HAL 层在初始化 ChiContext、构建 Static Metadata、加载 overrides、初始化 CSL。这意味着:如果你在
onOpened()之前尝试做任何相机操作,都会失败。
步骤 3:创建 Session + 开始预览
这一步是 HAL 层最忙的时候——UsecaseSelector 决策、Pipeline 创建、Buffer 分配都在这里发生。
private void startPreview() {// 从 TextureView 获取 SurfaceTexture,设置缓冲区大小SurfaceTexture texture = previewView.getSurfaceTexture();texture.setDefaultBufferSize(1920, 1080);Surface surface = new Surface(texture);// 创建 PREVIEW 模板的 Request BuilderCaptureRequest.Builder builder =cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);builder.addTarget(surface); // 指定输出目标// createCaptureSession — HAL 层开始 configure_streams()cameraDevice.createCaptureSession(Arrays.asList(surface), // 所有输出 Surface 的列表new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(CameraCaptureSession session) {// Session 就绪,开始重复请求captureSession = session;try {session.setRepeatingRequest(builder.build(),new CameraCaptureSession.CaptureCallback() {@Overridepublic void onCaptureCompleted(CameraCaptureSession s,CaptureRequest req,TotalCaptureResult result) {// 每帧回调。可以在这里读取 AE/AF 状态Long exposureNs = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);Integer iso = result.get(CaptureResult.SENSOR_SENSITIVITY);}},null // handler = null 用主线程 Looper);} catch (CameraAccessException e) { }}@Overridepublic void onConfigureFailed(CameraCaptureSession session) {// 配置失败 — 通常是 Surface 分辨率不被支持// 或所有 Surface 总带宽超出 ISP 能力}},null);}
💡
setRepeatingRequest和capture的本质区别HAL 层并不区分"预览"和"拍照"——它只看到
process_capture_request。区别在于:
setRepeatingRequest:HAL 持续收到请求,Pipeline 保持在 STREAM_ON 状态,不停处理 capture:HAL 收到一次请求,处理完就不再处理下一个(直到下次 capture) CamX 内部通过 Request 的频率来决定是预览还是拍照。
5.1.3 每个 Surface 对应一个 HAL Stream
💡 关于 Buffer:这里提到的 Surface、Stream、Buffer 的详细机制在第 9 章有完整讲解(Buffer 轮转、句柄传递、Gralloc 分配器)。如果你现在对 "Buffer 怎么传" 感到模糊,先继续往下看,学完第 9 章再回来复习这一节会豁然开朗。
createCaptureSession 传入的 Surface 列表,在 HAL 层会被转换为 Stream:
App 层的 createCaptureSession([previewSurface])→ HAL 的 configure_streams()→ Stream 0: format=PRIVATE, size=1920x1080, usage=PREVIEW→ 分配 Gralloc Buffer × 4~8 个→ BufferQueue consumer = previewSurface
如果你传入两个 Surface:
App: createCaptureSession([previewSurface, imageReaderSurface])→ HAL:→ Stream 0: PRIVATE, 1920x1080, PREVIEW → previewSurface→ Stream 1: JPEG, 4032x3024, STILL_CAPTURE → imageReaderSurface
每个 Stream 都要分配独立的 Buffer 池。 如果你配置了太多 Stream,ISP 带宽不够,configure_streams 就会失败。
5.2 拍照——多了一条 Stream
拍照不是在预览之外另起一个 Session。而是在同一个 Session 中加了一条 JPEG Stream。
5.2.1 原理
拍照时的 Session 配置:
createCaptureSession 时传入:[previewSurface, jpegReaderSurface]HAL 层:Stream 0 (PREVIEW): PRIVATE, 1920x1080 → previewSurfaceStream 1 (JPEG): JPEG, 4032x3024 → jpegReaderSurface
关键是 HAL 内部的处理:预览时 Stream 1 没有数据。只有当你调用 capture() 时,HAL 才会把 JPEG 数据写入 Stream 1。
预览帧:IFE → BPS → IPE → Stream 0 (Display) ← 实时 Pipeline 持续工作Stream 1 (不输出) ← Offline Pipeline 空闲拍照帧:IFE → BPS → IPE → Stream 0 (Display) ← 预览不中断Stream 1 → JPEG Node → JPEG 编码 → Stream 1 Buffer↑这是第 13 章讲的 Offline Pipeline
5.2.2 代码
// 创建 JPEG 输出的 ImageReaderprivate void setupImageReader() {imageReader = ImageReader.newInstance(4032, 3024, ImageFormat.JPEG, 1); // maxImages=1imageReader.setOnImageAvailableListener(reader -> {Image image = reader.acquireLatestImage();if (image != null) {ByteBuffer buffer = image.getPlanes()[0].getBuffer();byte[] jpegData = new byte[buffer.remaining()];buffer.get(jpegData);saveToFile(jpegData);image.close(); // ← 不 close 会阻塞后续帧!}}, null);}// 拍照——发一次 capture 请求private void captureStill() {CaptureRequest.Builder captureBuilder =cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);// ⚠ 很多教程只 addTarget JPEG Surface,导致拍照时预览卡顿captureBuilder.addTarget(previewSurface); // 保持预览captureBuilder.addTarget(imageReader.getSurface()); // 同时出 JPEGcaptureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);captureSession.capture(captureBuilder.build(), null, null);}
💡 预览不卡的关键——同时 addTarget 两个 Surface
// ❌ 可能卡顿:只给 JPEG SurfacecaptureBuilder.addTarget(imageReader.getSurface()); // 预览断流一帧// ✅ 预览流畅:同时给两个 SurfacecaptureBuilder.addTarget(previewSurface); // 预览继续captureBuilder.addTarget(imageReader.getSurface()); // 同时出 JPEGHAL 层看到 Request 的 output targets 包含两个 Surface,就会同时给两条 Stream 输出数据。预览和 JPEG 并行处理,互不阻塞。
5.3 录像——绕过 CPU 的 Buffer 共享
录像的核心:ISP 输出的 YUV buffer 通过 DMA-BUF fd 直接传给 MediaCodec 编码器,省掉 CPU 拷贝。
5.3.1 数据流
Sensor → ISP → YUV├──→ SurfaceView(显示预览)└──→ MediaCodec Surface(编码 H.264 → .mp4)↑ISP DMA 写入 Gralloc buffer → 通过 dma-buf fd 传 handle → 编码器硬件直接读全程没有 CPU memcpy!
这就是为什么 COLOR_FormatSurface 很重要——它告诉编码器"通过 gralloc handle 直接访问 buffer,不用 CPU 拷贝数据"。
5.3.2 代码
private MediaCodec setupMediaCodec(int width, int height) {MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); // H.264MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);format.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 3);format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);// ↑ 这个参数让编码器通过 gralloc handle 直接访问 ISP 写入的 bufferformat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 每秒一个关键帧codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);// 获取编码器的输入 Surface——把它传给 CameraSurface encoderSurface = codec.getInputSurface();// createCaptureSession 时把这个 Surface 加进去Listsurfaces = Arrays.asList( new Surface(previewView.getSurfaceTexture()), // 预览encoderSurface // 录像);cameraDevice.createCaptureSession(surfaces, ...);codec.start();return codec;}
5.3.3 码率设置
码率 = width × height × fps × 压缩因子典型值(H.264):1080p@30fps: 1920 × 1080 × 30 × 0.08 ≈ 5 Mbps4K@30fps: 3840 × 2160 × 30 × 0.08 ≈ 20 Mbps4K@60fps: 3840 × 2160 × 60 × 0.08 ≈ 40 Mbps设太低 → 画面糊设太高 → 文件大,编码器跟不上帧率
5.4 Surface 选择策略
常见组合
场景 1: 仅预览createCaptureSession([previewSurface])场景 2: 预览 + 拍照createCaptureSession([previewSurface, jpegReaderSurface])场景 3: 预览 + 录像createCaptureSession([previewSurface, mediaCodecSurface])场景 4: 预览 + 拍照 + 录像(3 条 Stream)createCaptureSession([previewSurface, jpegReaderSurface, mediaCodecSurface])⚠ 三条 Stream 意味着 ISP 需要同时输出三路数据——确认 ISP 带宽够用
5.5 常见问题
Q1: createCaptureSession 配置了最大分辨率,但报错 onConfigureFailed
根因:所有 Surface 的总带宽超出 ISP 处理能力。同时配 48MP JPEG + 4K YUV + 1080p 预览可能撑不住。
解决:降低一个或多个 Surface 的分辨率。或者分时复用。
Q2: ImageReader 收不到回调
四个检查点:
ImageReader 的 Surface 在 createCaptureSession时传入了吗?capture 的 Request 中 addTarget了吗?newInstance时 maxImages 是几?设为 1 的话,前一张不 close 会卡住 image.close()调了吗?不调会阻塞后续帧——这是最常见的泄漏原因
Q3: 拍照后预览卡了一帧
原因:你的 capture Request 只 addTarget 了 JPEG Surface,没加预览 Surface。HAL 在这一帧不给预览 Stream 输出数据,预览就"停"了一帧。
修复:拍照的 Request 同时 addTarget(previewSurface) 和 addTarget(jpegReaderSurface)。
Q4: 预览画面方向不对
原因:Sensor 的安装方向(SENSOR_ORIENTATION)和屏幕方向不一致。
int orientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION);// 后摄通常 90°,前摄通常 270°if (facing == LENS_FACING_FRONT) {orientation = (360 - orientation) % 360; // 前摄需镜像}textureView.setTransform(transformMatrix);
5.6 本章总结
预览 = setRepeatingRequest(持续发请求,Pipeline STREAM_ON)拍照 = capture(单次请求,多一条 JPEG Stream,触发 Offline Pipeline)录像 = MediaCodec + COLOR_FormatSurface(DMA-BUF 共享,省 CPU 拷贝)HAL 层对应的操作:openCamera() → initialize() + configure_streams()setRepeatingRequest → 每隔 ~33ms 一次 process_capture_request()capture() → 一次 process_capture_request() with JPEG targetclose() → flush() + StreamOff + 释放 BufferSurface 的选择决定性能和灵活性:预览用 SurfaceView/TextureView读像素用 ImageReader(YUV)拍照用 ImageReader(JPEG)录像用 MediaCodec Surface
动手验证
用上面的代码片段拼一个能预览的 App。打开相机,确认画面正常。
在 logcat 中找到
configure_streams对应的日志,确认 Stream 配置和你传入的 Surface 列表一致。在拍照的 capture Request 中先不加 previewSurface 的 addTarget,然后拍照——观察预览是否卡了一帧。再加上 addTarget(previewSurface),对比效果。
查看 dumpsys 输出中当前 Session 的 Stream 列表:
adb shell dumpsys media.camera | grep -A 15 ”Stream”确认 Stream 的数量、格式、分辨率与你的代码一致。
常见误解
- "createCaptureSession 就是拍照"
— 不对。它只是配置数据流,不产生任何图像。开始预览或触发拍照需要后续的 setRepeatingRequest或capture。 - "多开几个 ImageReader 没关系"
— 每多一个 ImageReader 就多一条 Stream,多一份 Buffer 池。ISP 带宽是有限的。
评论 (0)