Android Camera2 API架构解析:从并发控制到实战避坑指南
1. 从Camera1到Camera2为什么我们需要一套全新的API如果你在Android开发领域特别是多媒体或图像处理方向摸爬滚打超过五年那你一定对那个古老的android.hardware.Camera类记忆犹新。在早期的Android项目中我们用它来拍照、录像代码写起来似乎也挺直接获取实例、设置参数、设置预览、拍照。但真正深入做过相机应用尤其是需要自定义图像处理、多摄像头切换或者追求极致性能的开发者都曾在这个老旧的API上栽过跟头。它的设计充满了“历史包袱”比如参数设置是一个巨大的Camera.Parameters对象修改任何参数都可能触发一次完整的相机重启预览和拍照的回调是同步的很容易阻塞主线程。更头疼的是它对多摄像头的支持几乎为零你很难精确地控制图像数据的流向和处理过程。这就是Google推出Camera2 APIandroid.hardware.camera2的根本原因。它不是一次简单的版本升级而是一次彻底的架构重构。Camera1的设计哲学是“我帮你管”它试图提供一个简单的、封装好的相机操作界面但牺牲了灵活性和控制力。而Camera2的设计哲学是“你自己管”它将相机设备抽象为一个可以发送请求、接收响应的管道Pipeline开发者获得了前所未有的控制权可以精细地管理图像数据的捕获、处理和输出流程。这套新架构的核心价值在于并发性和可控性。想象一下你正在开发一个专业的相机应用用户希望同时看到取景预览、拍摄一张高分辨率的JPEG照片、并且后台还在进行人脸识别分析。在Camera1时代这几乎是不可能高效完成的因为它的流程是线性的、阻塞的。但在Camera2的架构下你可以向相机设备同时提交多个捕获请求CaptureRequest一个用于持续预览输出到SurfaceView或TextureView一个用于单次高画质拍照输出到ImageReader或MediaRecorder另一个甚至可以将YUV数据输出给一个人脸检测算法。相机硬件会尽可能地并行处理这些请求将不同格式、不同尺寸的图像数据送到你指定的目的地。所以当你看到“Android Camera API2架构和流程介绍”这个标题时它背后指向的绝不仅仅是一套新的类和方法而是一套全新的、更贴近现代相机硬件能力的编程模型。理解这套模型意味着你能开发出功能更强大、性能更优异、用户体验更流畅的相机应用。无论是想做一款媲美原生相机的App还是需要集成相机功能进行二维码扫描、AR渲染、文档扫描Camera2都是你必须跨越的门槛。接下来我们就抛开那些晦涩的官方文档描述从一个实践者的角度把这套架构和流程彻底拆解清楚。2. Camera2核心架构模型管道、会话与状态机要驾驭Camera2首先得在脑子里建立起它的核心架构模型。这个模型围绕着几个关键对象展开CameraManager、CameraDevice、CameraCaptureSession和CaptureRequest。你可以把它们想象成一个现代化工厂的生产流水线。2.1 核心组件角色解析CameraManager (android.hardware.camera2.CameraManager) 这是你的“设备总管”或“人力资源部”。它是一个系统服务你的所有相机操作都从这里开始。它的核心职责有两个一是枚举当前设备上所有可用的相机比如后置主摄、超广角、前置摄像头并告诉你每个相机的特性CameraCharacteristics二是根据相机ID帮你打开指定的相机设备。它不参与具体的拍照流程只是个入口和信息查询中心。CameraDevice (android.hardware.camera2.CameraDevice) 这是你申请到的“具体生产车间”。每个CameraDevice代表一个物理相机硬件实例。打开相机后你就获得了这个对象。它本身不直接产生图像它的核心作用是创建一个“生产会话”CameraCaptureSession并最终销毁自己关闭相机。你可以把它看作是硬件资源的持有者。CameraCaptureSession (android.hardware.camera2.CameraCaptureSession) 这是整个Camera2架构的心脏也是最需要理解的部分。我更喜欢叫它“管道配置器”或“流水线”。当你创建CameraCaptureSession时你需要向它提供一个或多个Surface比如用于预览的SurfaceTexture用于拍照的ImageReader的Surface。CameraCaptureSession的工作就是向相机硬件声明“我准备好了这些输出目的地Surface请根据我的要求把图像数据送到这些地方”。一旦创建成功这条配置好的数据管道就建立起来了。之后所有的图像捕获预览、拍照、录像都是通过向这个Session提交CaptureRequest来触发的。一个CameraDevice在同一时间只能有一个活跃的CameraCaptureSession但你可以销毁旧的用不同的Surface组合创建新的。CaptureRequest (android.hardware.camera2.CaptureRequest) 这是发给生产流水线的“工单”或“配方”。它描述了你想要什么。一个CaptureRequest主要包含两部分信息目标Surface列表 这张“工单”产出的图像要送到哪些Surface去。它必须是创建CameraCaptureSession时所用的Surface的子集。参数配置 这是一系列键值对用来控制相机硬件的行为。比如CaptureRequest.CONTROL_AF_MODE: 自动对焦模式自动、连续、微距等。CaptureRequest.CONTROL_AE_MODE: 自动曝光模式。CaptureRequest.JPEG_QUALITY: JPEG压缩质量。CaptureRequest.SCALER_CROP_REGION: 数码变焦裁剪区域。 你可以从一个CameraCharacteristics中获取一个“模板”TEMPLATE_PREVIEW,TEMPLATE_STILL_CAPTURE等基于模板创建CaptureRequest.Builder然后修改你需要的参数。CaptureResult (android.hardware.camera2.CaptureResult) 这是相机硬件执行完一个CaptureRequest后返回的“生产报告”。它包含了这次捕获的实际状态信息比如实际使用的感光度ISO、曝光时间、对焦状态、时间戳等。对于预览你可以通过CaptureCallback持续收到这些结果用来更新UI比如显示对焦成功的图标。对于拍照它通常和最终的图像数据一起返回。Image/ImageReader (android.media.ImageReader) 这是接收高层级图像数据如YUV、JPEG的容器。你创建一个ImageReader指定图像尺寸和格式如ImageFormat.JPEG然后从它那里获取一个Surface将这个Surface加入到CameraCaptureSession中。当有图像数据到来时你可以在回调中从ImageReader里取出Image对象进行处理。处理完后务必记得调用Image.close()否则会迅速耗尽图像缓冲区导致管道停滞。2.2 状态机理解相机的生命周期Camera2 API通过回调来通知你状态的变化理解这些状态是避免崩溃和资源泄漏的关键。主要涉及两个状态机CameraDevice状态机 通过CameraDevice.StateCallback监听。onOpened: 相机成功打开你可以在这里创建CameraCaptureSession。onDisconnected: 相机被异常断开如被更高优先级的应用抢占。onError: 打开过程中发生错误。重要实践在onDisconnected或onError中必须关闭CameraDevice并释放相关资源。CameraCaptureSession状态机 通过CameraCaptureSession.StateCallback监听。onConfigured: 会话配置成功管道已就绪。此时你可以开始发送捕获请求例如开始预览session.setRepeatingRequest(previewRequest, ...)。onConfigureFailed: 配置失败。通常是因为提供的Surface无效例如TextureView还未准备好或不兼容。此时该会话对象已失效需要清理并重试。onClosed: 会话已关闭。踩坑心得状态回调是异步的并且可能不在主线程。更新UI一定要切回主线程。另外CameraCaptureSession的创建createCaptureSession是一个相对耗时的操作最好在子线程中进行或者至少确保不在主线程进行耗时计算以免造成界面卡顿。3. Camera2工作流程全链路拆解理论说再多不如一行代码。下面我们以一个典型的“打开相机 - 预览 - 拍照 - 关闭”流程为例把Camera2的完整工作链路走一遍并解释每个环节的意图和注意事项。3.1 第一步权限检查与相机枚举在操作任何硬件之前权限是敲门砖。你需要在AndroidManifest.xml中声明相机权限并在运行时Android 6.0动态申请。这属于Android基础此处不赘述。拿到权限后第一步是获取CameraManager并枚举相机。CameraManager manager (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); try { String[] cameraIdList manager.getCameraIdList(); for (String cameraId : cameraIdList) { CameraCharacteristics characteristics manager.getCameraCharacteristics(cameraId); Integer facing characteristics.get(CameraCharacteristics.LENS_FACING); if (facing ! null facing CameraCharacteristics.LENS_FACING_BACK) { // 找到了后置摄像头记录cameraId mCameraId cameraId; break; } } } catch (CameraAccessException e) { e.printStackTrace(); }这里CameraCharacteristics是一个信息宝库你可以查询到该相机支持的所有输出尺寸、硬件支持级别、闪光灯模式、自动对焦能力等。在做任何实质性操作前查询这些特性是必须的比如你需要确认相机是否支持JPEG输出以及支持哪些分辨率。3.2 第二步打开相机设备确定了要使用的相机ID后就可以打开它了。这是一个异步操作。manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);mCameraId: 上一步获取的相机标识符。mStateCallback: 一个CameraDevice.StateCallback实例用于接收相机打开成功或失败的通知。mBackgroundHandler: 一个Handler指定回调在哪个线程执行。强烈建议使用一个专用的后台线程Handler避免阻塞UI线程。如果传null回调会在当前线程的主Looper中执行这可能不是最优选择。在你的StateCallback中private final CameraDevice.StateCallback mStateCallback new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice cameraDevice) { // 相机已打开保存cameraDevice实例 mCameraDevice cameraDevice; // 关键步骤创建预览的Surface并开始创建CaptureSession createCameraPreviewSession(); } Override public void onDisconnected(NonNull CameraDevice cameraDevice) { cameraDevice.close(); mCameraDevice null; } Override public void onError(NonNull CameraDevice cameraDevice, int error) { cameraDevice.close(); mCameraDevice null; } };3.3 第三步准备输出Surface与创建CaptureSession这是流程中最核心也最容易出错的一步。假设我们有两个输出目标一个用于预览TextureView一个用于拍摄JPEG照片ImageReader。private void createCameraPreviewSession() { try { // 1. 准备预览Surface (来自TextureView) SurfaceTexture texture mTextureView.getSurfaceTexture(); // 必须设置默认缓冲区大小最好从相机支持的分辨率里选一个合适的 texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface previewSurface new Surface(texture); // 2. 准备拍照Surface (来自ImageReader) // 选择相机支持的JPEG尺寸比如最大的那个 mImageReader ImageReader.newInstance(captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, /*maxImages*/ 2); mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); Surface captureSurface mImageReader.getSurface(); // 3. 构建用于创建Session的Surface列表 ListSurface outputSurfaces new ArrayList(2); outputSurfaces.add(previewSurface); outputSurfaces.add(captureSurface); // 注意拍照Surface也加进去但预览时不使用它 // 4. 创建CaptureRequest.Builder (使用预览模板) mPreviewRequestBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); // 将预览Surface设置为这个Request的目标 mPreviewRequestBuilder.addTarget(previewSurface); // 5. 创建CameraCaptureSession mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { mCaptureSession session; // 会话创建成功开始发送预览请求 startPreview(); } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { Toast.makeText(getContext(), 创建会话失败, Toast.LENGTH_SHORT).show(); } }, mBackgroundHandler); // 同样在后台线程执行 } catch (CameraAccessException e) { e.printStackTrace(); } }关键点解析TextureView的SurfaceTexture必须在onSurfaceTextureAvailable回调之后才能使用否则getSurfaceTexture()可能返回null。ImageReader的maxImages参数表示可以同时留存多少张未处理的图像。设置太小如1容易因为处理不及时导致丢帧设置太大会占用更多内存。对于拍照2通常是个安全值。createCaptureSession的outputSurfaces列表定义了这条数据管道所有可能的输出目的地。即使预览请求不往captureSurface发送数据它也必须在这里声明。TEMPLATE_PREVIEW模板优化了持续预览的功耗和延迟。对于拍照我们会用TEMPLATE_STILL_CAPTURE。3.4 第四步开始预览与处理拍照会话创建成功后就可以开始循环发送预览请求了。private void startPreview() { if (mCameraDevice null || !mTextureView.isAvailable()) { return; } try { // 设置一些预览参数比如连续自动对焦 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 设置自动曝光模式 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); // 创建最终的预览Request CaptureRequest previewRequest mPreviewRequestBuilder.build(); // 设置重复请求第三个参数是CaptureCallback用于接收每一帧的元数据 mCaptureSession.setRepeatingRequest(previewRequest, mCaptureCallback, // 可以监听对焦状态等 mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }当用户点击拍照按钮时我们需要构造一个专门的拍照请求private void takePicture() { if (mCameraDevice null) return; try { // 1. 先停止预览确保拍照瞬间画面是静止的非必须但常见做法 mCaptureSession.stopRepeating(); // 2. 创建拍照用的CaptureRequest.Builder使用STILL_CAPTURE模板 final CaptureRequest.Builder captureBuilder mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); // 这个请求的目标是ImageReader的Surface captureBuilder.addTarget(mImageReader.getSurface()); // 3. 设置拍照专用参数 // 使用和预览相同的对焦、曝光模式或者锁定当前状态 captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE)); // 设置JPEG方向根据设备方向 int rotation getWindowManager().getDefaultDisplay().getRotation(); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); // 可以设置JPEG质量、GPS信息等 // 4. 发送单次捕获请求 mCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull TotalCaptureResult result) { // 捕获完成可以在这里重新开始预览 startPreview(); } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }在ImageReader的回调中处理图像数据private final ImageReader.OnImageAvailableListener mOnImageAvailableListener new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { // 必须在后台线程中处理避免阻塞ImageReader try (Image image reader.acquireNextImage()) { if (image ! null) { ByteBuffer buffer image.getPlanes()[0].getBuffer(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); // 现在bytes就是JPEG图片的数据可以保存到文件或进行其他处理 saveImageToFile(bytes); } } catch (Exception e) { e.printStackTrace(); } } };3.5 第五步资源释放所有操作完成后必须按照正确的顺序释放资源否则会引起应用崩溃或相机服务被占用。private void closeCamera() { if (mCaptureSession ! null) { mCaptureSession.close(); mCaptureSession null; } if (mCameraDevice ! null) { mCameraDevice.close(); mCameraDevice null; } if (mImageReader ! null) { mImageReader.close(); mImageReader null; } }通常在Activity/Fragment的onPause或onDestroy中调用closeCamera。记住CameraCaptureSession和CameraDevice的close()方法是异步的但调用它们会触发相应的onClosed回调你可以在此进行后续清理。4. 高级特性与实战避坑指南掌握了基本流程只是拿到了Camera2的入场券。在实际项目中你会遇到更多复杂需求和坑点。4.1 多Surface输出与数据流选择Camera2的强大之处在于可以同时向多个Surface输出数据。但这里有一个关键限制所有添加到同一个CaptureSession的Surface必须来自相同尺寸的图像流不完全是。更准确的说法是相机硬件有若干条内部分辨率的“数据流”你的输出尺寸必须匹配这些数据流支持的格式和尺寸。你可以通过CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP来查询相机支持的所有输出格式如ImageFormat.YUV_420_888,ImageFormat.JPEG,ImageFormat.RAW_SENSOR及其对应的可用尺寸。一个典型的多输出场景是一个较小的分辨率如1080p用于预览保证流畅一个较大的分辨率如1200万像素用于拍照同时还有一个YUV_420_888格式的输出用于实时图像分析如二维码识别。你需要为这三个输出分别创建Surface并在创建Session时全部传入。然后你可以创建两个不同的CaptureRequest预览请求只输出到预览Surface而拍照请求则输出到拍照和YUV两个Surface如果你需要同时保存照片和进行分析。4.2 对焦、测光与手动控制Camera2提供了比Camera1精细得多的控制。对焦通过CaptureRequest.CONTROL_AF_MODE设置模式如CONTINUOUS_PICTURE。你还可以通过CaptureRequest.CONTROL_AF_TRIGGER来触发一次对焦AF_TRIGGER_START并在CaptureCallback中监听CaptureResult.CONTROL_AF_STATE来判断对焦是否成功。测光区域可以设置CaptureRequest.CONTROL_AE_REGIONS和CaptureRequest.CONTROL_AF_REGIONS来指定自动曝光和对焦的感兴趣区域。这需要相机硬件支持CONTROL_MAX_REGIONS_AE 0。手动控制对于专业模式你可以完全关闭自动控制CONTROL_MODE设为OFF然后手动设置SENSOR_EXPOSURE_TIME曝光时间、SENSOR_SENSITIVITYISO和LENS_FOCUS_DISTANCE对焦距离。务必注意手动参数有有效范围需要通过CameraCharacteristics查询如SENSOR_INFO_EXPOSURE_TIME_RANGE。4.3 常见的“坑”与解决方案createCaptureSession失败这是新手最常见的问题。原因通常是Surface未就绪如TextureView的SurfaceTexture还没创建好。解决方案确保在TextureView.SurfaceTextureListener的onSurfaceTextureAvailable回调后再执行创建操作。提供的Surface尺寸相机不支持。解决方案从StreamConfigurationMap中选取支持的尺寸。传入的Surface列表为空或包含无效Surface。预览方向不对预览画面旋转了90度或180度。这是因为相机传感器方向与屏幕自然方向不一致。解决方案在创建预览Surface时根据CameraCharacteristics.SENSOR_ORIENTATION和当前设备旋转计算并设置TextureView或SurfaceTexture的变换矩阵。private void configureTransform(int viewWidth, int viewHeight) { if (null mTextureView) return; int rotation getWindowManager().getDefaultDisplay().getRotation(); Matrix matrix new Matrix(); RectF viewRect new RectF(0, 0, viewWidth, viewHeight); RectF bufferRect new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth()); // 注意宽高交换 float centerX viewRect.centerX(); float centerY viewRect.centerY(); if (Surface.ROTATION_90 rotation || Surface.ROTATION_270 rotation) { bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); float scale Math.max( (float) viewHeight / mPreviewSize.getHeight(), (float) viewWidth / mPreviewSize.getWidth()); matrix.postScale(scale, scale, centerX, centerY); matrix.postRotate(90 * (rotation - 2), centerX, centerY); // 根据旋转角度调整 } mTextureView.setTransform(matrix); }拍照后预览不恢复在takePicture中调用了session.stopRepeating()但在拍照完成的回调onCaptureCompleted中忘记重新调用startPreview()。解决方案确保在单次捕获的回调中重新发起重复预览请求。内存泄漏与“相机被占用”没有正确关闭CameraDevice、CameraCaptureSession和ImageReader。解决方案建立严格的生命周期管理在onPause中释放所有资源。确保在StateCallback的onDisconnected和onError中也进行关闭操作。ImageReader缓冲区耗尽导致预览卡顿在onImageAvailable回调中处理图像耗时过长或者忘记调用Image.close()。解决方案将耗时操作如保存文件、图像分析移到工作线程并务必使用try-with-resources或finally块确保Image被关闭。5. CameraX更简单的选择但理解Camera2仍有必要看到这里你可能会觉得Camera2的流程颇为复杂。确实Google也意识到了这一点于是推出了CameraX库。CameraX是一个Jetpack支持库它在底层封装了Camera2在兼容设备上但提供了一套更简单、生命周期感知的API。它主要抽象出了几个核心用例预览PreviewView、图片拍摄ImageCapture、图片分析ImageAnalysis。对于绝大多数常见的相机功能扫码、证件照、简单拍照使用CameraX可以极大地提升开发效率减少样板代码。那么还有必要深入理解Camera2吗我的答案是绝对有必要尤其是对于中高级开发者。底层原理CameraX是上层建筑其稳定性和性能优化都建立在Camera2的机制之上。当CameraX出现一些难以解释的问题或无法满足定制化需求时比如需要精细控制3A算法、使用RAW格式、实现复杂的多摄像头同步你最终还是要回到Camera2来找答案。兼容性与控制力CameraX旨在提供一致的API但在某些老旧设备或特定厂商的定制系统上其行为可能不一致。理解Camera2能帮助你在这些边缘情况下进行调试和兜底。架构思维学习Camera2的过程是理解现代移动端相机硬件如何与软件交互的绝佳途径。这种“管道-请求”的模型在多媒体处理领域非常普遍。因此我的建议是对于新项目优先使用CameraX来快速实现核心功能。但同时将Camera2作为一项重要的底层知识来学习和储备。当你的应用需要更高级的特性或者你在使用CameraX遇到瓶颈时Camera2的知识将成为你解决问题的钥匙。毕竟知其然亦知其所以然才能走得更远。