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_CLOSED),CameraCaptureSession也有(如STATE_CONFIGURED,STATE_CLOSED)。更重要的是,每个CaptureRequest的执行会触发一系列回调,这些回调精确地描述了捕获的进度:
onCaptureStarted: 传感器开始曝光。onCaptureProgressed: 部分元数据可用(较少使用)。onCaptureCompleted: 单次捕获完成,所有结果数据(图像和元数据)都已送达目标Surface。onCaptureFailed: 捕获失败。
注意:
onCaptureCompleted回调只意味着相机硬件完成了它的工作,数据已经放入缓冲区队列。但这不意味着你的应用(例如ImageReader的监听器)已经处理完这份数据。如果处理太慢,缓冲区会被耗尽,导致预览卡顿或丢帧。
3. 从零构建一个Camera2应用的关键步骤
理论说再多,不如动手搭一个。下面我以一个支持预览和拍照的基础应用为例,拆解每一步的关键代码和意图。
3.1 环境准备与权限声明
首先,在AndroidManifest.xml中声明必要的权限和特性。Camera2需要运行时权限,别忘了在代码中动态申请。
<uses-permission android:name="android.permission.CAMERA" /> <!-- 如果拍照需要保存到外部存储 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- Android 10及以上使用作用域存储 --> <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />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 = mutableListOf<Surface>(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. 添加拍照的Surface(ImageReader的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的支持程度差异很大,特别是在手动控制和并行流方面,务必做好兼容性测试和降级方案。