Android Camera2 Pipeline模型深度解析与实战指南
1. 项目概述从Camera1到Camera2的范式迁移如果你在Android相机开发上还停留在Camera类那个“上古时代”每次调用takePicture都像是在开盲盒那今天这篇总结就是为你准备的。我花了相当长的时间才把Android Camera2这套复杂的管道Pipeline模型给啃下来。简单说Camera2 API是对传统Camera API的一次彻底重构它把相机从一台“傻瓜式”的自动相机变成了一台你可以手动控制光圈、快门、ISO的“专业单反”。它的核心就是这个Pipeline管道模型。理解了这个模型你才算真正拿到了Camera2的钥匙才能写出稳定、高效、功能强大的相机应用无论是做美颜、AR、还是高帧率录像都离不开对Pipeline的精准控制。2. Camera2 Pipeline核心架构深度拆解Camera2的设计哲学是“生产者-消费者”模型。整个相机系统被抽象为一个数据管道CameraDevice是生产者Surface是消费者而CaptureRequest则是控制每一次数据生产的指令单。2.1 核心组件角色与职责CameraDevice 这是相机的抽象代表相当于你拿到了相机的控制权柄。通过它你可以创建CaptureRequest并建立到具体输出目标Surface的数据管道。一个CameraDevice可以同时向多个Surface输出数据这是实现预览、拍照、录像并发的关键。Surface 这是数据的终点站。它不是一个简单的View而是一个可以被填充图像数据的缓冲区生产者接口。常见的Surface来源包括SurfaceView或TextureView的Surface用于预览。ImageReader的Surface用于获取可供分析的静态图片或YUV/RAW数据流。MediaCodec的输入Surface用于视频编码和录像。Allocation的Surface用于RenderScript处理。CaptureRequest 这是控制单次图像捕获的“配方”。它包含了两类关键信息目标Surface列表 指定本次捕获的数据要发送到哪些Surface。捕获参数 一个键值对集合用于控制本次捕获的所有特性例如CAPTURE_INTENT: 告诉相机本次捕获的意图预览、拍照、录像系统会据此优化参数。CONTROL_AE_MODE: 自动曝光模式。SENSOR_EXPOSURE_TIME: 手动曝光时间纳秒。LENS_FOCAL_DISTANCE: 对焦距离。JPEG_QUALITY: JPEG压缩质量。CameraCaptureSession 这是整个Pipeline的“会话管理器”和“交通枢纽”。它负责配置CameraDevice到一组Surface的固定数据流管道并处理CaptureRequest的提交与排队。一旦Session被创建其绑定的输出Surface集合就固定了但你可以通过不同的CaptureRequest向这些Surface发送数据。2.2 Pipeline数据流与状态机理解数据流关键在于区分配置和捕获。配置阶段 调用CameraDevice.createCaptureSession()传入一个Surface列表比如预览的SurfaceView和拍照的ImageReader。系统内部会建立一条从相机传感器到这多个Surface的并行数据通道。这个阶段开销较大。捕获阶段 Session创建成功后你可以创建CaptureRequest并调用session.setRepeatingRequest()用于预览或session.capture()用于单次拍照。每个CaptureRequest都像一列火车载着图像数据沿着已建立的管道驶向Request中指定的一个或多个Surface终点站。状态机是另一个容易让人困惑的点。CameraDevice有自己的状态如STATE_OPEN,STATE_CLOSEDCameraCaptureSession也有如STATE_CONFIGURED,STATE_CLOSED。更重要的是每个CaptureRequest的执行会触发一系列回调这些回调精确地描述了捕获的进度onCaptureStarted: 传感器开始曝光。onCaptureProgressed: 部分元数据可用较少使用。onCaptureCompleted: 单次捕获完成所有结果数据图像和元数据都已送达目标Surface。onCaptureFailed: 捕获失败。注意onCaptureCompleted回调只意味着相机硬件完成了它的工作数据已经放入缓冲区队列。但这不意味着你的应用例如ImageReader的监听器已经处理完这份数据。如果处理太慢缓冲区会被耗尽导致预览卡顿或丢帧。3. 从零构建一个Camera2应用的关键步骤理论说再多不如动手搭一个。下面我以一个支持预览和拍照的基础应用为例拆解每一步的关键代码和意图。3.1 环境准备与权限声明首先在AndroidManifest.xml中声明必要的权限和特性。Camera2需要运行时权限别忘了在代码中动态申请。uses-permission android:nameandroid.permission.CAMERA / !-- 如果拍照需要保存到外部存储 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 10及以上使用作用域存储 -- uses-feature android:nameandroid.hardware.camera android:requiredtrue / uses-feature android:nameandroid.hardware.camera.autofocus android:requiredfalse /3.2 相机选择与特性查询不是所有相机都支持所有特性。在打开相机前我们必须先查询。val cameraManager context.getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraIdList cameraManager.cameraIdList for (id in cameraIdList) { val characteristics cameraManager.getCameraCharacteristics(id) // 1. 检查镜头朝向 val lensFacing characteristics.get(CameraCharacteristics.LENS_FACING) if (lensFacing CameraCharacteristics.LENS_FACING_BACK) { // 这是后置摄像头 } // 2. 检查支持的硬件级别判断功能上限 val hardwareLevel characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) if (hardwareLevel CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL) { // 支持所有手动控制和RAW拍照 } // 3. 查询输出尺寸 val streamConfigMap characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) val previewSizes streamConfigMap?.getOutputSizes(SurfaceTexture::class.java) val jpegSizes streamConfigMap?.getOutputSizes(ImageFormat.JPEG) // 选择尺寸逻辑通常选择预览尺寸时要兼顾屏幕宽高比和可用分辨率 }选择相机ID和尺寸的策略很重要。对于预览我通常会选择与屏幕比例最接近、且不超过1920x1080的分辨率以平衡性能。对于拍照则选择最大可用分辨率。3.3 创建输出目标Surface我们需要为预览和拍照创建各自的Surface。// 1. 预览Surface (使用TextureView) class CameraPreviewView : TextureView(context), TextureView.SurfaceTextureListener { private lateinit var previewSurface: Surface override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { // SurfaceTexture就绪后用它创建Surface surfaceTexture.setDefaultBufferSize(previewSize.width, previewSize.height) previewSurface Surface(surfaceTexture) // 触发创建CameraCaptureSession createCaptureSession() } // ... 其他回调方法 } // 2. 拍照Surface (使用ImageReader) // 我们想获取最大尺寸的JPEG图片 val imageReader ImageReader.newInstance( captureSize.width, captureSize.height, ImageFormat.JPEG, 2 // 缓冲区数量至少2个 ).apply { setOnImageAvailableListener({ reader - // 在这里处理拍到的图片 val image reader.acquireLatestImage() // 从image中获取ByteBuffer保存为文件或处理 image.close() }, handler) } val captureSurface imageReader.surface3.4 创建并配置CameraCaptureSession这是搭建管道的核心一步。我们需要将上一步创建的所有Surface一次性配置给相机。private fun createCaptureSession() { // 准备所有输出Surface的列表 val outputSurfaces mutableListOfSurface(previewSurface, captureSurface) // 创建SessionStateCallback来监听Session创建状态 val sessionStateCallback object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { cameraCaptureSession session // Session配置成功可以开始发送CaptureRequest了 startPreview() } override fun onConfigureFailed(session: CameraCaptureSession) { // 配置失败通常是Surface无效或不兼容 showToast(相机会话配置失败) } } // 创建CaptureSession cameraDevice?.createCaptureSession(outputSurfaces, sessionStateCallback, backgroundHandler) }实操心得createCaptureSession是一个耗时操作务必在后台线程backgroundHandler中调用。传入的Surface列表决定了本次Session生命周期的固定输出集。之后的所有CaptureRequest其目标Surface都必须是这个列表的子集。3.5 构建CaptureRequest并开始预览Session创建好后我们需要构建一个用于预览的重复请求。private fun startPreview() { // 1. 创建用于预览的CaptureRequest.Builder使用TEMPLATE_PREVIEW模板 val previewRequestBuilder cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { // 2. 添加预览Surface作为输出目标 this?.addTarget(previewSurface) // 3. 设置自动对焦模式为连续对焦适合预览 this?.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) // 4. 设置自动曝光模式为连续适合预览 this?.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) // 5. 设置捕获意图为预览让系统优化功耗和画质 this?.set(CaptureRequest.CAPTURE_INTENT, CaptureRequest.CAPTURE_INTENT_PREVIEW) } previewRequestBuilder?.build()?.let { request - // 6. 设置重复请求开始预览流 cameraCaptureSession?.setRepeatingRequest(request, null, backgroundHandler) } }TEMPLATE_PREVIEW模板会自动填充一组适合实时预览的默认参数。setRepeatingRequest会不断循环发送这个请求形成预览画面。3.6 实现拍照功能拍照需要创建一个新的、独立的CaptureRequest。fun takePicture() { // 0. 确保Session和Device都可用 if (cameraCaptureSession null || cameraDevice null) return // 1. 先停止重复的预览请求避免干扰 cameraCaptureSession?.stopRepeating() // 2. 创建用于拍照的CaptureRequest.Builder使用TEMPLATE_STILL_CAPTURE模板 val captureRequestBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE).apply { // 3. 添加拍照的SurfaceImageReader的surface addTarget(captureSurface) // 4. 设置JPEG方向根据传感器方向与设备方向计算 val sensorOrientation characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) val rotation (windowManager.defaultDisplay.rotation * 90) // 设备旋转角度 val jpegOrientation (sensorOrientation rotation 360) % 360 set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation) // 5. 可以设置JPEG质量等参数 set(CaptureRequest.JPEG_QUALITY, 95.toByte()) // 6. 设置捕获意图为静态拍照 set(CaptureRequest.CAPTURE_INTENT, CaptureRequest.CAPTURE_INTENT_STILL_CAPTURE) // 注意对焦和曝光模式通常沿用预览的最后状态或在此处锁定。 // 对于自动模式可以先触发一次对焦和测光锁定后再拍照流程更复杂。 } // 7. 构建并提交单次捕获请求 val captureRequest captureRequestBuilder.build() cameraCaptureSession?.capture(captureRequest, object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { // 拍照完成元数据在result中 // 图片数据会在ImageReader的监听器中收到 // 重新开始预览 startPreview() } override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) { // 拍照失败处理 startPreview() // 失败后也需恢复预览 } }, backgroundHandler) }这里有一个关键细节拍照前先stopRepeating()。这是因为相机硬件处理队列是顺序的如果不停掉预览流拍照请求可能会在队列中等待很久导致快门延迟。拍照完成后务必记得重新调用startPreview()。4. 高级特性与性能优化实战掌握了基础流程我们来看看如何利用Pipeline模型实现高级功能并优化性能。4.1 并行流处理预览、拍照、录像三合一Camera2 Pipeline的强大之处在于支持向多个Surface同时输出不同格式、不同尺寸的数据流。这需要仔细规划Surface的配置。创建多个输出目标 一个用于预览的SurfaceView一个用于高分辨率JPEG拍照的ImageReader一个用于视频编码的MediaCodec输入Surface。一次性创建Session 在createCaptureSession时将这三个Surface全部传入。使用不同的CaptureRequest预览请求只添加预览Surface为目标使用TEMPLATE_PREVIEW。拍照请求只添加拍照ImageReader的Surface为目标使用TEMPLATE_STILL_CAPTURE。提交前先stopRepeating预览请求拍完后恢复。录像请求添加预览Surface和MediaCodec的Surface为目标使用TEMPLATE_RECORD。然后调用setRepeatingRequest开始持续录像流。此时预览和编码会同时收到数据。注意事项 同时开启的流越多对相机硬件的负载和内存带宽的压力越大。务必通过StreamConfigurationMap的isOutputSupportedFor()和getOutputStallDuration()等方法检查你选择的尺寸和格式组合是否被硬件支持以及拍照后预览是否会卡顿。4.2 手动控制与参数调优在LEGACY或LIMITED硬件级别上手动控制选项有限。但在FULL级别设备上你可以获得单反般的控制体验。手动曝光// 切换到手动模式 previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_OFF) // 设置手动曝光时间单位纳秒。需要从CameraCharacteristics中查询可用范围。 val exposureRange characteristics.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE) previewRequestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, desiredExposureTimeNs) // 设置手动感光度ISO val isoRange characteristics.get(CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE) previewRequestBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, desiredIso)手动对焦// 切换到手动对焦模式 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_OFF) // 设置对焦距离单位屈光度。0.0表示无限远。 previewRequestBuilder.set(CaptureRequest.LENS_FOCUS_DISTANCE, focusDistance)手动控制需要你构建一个实时更新的UI如滑杆并在每次数值变化时用新的参数重建CaptureRequest并更新给setRepeatingRequest。4.3 内存管理与缓冲区处理图像缓冲区是稀缺资源处理不当会导致应用崩溃或性能低下。及时关闭资源Image对象用完必须调用close()否则缓冲区无法被回收最终会导致ImageReader无法产生新图像预览卡死。缓冲区数量 创建ImageReader或MediaCodec时指定的缓冲区数量是关键。太少会导致丢帧太多会浪费内存。对于30fps预览2-3个缓冲区通常足够。对于高速连拍可能需要更多。后台线程 所有相机回调CameraCaptureSession.CaptureCallback,ImageReader.OnImageAvailableListener都应在后台线程执行避免阻塞UI。图像处理如保存、滤镜更是必须在后台进行。5. 常见问题排查与避坑指南在实际开发中我踩过不少坑这里总结几个最典型的。5.1 预览方向不对或拉伸变形问题描述 预览画面旋转了90度或者被拉伸没有填满TextureView。根本原因 相机传感器有固定的方向而设备可以旋转。预览Surface的显示方向没有正确设置。解决方案计算显示旋转 根据设备当前旋转方向和传感器方向计算TextureView需要的变换矩阵。private fun configureTransform(viewWidth: Int, viewHeight: Int) { val matrix Matrix() val viewRect RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) val bufferRect RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) // 注意宽高交换 val centerX viewRect.centerX() val centerY viewRect.centerY() bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) // 计算缩放和旋转 matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) val rotation (windowManager.defaultDisplay.rotation * 90) matrix.postRotate(rotation.toFloat(), centerX, centerY) textureView.setTransform(matrix) }在onSurfaceTextureAvailable和onSurfaceTextureSizeChanged中调用此方法确保尺寸变化时能及时调整。5.2 拍照后预览卡顿或黑屏几秒问题描述 按下拍照按钮后预览画面停止等好几秒才恢复。根本原因 这就是前面提到的“Stall停滞时间”。高分辨率拍照尤其是JPEG编码和处理会占用大量CPU/GPU资源阻塞了预览缓冲区的生产和消费。排查与优化检查ImageReader的缓冲区数量 确保至少为2。如果只设1拍照时取图处理会占用唯一的缓冲区预览流因无缓冲区可用而卡住。优化图片处理逻辑 在ImageReader的回调中acquireLatestImage()和image.close()之间的处理要尽可能快。如果需要保存大图或复杂处理应该将ByteArray拷贝出来放到另一个线程池中去处理立即关闭Image。使用TEMPLATE_ZERO_SHUTTER_LAG如果支持 这个模板会让相机在后台持续缓冲图像拍照时直接从缓冲区取图减少延迟。降低拍照分辨率 如果不需要全分辨率选择一个更小的尺寸。5.3 打开相机失败或创建Session失败问题描述CameraDevice.StateCallback.onError或CameraCaptureSession.StateCallback.onConfigureFailed被调用。排查步骤检查权限 确保CAMERA权限已动态申请并被授予。检查Surface有效性 传递给createCaptureSession的Surface必须都是有效的、未关闭的。确保TextureView的SurfaceTexture已经可用onSurfaceTextureAvailable被调用并且ImageReader或MediaCodec没有提前被关闭。检查尺寸兼容性 通过StreamConfigurationMap的getOutputSizes()和isOutputSupportedFor()确认你选择的尺寸和格式组合被相机硬件支持。常见的错误是给SurfaceView设置了一个不被支持的巨大分辨率。检查生命周期 在Activity的onPause中必须正确关闭相机close()CameraCaptureSession和CameraDevice在onResume中重新打开。否则可能因为资源未释放导致再次打开失败。查看Logcat Camera2框架会输出详细的错误日志如CREATE_SESSION_FAILED通常会附带具体原因。5.4 对焦、测光等3A算法控制不理想Camera2提供了区域对焦和测光的功能但实现起来比旧API复杂。// 创建一个测光区域权重区域 val meterRect RectF(left, top, right, bottom).let { rect - // 需要将屏幕坐标转换为传感器坐标-1000到1000 val meteringArea MeteringRectangle( (rect.left * 2000 / viewWidth - 1000).toInt(), (rect.top * 2000 / viewHeight - 1000).toInt(), (rect.width() * 2000 / viewWidth).toInt(), (rect.height() * 2000 / viewHeight).toInt(), MeteringRectangle.METERING_WEIGHT_MAX ) arrayOf(meteringArea) } // 创建对焦区域通常与测光区域相同 val focusRect meterRect // 在CaptureRequest.Builder中设置 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, focusRect) previewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, meterRect) // 然后需要触发一次对焦锁定 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START) cameraCaptureSession?.capture(previewRequestBuilder.build(), captureCallback, handler)关键点 设置区域后必须发送一个带有CONTROL_AF_TRIGGER_START的请求来触发对焦过程。对焦完成后可以通过CaptureResult.CONTROL_AF_STATE来监听对焦状态。我个人在实际项目中处理Camera2最深的体会就是一定要尊重它的异步和状态机模型。所有操作几乎都是异步的回调嵌套回调。务必用Handler管理好线程并在每个状态回调onOpened,onConfigured,onCaptureCompleted里做好状态检查和错误处理。把相机操作封装成一个状态清晰的管理类是避免代码陷入“回调地狱”的最佳实践。另外不同厂商、不同型号的设备对Camera2的支持程度差异很大特别是在手动控制和并行流方面务必做好兼容性测试和降级方案。