第 4 章讲了 Camera2 API 的五个核心类。这章把它们串起来——写一个能跑的相机 App。
不过不要以为"能跑"就够了。这章真正要教你的不是 App 代码本身,而是每行 App 代码在 HAL 层触发了什么。理解了这层映射关系,你在 HAL 层写代码时才知道"App 那边到底要什么"。
📊 对照这张图看本节:
/api/course-files/camera-fullstack/documents/diagrams/05_preview_timing.png
预览的本质:App 发起一个持续重复的 CaptureRequest,HAL 收到后每帧处理,处理完的帧通过 BufferQueue 送到 Surface 显示。
对照 05_preview_timing.png,整个预览流程分 5 个阶段:
阶段 1:打开相机
App: CameraManager.openCamera(cameraId, callback, handler)
HAL: camera_module_t.open() → camera3_device_t.initialize()
耗时: ~200ms(ChiContext 创建 + Static Metadata 构建)
阶段 2:创建 Session
App: CameraDevice.createCaptureSession(surfaces, callback)
HAL: configure_streams() → UsecaseSelector 选择 Usecase → 创建 Pipeline → 分配 Buffer
耗时: ~100ms
阶段 3:开始预览
App: CameraCaptureSession.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: CameraCaptureSession.close() 或 CameraDevice.close()
HAL: flush() → Pipeline StreamOff → 释放 Buffer
步骤 1:TextureView 就绪后打开相机
TextureView 的 SurfaceTexture 就绪后,才能创建 Surface 传给 HAL。
previewView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int w, int h) {
openCamera(); // Surface 准备好了,可以打开相机了
}
// ... 其他回调省略
});
步骤 2:打开相机
private void openCamera() {
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice device) {
// 对应 HAL: camera_module_t.open() 完成
cameraDevice = device;
startPreview();
}
@Override
public void onDisconnected(CameraDevice device) {
// HAL 层 Provider 进程崩溃时会触发这个回调
device.close();
}
@Override
public 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 Builder
CaptureRequest.Builder builder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(surface); // 指定输出目标
// createCaptureSession — HAL 层开始 configure_streams()
cameraDevice.createCaptureSession(
Arrays.asList(surface), // 所有输出 Surface 的列表
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
// Session 就绪,开始重复请求
captureSession = session;
try {
session.setRepeatingRequest(
builder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public 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) { }
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
// 配置失败 — 通常是 Surface 分辨率不被支持
// 或所有 Surface 总带宽超出 ISP 能力
}
},
null
);
}
💡
setRepeatingRequest和capture的本质区别HAL 层并不区分"预览"和"拍照"——它只看到
process_capture_request。区别在于:
setRepeatingRequest:HAL 持续收到请求,Pipeline 保持在 STREAM_ON 状态,不停处理capture:HAL 收到一次请求,处理完就不再处理下一个(直到下次 capture)CamX 内部通过 Request 的频率来决定是预览还是拍照。
💡 关于 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 就会失败。
拍照不是在预览之外另起一个 Session。而是在同一个 Session 中加了一条 JPEG Stream。
拍照时的 Session 配置:
createCaptureSession 时传入:
[previewSurface, jpegReaderSurface]
HAL 层:
Stream 0 (PREVIEW): PRIVATE, 1920x1080 → previewSurface
Stream 1 (JPEG): JPEG, 4032x3024 → jpegReaderSurface
关键是 HAL 内部的处理:预览时 Stream 1 没有数据。只有当你调用 capture() 时,HAL 才会把 JPEG 数据写入 Stream 1。
预览帧:
IFE → IPE → Stream 0 (Display) ← 实时 Pipeline 持续工作
Stream 1 (不输出) ← Offline Pipeline 空闲
拍照帧:
IFE → IPE → Stream 0 (Display) ← 预览不中断
Stream 1 → BPS → IPE → JPEG Node → JPEG 编码 → Stream 1 Buffer
↑
这是第 13 章讲的 Offline Pipeline(BPS 在此做 demosaic)
// 创建 JPEG 输出的 ImageReader
private void setupImageReader() {
imageReader = ImageReader.newInstance(
4032, 3024, ImageFormat.JPEG, 1); // maxImages=1
imageReader.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()); // 同时出 JPEG
captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);
captureSession.capture(captureBuilder.build(), null, null);
}
💡 预览不卡的关键——同时 addTarget 两个 Surface
// ❌ 可能卡顿:只给 JPEG Surface captureBuilder.addTarget(imageReader.getSurface()); // 预览断流一帧 // ✅ 预览流畅:同时给两个 Surface captureBuilder.addTarget(previewSurface); // 预览继续 captureBuilder.addTarget(imageReader.getSurface()); // 同时出 JPEGHAL 层看到 Request 的 output targets 包含两个 Surface,就会同时给两条 Stream 输出数据。预览和 JPEG 并行处理,互不阻塞。
录像的核心:ISP 输出的 YUV buffer 通过 DMA-BUF fd 直接传给 MediaCodec 编码器,省掉 CPU 拷贝。
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 拷贝数据"。
private MediaCodec setupMediaCodec(int width, int height) {
MediaCodec codec = MediaCodec.createEncoderByType(
MediaFormat.MIMETYPE_VIDEO_AVC); // H.264
MediaFormat 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 写入的 buffer
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 每秒一个关键帧
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 获取编码器的输入 Surface——把它传给 Camera
Surface encoderSurface = codec.getInputSurface();
// createCaptureSession 时把这个 Surface 加进去
List<Surface> surfaces = Arrays.asList(
new Surface(previewView.getSurfaceTexture()), // 预览
encoderSurface // 录像
);
cameraDevice.createCaptureSession(surfaces, ...);
codec.start();
return codec;
}
码率 = width × height × fps × 压缩因子
典型值(H.264):
1080p@30fps: 1920 × 1080 × 30 × 0.08 ≈ 5 Mbps
4K@30fps: 3840 × 2160 × 30 × 0.08 ≈ 20 Mbps
4K@60fps: 3840 × 2160 × 60 × 0.08 ≈ 40 Mbps
设太低 → 画面糊
设太高 → 文件大,编码器跟不上帧率
| Surface 类型 | 能否 CPU 读取 | 性能 | 适用场景 |
|---|---|---|---|
| SurfaceView | 不能 | 最高(硬件 overlay) | 纯显示预览 |
| TextureView | 不能 | 高(比 SurfaceView 多一次合成) | 需要变换预览(缩放/旋转) |
| ImageReader(YUV) | 能 | 中(CPU 可访问,有一份拷贝) | 需要读像素做处理 |
| ImageReader(JPEG) | 能 | 中 | 拍照输出 |
| MediaCodec Surface | 不能 | 最高(DMA-BUF 共享,无 CPU 拷贝) | 录像 |
场景 1: 仅预览
createCaptureSession([previewSurface])
场景 2: 预览 + 拍照
createCaptureSession([previewSurface, jpegReaderSurface])
场景 3: 预览 + 录像
createCaptureSession([previewSurface, mediaCodecSurface])
场景 4: 预览 + 拍照 + 录像(3 条 Stream)
createCaptureSession([previewSurface, jpegReaderSurface, mediaCodecSurface])
⚠ 三条 Stream 意味着 ISP 需要同时输出三路数据——确认 ISP 带宽够用
根因:所有 Surface 的总带宽超出 ISP 处理能力。同时配 48MP JPEG + 4K YUV + 1080p 预览可能撑不住。
解决:降低一个或多个 Surface 的分辨率。或者分时复用。
四个检查点:
createCaptureSession 时传入了吗?addTarget 了吗?newInstance 时 maxImages 是几?设为 1 的话,前一张不 close 会卡住image.close() 调了吗?不调会阻塞后续帧——这是最常见的泄漏原因原因:你的 capture Request 只 addTarget 了 JPEG Surface,没加预览 Surface。HAL 在这一帧不给预览 Stream 输出数据,预览就"停"了一帧。
修复:拍照的 Request 同时 addTarget(previewSurface) 和 addTarget(jpegReaderSurface)。
原因:Sensor 的安装方向(SENSOR_ORIENTATION)和屏幕方向不一致。
int orientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION);
// 后摄通常 90°,前摄通常 270°
if (facing == LENS_FACING_FRONT) {
orientation = (360 - orientation) % 360; // 前摄需镜像
}
textureView.setTransform(transformMatrix);
预览 = 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 target
close() → flush() + StreamOff + 释放 Buffer
Surface 的选择决定性能和灵活性:
预览用 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 的数量、格式、分辨率与你的代码一致。
setRepeatingRequest 或 capture。