1. 从Camera1到Camera2:为什么我们需要一套全新的API?
如果你在Android开发领域,特别是多媒体或图像处理方向摸爬滚打超过五年,那你一定对那个古老的android.hardware.Camera类记忆犹新。在早期的Android项目中,我们用它来拍照、录像,代码写起来似乎也挺直接:获取实例、设置参数、设置预览、拍照。但真正深入做过相机应用,尤其是需要自定义图像处理、多摄像头切换或者追求极致性能的开发者,都曾在这个老旧的API上栽过跟头。它的设计充满了“历史包袱”,比如参数设置是一个巨大的Camera.Parameters对象,修改任何参数都可能触发一次完整的相机重启,预览和拍照的回调是同步的,很容易阻塞主线程。更头疼的是,它对多摄像头的支持几乎为零,你很难精确地控制图像数据的流向和处理过程。
这就是Google推出Camera2 API(android.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,然后修改你需要的参数。
- 目标Surface列表: 这张“工单”产出的图像要送到哪些
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列表 List<Surface> 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_SENSITIVITY(ISO)和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的知识将成为你解决问题的钥匙。毕竟,知其然,亦知其所以然,才能走得更远。