Android Camera2 API深度解析:从架构原理到实战应用
1. 项目概述:为什么我们需要深入理解Camera2 API?
如果你是一名Android应用开发者,并且你的应用需要与摄像头打交道,那么你大概率已经听说过,甚至“深受其苦”于Camera2 API。从Android 5.0(API Level 21)开始,Google引入了全新的Camera2 API来取代老旧的Camera API。这个变化,对于很多习惯了简单几行代码就能调用摄像头的开发者来说,无异于一次认知上的“降维打击”。Camera1时代,我们调用Camera.open(),设置一些参数,然后startPreview(),一个简单的预览画面就出来了。但Camera2完全不同,它引入了一套基于管道(Pipeline)和会话(Session)的异步、事件驱动的复杂架构。
那么,为什么Google要“自找麻烦”,设计出如此复杂的Camera2 API呢?核心原因在于,老旧的Camera API已经无法满足现代智能手机摄像头硬件飞速发展的需求。如今的手机摄像头,早已不是简单的“一个镜头、一个传感器”。多摄像头系统(广角、超广角、长焦)、高动态范围(HDR)、人像模式、夜景模式、高速连拍、4K/8K视频录制……这些复杂的功能背后,是硬件厂商在图像信号处理器(ISP)、传感器、镜头模组上投入的巨大研发。Camera1 API那种“一刀切”的简单模型,严重限制了硬件能力的发挥,也使得不同厂商的设备表现差异巨大。
Camera2 API的设计哲学,是将摄像头硬件抽象为一个“数据生产工厂”。应用(你写的代码)作为这个工厂的“总调度师”,可以精细地控制生产流水线(Pipeline)的每一个环节:从原材料的获取(传感器捕获原始数据),到不同车间的加工(例如,一个流水线产出预览图,另一个流水线产出高分辨率照片),再到最终产品的输出格式和目的地。这套模型赋予了开发者前所未有的控制力,能够充分发挥硬件的潜力,实现更专业、更复杂的拍摄效果。但同时,这也意味着开发者需要承担更多的责任:理解架构、管理状态、处理异步回调、协调资源。可以说,Camera2 API是一把双刃剑,用好了能打造出媲美原生相机应用的体验,用不好则会让应用崩溃、卡顿、耗电如流水。
接下来,我将以一个“工厂调度师”的视角,带你彻底拆解Camera2 API的架构和核心工作流程。无论你是正在为项目集成一个自定义相机,还是单纯想理解这套复杂系统背后的逻辑,这篇文章都将提供从理论到实操的完整路径。我们会避开官方文档那种冰冷的叙述,用实际编码中会遇到的问题和解决方案,来串联起整个知识体系。
2. Camera2 API核心架构深度拆解
要驾驭Camera2,首先必须建立起正确的“心智模型”。你不能把它看作是一组简单的函数调用,而应该视为一个需要你精心编排的、由多个独立组件构成的系统。
2.1 核心组件与角色扮演
Camera2 API的核心围绕着几个关键类展开,每个类都扮演着“工厂”中的特定角色:
CameraManager(工厂经理): 这是你与设备上所有摄像头硬件交互的入口点。它就像一个工厂的总经理办公室,你通过它来查询工厂有哪些生产线(摄像头),以及每条生产线的基本能力。它的核心方法是
getCameraIdList()(获取所有摄像头ID)和openCamera()(申请打开并连接一条特定的生产线)。注意:
openCamera()是一个异步操作。你不能在主线程(UI线程)上调用它,因为它可能耗时较长(需要初始化硬件),会导致应用无响应(ANR)。必须在后台线程执行。CameraDevice(具体生产线): 成功调用
openCamera()后,你会通过回调得到一个CameraDevice实例。这个对象就代表了你刚刚申请到的那个具体的摄像头硬件。它是所有后续操作的基础。你可以把它理解为一条已经通电、待命的生产线,但流水线具体怎么跑,生产什么,还没定。CameraCharacteristics(生产线说明书): 在打开摄像头之前或之后,你都可以通过
CameraManager.getCameraCharacteristics(cameraId)来获取这个摄像头的“能力说明书”。这是一个不可变的对象,里面包含了海量的信息:- 硬件能力:是否支持自动对焦(AF)、自动曝光(AE)、光学防抖(OIS)等。
- 输出格式支持:支持哪些图像尺寸(
SCALER_STREAM_CONFIGURATION_MAP),例如哪些分辨率支持YUV_420_888格式,哪些支持JPEG直接输出。 - 传感器信息:朝向、物理尺寸等。
- 3A(AF/AE/AWB)模式范围:支持哪些对焦模式(固定、自动、连续等)。 在编写任何拍摄逻辑前,仔细阅读这份“说明书”是避免踩坑的关键。例如,你不能强行要求一个没有自动对焦模块的摄像头去执行连续自动对焦。
CaptureRequest(生产订单): 这是Camera2 API的灵魂。它定义了一次“捕获”操作的具体要求。你可以把它想象成一张发给生产线的“订单单”。
- 订单模板:通过
CameraDevice.createCaptureRequest(int templateType)创建。系统预定义了几个模板,如TEMPLATE_PREVIEW(用于预览)、TEMPLATE_STILL_CAPTURE(用于高质量拍照)、TEMPLATE_RECORD(用于录像)。模板会预先填充一些合理的默认参数。 - 订单细节:你可以对
CaptureRequest.Builder对象设置大量的参数,来定制这张订单。例如:CaptureRequest.CONTROL_AF_MODE: 设置对焦模式(如CONTROL_AF_MODE_CONTINUOUS_PICTURE用于拍照连续对焦)。CaptureRequest.JPEG_QUALITY: 设置JPEG图片质量。CaptureRequest.SCALER_CROP_REGION: 设置数字变焦区域。- 最重要的是,你需要为订单指定“收货地址”,即
addTarget(Surface)。一个Surface代表一个数据输出的目的地(如预览的SurfaceView/TextureView,拍照的ImageReader)。
- 订单模板:通过
Surface(货物目的地/仓库): 这是图像数据的最终去向。在Android系统中,
Surface代表一个可以绘制图像数据的缓冲区生产者端。常见的Surface来源有:SurfaceView/TextureView:用于实时预览,将图像直接显示在屏幕上。ImageReader:一个极其重要的组件。它允许你以指定的格式(如YUV_420_888, JPEG)和尺寸,在内存中获取图像数据,从而进行后续处理(如人脸识别、滤镜、保存到文件)。MediaRecorder:用于视频录制,将图像数据编码为视频文件。 一个CaptureRequest可以同时向多个Surface发送数据,这就是实现“一拍多得”(例如同时生成预览图和全尺寸JPEG)的基础。
CameraCaptureSession(生产流水线与调度中心): 这是将上述所有部分串联起来的“调度中心”。在你配置好一个或多个
Surface(目的地)后,你需要通过CameraDevice.createCaptureSession()来创建一个会话。这个会话会建立一条从摄像头传感器到这些Surface的固定数据流水线。- 会话配置:创建会话时,你需要传入一个
List<Surface>,即所有你希望数据流向的目的地。会话一旦创建,这条流水线的拓扑结构就固定了。之后你可以通过这个会话来提交不同的CaptureRequest(订单),但订单的“收货地址”必须是创建会话时指定的那些Surface之一。 - 会话的生命周期:
创建 -> 就绪 -> 运行(提交请求)-> 关闭。关闭会话是一个昂贵的操作,应尽量避免在频繁拍照/预览中反复创建和关闭。
- 会话配置:创建会话时,你需要传入一个
2.2 状态机:理解摄像头的“工作心情”
CameraDevice和CameraCaptureSession都有自己的状态机。不理解它们,你的应用就会经常遇到“IllegalStateException”(非法状态异常)。
CameraDevice状态机:
- CLOSED:关闭。初始和最终状态。
- OPENING:正在打开。调用
openCamera()后进入。 - OPENED:已打开。可以创建
CaptureSession。 - DISCONNECTED:设备意外断开(如另一个高优先级应用抢占了摄像头)。你必须清理资源并关闭设备。
- ERROR:发生错误。通常需要关闭设备。
你需要监听
CameraDevice.StateCallback来响应这些状态变化。重中之重:在onDisconnected()和onError()回调中,必须关闭CameraDevice并释放所有相关资源。CameraCaptureSession状态机:
- CONFIGURING:正在配置。调用
createCaptureSession后进入。 - READY:配置成功,就绪。可以在
onConfigured回调中开始提交捕获请求。 - CLOSED:已关闭。
- FAILED:配置失败。会话不可用,必须创建新的。
监听
CameraCaptureSession.StateCallback,特别是onConfigured(),这是你开始发送CaptureRequest的绿灯信号。- CONFIGURING:正在配置。调用
实操心得:很多初学者遇到的“Session has been closed”或“Device is not open”错误,根源就在于没有理清这些异步回调的顺序。一个稳健的流程是:在CameraDevice.StateCallback.onOpened()中创建CaptureSession,然后在CameraCaptureSession.StateCallback.onConfigured()中开始发送预览请求。确保所有耗时操作(打开设备、创建会话)都在后台线程进行,但状态回调的处理要注意线程上下文,更新UI需切回主线程。
3. Camera2 完整工作流程与实操实现
理论讲完了,我们来看一个从零开始,实现相机预览和拍照的完整流程。我会把代码拆解成块,并解释每一步的意图和注意事项。
3.1 第一步:权限申请与相机选择
在AndroidManifest.xml中声明权限:
<uses-permission android:name="android.permission.CAMERA" /> <!-- 如果需保存文件,还需要 --> <uses-feature android:name="android.hardware.camera" android:required="true" />在Android 6.0 (API 23)及以上,需要在运行时动态申请CAMERA权限。同时,如果需要写入存储,还需要申请WRITE_EXTERNAL_STORAGE权限。
选择相机时,通常后置主摄的ID是“0”,前置是“1”,但这不是绝对的。更可靠的做法是遍历CameraManager.getCameraIdList(),并通过CameraCharacteristics检查LENS_FACING属性来区分前后置。
private String getBackCameraId(CameraManager manager) throws CameraAccessException { for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { // 进一步检查能力,例如是否支持自动对焦 int[] afModes = characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); if (afModes != null && afModes.length > 0) { return cameraId; // 找到一个可用的后置摄像头 } } } return null; }3.2 第二步:打开相机与配置输出目的地
假设我们有两个输出目的地:一个用于预览的TextureView,一个用于捕获高分辨率JPEG照片的ImageReader。
private CameraDevice cameraDevice; private CameraCaptureSession captureSession; private ImageReader imageReader; private Size previewSize; // 根据TextureView尺寸和相机支持的分辨率计算得出 private Size captureSize; // 选择相机支持的最大JPEG分辨率 // 1. 创建用于拍照的ImageReader imageReader = ImageReader.newInstance(captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, /*maxImages*/ 2); // 缓冲区数量,2通常足够 imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler); // 2. 获取TextureView的Surface SurfaceTexture texture = textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface previewSurface = new Surface(texture); // 3. 打开相机 CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); String cameraId = getBackCameraId(manager); // 使用上面定义的方法 manager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { cameraDevice = camera; // 相机已打开,接下来创建CaptureSession createCameraPreviewSession(); } @Override public void onDisconnected(@NonNull CameraDevice camera) { camera.close(); cameraDevice = null; } @Override public void onError(@NonNull CameraDevice camera, int error) { camera.close(); cameraDevice = null; } }, backgroundHandler); // 注意:传入后台线程的Handler关键点解析:
ImageReader的maxImages参数:它设置了可以同时存在于队列中的最大图像数量。如果处理速度跟不上图像生产速度,队列满了之后,旧的图像会被丢弃。对于拍照,设置为2(一个正在处理,一个在队列)通常足够。对于高帧率预览流,可能需要更大。SurfaceTexture的setDefaultBufferSize:必须调用!这告诉SurfaceTexture底层缓冲区应该分配多大,必须与你选择的预览分辨率一致,否则预览会拉伸或变形。openCamera的回调:onOpened是成功信号,onDisconnected和onError是失败/异常信号,必须在其中进行资源清理。
3.3 第三步:创建捕获会话并开始预览
在onOpened回调中,我们创建会话。
private void createCameraPreviewSession() { try { Surface previewSurface = ... // 从TextureView获取 Surface jpegSurface = imageReader.getSurface(); // 准备一个用于预览的CaptureRequest.Builder CaptureRequest.Builder previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewRequestBuilder.addTarget(previewSurface); // 预览流输出到TextureView // 创建会话,传入所有要使用的Surface cameraDevice.createCaptureSession( Arrays.asList(previewSurface, jpegSurface), // 这里把拍照的Surface也加进去,为后续拍照做准备 new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { captureSession = session; // 会话就绪,开始连续发送预览请求 try { // 设置连续自动对焦模式,适合预览 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 设置自动曝光模式 previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); CaptureRequest previewRequest = previewRequestBuilder.build(); // setRepeatingRequest 会持续发送请求,形成预览流 captureSession.setRepeatingRequest(previewRequest, null, // 可选的CaptureCallback,用于监听3A状态 backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { showToast("创建会话失败"); } }, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }核心操作解析:
createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW):使用预览模板,它会自动配置一些适合实时预览的低延迟、高帧率参数。addTarget(Surface):将预览的Surface添加到请求中,意味着这个请求产生的图像数据会流向这里。createCaptureSession(List<Surface>, ...):这是建立数据流水线的关键调用。传入的Surface列表定义了本次会话所有可能的数据出口。一旦创建,不能动态增删。setRepeatingRequest(CaptureRequest, ...):这个方法会循环不断地向摄像头提交同一个(或同一系列)请求,从而形成连续的预览画面流。这是预览功能的核心。
3.4 第四步:实现拍照功能
拍照,本质上就是通过同一个CaptureSession,提交一个不同于预览请求的新CaptureRequest。
private void takePicture() { if (cameraDevice == null || captureSession == null) { return; } try { // 1. 创建一个用于拍照的CaptureRequest,使用STILL_CAPTURE模板 CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(imageReader.getSurface()); // 输出到ImageReader // 2. 配置拍照专用参数 // 使用与预览相同的对焦和曝光设置(或者可以锁定它们) captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 设置JPEG方向(根据设备旋转传感器信息) int rotation = getWindowManager().getDefaultDisplay().getRotation(); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation)); // 设置JPEG质量 captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95); // 3. 停止预览(可选,为了获得更稳定的图像,特别是闪光灯场景) // captureSession.stopRepeating(); // 4. 提交单次捕获请求 captureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { // 拍照完成!图像数据已经在ImageReader的队列里了。 showToast("拍照完成"); // 恢复预览(如果在第3步停止了) // startPreviewAgain(); } @Override public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) { showToast("拍照失败: " + failure.getReason()); } }, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }拍照流程精讲:
- 创建独立请求:使用
TEMPLATE_STILL_CAPTURE模板,它针对高画质静态图像进行了优化(如可能启用降噪、使用更高的图像质量设置)。 - 指定目标:
addTarget(imageReader.getSurface())是关键。这意味着这张“订单”的货物(JPEG图像)将被送到ImageReader这个“仓库”,而不是预览的SurfaceView。 - 参数设置:
JPEG_ORIENTATION:极其重要!传感器方向是固定的,但手机可以旋转。这个参数告诉摄像头硬件在生成JPEG时进行旋转,保证最终照片的方向正确。你需要根据设备当前朝向和传感器方向来计算这个值。JPEG_QUALITY:设置压缩质量(1-100)。
- 停止预览:在部分场景下,特别是使用闪光灯时,为了保证拍照瞬间的曝光稳定,可以先调用
captureSession.stopRepeating()停止预览流,拍完后再恢复。这不是必须的,但能提升一些场景下的成片率。 - 提交请求:使用
capture()而非setRepeatingRequest()。这是一个单次请求,执行完就结束。 - 处理结果:在
CaptureCallback.onCaptureCompleted中,你知道硬件已经处理完这次捕获。但图像数据本身是通过ImageReader的监听器异步送达的。
3.5 第五步:处理捕获的图像数据
拍照请求完成后,图像数据被送入ImageReader的队列。
private ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { // 必须在后台线程处理,因为可能涉及耗时的I/O操作 try (Image image = reader.acquireNextImage()) { if (image != null) { // 通常我们只关心JPEG格式 if (image.getFormat() == ImageFormat.JPEG) { ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); // 现在bytes就是JPEG图片的字节数组 saveImageToFile(bytes); // 保存到文件 // 或者解码为Bitmap: Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); } } } catch (Exception e) { e.printStackTrace(); } } }; private void saveImageToFile(byte[] jpegData) { // 创建文件路径,注意Android 10及以上作用域存储 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String fileName = "JPEG_" + timeStamp + ".jpg"; File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), fileName); try (FileOutputStream output = new FileOutputStream(outputFile)) { output.write(jpegData); runOnUiThread(() -> showToast("照片已保存: " + outputFile.getAbsolutePath())); } catch (IOException e) { e.printStackTrace(); } }关键细节:
acquireNextImage():从队列中获取下一张可用的图像。这个方法必须在onImageAvailable被调用后尽快执行,因为ImageReader的缓冲区是有限的。获取到的Image对象必须及时关闭(使用try-with-resources语法),释放底层缓冲区以供重用。Image.getPlanes():对于JPEG格式,只有一个平面(Plane 0),其ByteBuffer包含了完整的JPEG文件数据。对于YUV等格式,会有多个平面(Y、U、V),处理起来更复杂。- 线程安全:
onImageAvailable回调在创建ImageReader时传入的Handler所在的线程执行。务必确保在这个线程中进行文件保存等I/O操作,避免阻塞UI线程。
4. 高级话题、常见问题与避坑指南
掌握了基本流程,我们来看看那些容易让人“掉头发”的高级问题和坑点。
4.1 多Surface输出与流配置
Camera2的强大之处在于可以同时向多个Surface输出不同格式、不同尺寸的数据流。例如,你可以同时:
- 向一个小的
SurfaceView输出低分辨率、高帧率的预览流。 - 向一个
ImageReader输出中等分辨率的YUV流,用于实时人脸检测。 - 向另一个
ImageReader输出全分辨率的JPEG流,用于拍照。 - 向
MediaRecorder的Surface输出编码视频流。
关键在于创建CaptureSession时,传入所有需要的Surface。然后,你可以为不同的CaptureRequest指定不同的目标组合。但有一个硬性限制:所有Surface的尺寸组合,必须在相机硬件支持的“流配置表”(SCALER_STREAM_CONFIGURATION_MAP)内。你可以通过CameraCharacteristics获取这个表,并使用isOutputSupportedFor(Surface)或检查尺寸/格式组合是否在支持列表中来验证。
避坑技巧:在配置多流时,一个常见的错误是请求了硬件不支持的尺寸或格式组合,导致createCaptureSession失败。务必在代码中动态检查。一个简单的策略是,优先确定对画质要求最高的那个流(如全尺寸拍照流)的尺寸,然后根据StreamConfigurationMap.getOutputSizes()为其他流选择兼容的、更小的尺寸。
4.2 对焦、测光与曝光(3A)控制
Camera2提供了精细的3A控制。除了设置模式(如CONTROL_AF_MODE_CONTINUOUS_PICTURE),你还可以指定对焦/测光区域。
// 假设用户在预览画面上点击了(x, y)点,需要实现点击对焦 private void handleFocus(MeteringRectangle focusArea) { if (captureSession == null) return; try { // 1. 取消任何正在进行的对焦(可选) previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); // 2. 设置新的对焦区域 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusArea}); // 同样可以设置测光区域 CONTROL_AE_REGIONS previewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{focusArea}); // 3. 触发对焦 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); // 设置对焦模式为 AUTO previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); // 4. 提交这个对焦请求 captureSession.capture(previewRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { // 检查对焦状态 Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { // 对焦已锁定(成功或失败) // 可以恢复连续对焦模式 previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); try { captureSession.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { /* ... */ } } } }, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }注意事项:
MeteringRectangle的坐标是相对于传感器有效阵列的坐标系(通常左上角为(0,0),右下角为SCALER_ACTIVE_ARRAY_SIZE定义的宽高),而不是屏幕坐标。你需要将屏幕触摸点转换到传感器坐标系。- 对焦是一个异步过程,需要通过监听
CaptureResult.CONTROL_AF_STATE来获知对焦状态(对焦中、已锁定、失败等)。 - 触发单次对焦(
CONTROL_AF_TRIGGER_START)后,通常需要在完成后将触发器设回IDLE,并恢复之前的对焦模式。
4.3 生命周期管理与资源释放
这是Camera2开发中最容易导致内存泄漏和崩溃的部分。你必须严格遵守以下顺序:
- 关闭会话:
captureSession.close() - 关闭相机设备:
cameraDevice.close() - 释放Surface:关闭
ImageReader(imageReader.close()),释放SurfaceTexture等。
最佳实践是在Activity/Fragment的onPause()中执行关闭操作,在onResume()中重新打开。但要注意,onPause()/onResume()可能频繁调用(如弹出对话框),反复开关相机成本很高。一个更优的策略是使用LifecycleObserver,在ON_STOP生命周期时关闭,在ON_START时打开。
一个典型的释放方法:
private void closeCamera() { if (captureSession != null) { captureSession.close(); captureSession = null; } if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; } if (imageReader != null) { imageReader.close(); imageReader = null; } }4.4 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 打开相机失败 | 1. 权限未授予。 2. 相机被其他应用占用(如系统相机)。 3. CameraManager.openCamera()在主线程调用导致ANR。 | 1. 检查运行时权限。 2. 捕获 CameraAccessException,其getReason()常为CAMERA_IN_USE。3. 确保在后台线程调用。 |
| 创建CaptureSession失败 | 1. 传入的Surface无效或已释放。2. Surface的尺寸/格式组合相机不支持。3. 相机设备已关闭。 | 1. 检查Surface是否有效(如TextureView是否已可用)。2. 用 StreamConfigurationMap验证尺寸/格式。3. 检查 cameraDevice状态。 |
| 预览画面拉伸或变形 | TextureView的SurfaceTexture未设置正确的缓冲区尺寸。 | 在SurfaceTexture可用后,调用setDefaultBufferSize(previewSize.width, previewSize.height)。 |
| 拍照后图片方向错误 | 未设置JPEG_ORIENTATION参数。 | 根据设备当前旋转方向和传感器方向,计算正确的JPEG_ORIENTATION值并设置到拍照请求中。 |
| 拍照卡顿或延迟 | 1.ImageReader的maxImages设置太小,缓冲区不足。2. 保存图片等I/O操作阻塞了回调线程。 3. 未及时关闭 Image对象,导致缓冲区无法重用。 | 1. 适当增加maxImages(如从2调到3)。2. 确保 onImageAvailable中的处理(特别是文件保存)在独立线程进行。3. 使用try-with-resources确保 Image.close()被调用。 |
| 同时预览和录像时崩溃 | 多流配置超出了硬件能力。 | 检查StreamConfigurationMap,选择一组硬件支持的尺寸组合(通常预览用小尺寸,录像用支持的录像尺寸)。 |
IllegalStateException | 状态机错误。例如在会话关闭后仍提交请求,或在设备断开后未正确清理。 | 在所有CameraDevice.StateCallback和CameraCaptureSession.StateCallback的错误/断开回调中,严格执行资源释放逻辑。在提交任何请求前,检查cameraDevice和captureSession不为null且未关闭。 |
5. 从Camera2到CameraX:如何选择?
如果你被Camera2的复杂性吓到,或者你的应用不需要那么极致的控制,只是想快速、稳定地实现一个相机功能,那么CameraX是你的绝佳选择。CameraX是Jetpack组件库的一部分,它在Camera2的基础上,提供了一套更简单、生命周期感知(lifecycle-aware)且向后兼容(直到Android 5.0)的API。
CameraX的核心优势:
- 用例(Use Case)驱动:你不需要直接管理
CameraDevice、CaptureSession。而是声明你需要什么:预览(Preview)、图片分析(ImageAnalysis)、拍照(ImageCapture)。CameraX帮你处理底层的兼容性和生命周期。 - 自动设备选择:无需自己遍历摄像头ID,可以指定
CameraSelector(如DEFAULT_BACK_CAMERA)。 - 简化的生命周期绑定:通过
ProcessCameraProvider.bindToLifecycle(),相机的开启和关闭会自动与你的Activity/Fragment生命周期同步。 - 一致的API体验:在不同厂商、不同Android版本的设备上,行为更一致。
Camera2 vs CameraX 选择建议:
- 选择Camera2:你需要对图像处理管线进行极精细的控制(例如自定义3A算法、直接处理RAW传感器数据、实现专业的手动模式)、你的应用是专业摄影工具、或者你需要支持非常古老的、CameraX不支持的特定功能。
- 选择CameraX:你需要快速集成相机功能、你的应用是社交、扫描、简单拍摄类应用、你希望减少兼容性测试工作量、你不想处理复杂的生命周期和状态机。
我个人在实际项目中的体会是,对于90%的常规相机需求(预览、拍照、录像、简单的图像分析),CameraX已经完全够用,且能节省大量的开发和维护成本。它封装了Camera2的复杂性,让你能更专注于业务逻辑。但对于那10%需要触及硬件底层能力的场景,深入理解Camera2的架构,仍然是无可替代的。理解Camera2,也能让你更好地理解CameraX背后发生了什么,当遇到CameraX无法解决的极端情况时,你才有能力深入底层去排查和解决。
