
简介Camera2Demo是一份面向Android开发者的Camera2 API封装示例专门解决原生相机接口初始化复杂、业务耦合度高的问题。它把预览、拍照、保存等操作从Activity中剥离通过统一封装降低API使用门槛让开发者能够更专注于界面与业务逻辑。资源包共1087个文件压缩后20.81MB主要包含Java源码、XML布局与配置、Gradle构建脚本以及可直接安装的APK另有大量PNG图片和class编译产物便于对照学习或直接导入工程二次开发。目前已有1291人学习下载。项目核心覆盖CameraManager设备管理、PreviewSurface预览显示、CaptureCallback拍照回调、ImageSaver图像存储等关键模块并附完整的Gradle配置与IDE工程结构可帮助开发者快速理清Camera2的异步回调流程与生命周期管理减少底层细节带来的踩坑成本。适合正在集成拍照功能或希望深入理解相机底层机制的移动端工程师借助该示例可在实际项目中复用这套低耦合设计。 我记得第一次在真机上跑通 Camera2 预览的时候屏幕整整黑了三分钟最后发现只是 TextureView 的 SurfaceTextureListener 回调时机没处理好。如果你也正准备折腾 Camera2或者已经被那套回调嵌套折磨得头大这篇东西应该能帮你省下大量查 Stack Overflow 的时间。我做这个 Camera2Demo 的动力很直接项目里要做一个带实时滤镜和自定义分辨率拍照的相机模块Camera 老 API 在 API 21 之后虽然还能用但在低光、对焦、帧率控制这些方面的能力和 Camera2 完全不在一个量级。Camera2 从 Android 5.0 开始引入设计思路是把相机的控制权完整交给开发者换来的是极高的灵活性但代价是使用门槛陡增。这篇博文会从架构思路讲到具体实现再到我把项目里遇到的各种坑整理成一份排查手册希望能让准备上手的你少走弯路。1. Camera2 的整体设计把相机当成一条数据流水线1.1 Camera 到 Camera2从“黑盒”到“可编程”老 Camera API 的设计方式非常像一台傻瓜相机打开设备、设置参数、调用预览、拍照每个动作都是独立方法系统内部帮你做了大量决策。你无法直接控制传感器曝光时间、无法精确指定对焦区域、更不可能在预览的同时对每一帧做实时干预。而 Camera2 的设计思路完全转变它把整个相机模块抽象成了一条典型的数据流水线应用向相机设备提交 CaptureRequest捕获请求相机设备CameraDevice配置并执行捕获随后通过 CaptureSession 将每一帧结果交给 Surface 队列最后呈现在预览界面或进入 ImageReader 做后续处理。开发者不再告诉相机“我要拍一张照片”而是“请按照这组参数、通过这条流、把每一帧数据放到这个 Surface 里”。这种设计让一切变得可控也让问题变得极其具体。比如你想实现“点击屏幕对焦”这个看起来非常基本的功能在 Camera2 里你需要拿到 MeteringRectangle 类型的对焦区域构造一个带 CONTROL_AF_REGIONS 的 CaptureRequest再重新配置会话。每一步都有明确的 API 和数据对象支撑出错的地方也因此精确到某个参数和某个回调而不是笼统的“拍不了照”。1.2 “会话(Session) 请求(Request) 流(Stream)”三者之间的关系Camera2 的核心运转模式可以压缩成三个概念CameraDevice 代表物理摄像头CaptureSession 是对特定 Surface 组合的会话描述CaptureRequest 则是每次捕获的参数集合。会话先绑定一组目标 Surface预览 Surface、拍照 Surface、帧处理 Surface此后每次发起的 CaptureRequest 都会把数据输出到这些 Surface 上。这意味着如果你需要“预览的同时拍照并把原始帧扔给图像处理模块”就得在创建会话时把 TextureView 的 Surface、ImageReader 的 Surface 一起传入而不是像老 API 那样边预览边切换模式。我最初犯过一个典型错误把拍照 ImageReader 的 Surface 放在会话创建之后才 addTarget结果一直报 “Surface was abandoned”。后来才理解会话建立的本质是系统为这组输出目标预先分配好流配置Session 创建后再动态往不同会话里加 Surface 属于另一个机制需要重新创建会话或使用 mSession.setRepeatingRequest 之外的手段不是随手 append 就能解决。这个理解一旦到位后面整个流程就顺了。2. 动手前必懂的核心 API 与关键概念2.1 五个绕不开的核心类我第一次把 Camera2 的官方文档快速翻完脑子里只剩一个感觉类不多但关系复杂。这五个类是你无论如何都会频繁碰到的值得先彻底吃透。CameraManager系统服务入口负责枚举摄像头、查询 CameraCharacteristics摄像头特征、打开 CameraDevice。几乎所有操作的第一步都从这里出发。CameraDevice代表实际的摄像头硬件。通过 CameraManager.openCamera 打开是一个异步过程需要传入 StateCallback 接收打开结果。CaptureRequest / CaptureRequest.Builder一次捕获的参数集合。Builder 模式构造设置对焦、曝光、闪光灯、3A 模式等达到设计好的参数描述。CameraCaptureSession捕获会话。管理一组输出的 Surface并执行单次预览请求或重复预览请求可以通过 setRepeatingRequest 持续输出帧流。ImageReader用于读取捕获帧数据的生产者-消费者队列。拍照和实时帧处理的场景几乎都绕不开它可以从 acquireLatestImage 拿到原始数据。打个比方CameraManager 更像是“硬件服务的注册中心”CameraDevice 是“设备的句柄”而 CaptureSession 和 CaptureRequest 则是你给相机下达的“生产工单”ImageReader 就是工单完成后货物进入的“缓冲区”。理解这一层协作关系比死记方法名更有用。2.2 CaptureRequest 里的“模板(Template)机制”模板机制是 Camera2 里相当好用却容易被忽略的设计。CameraDevice.createCaptureRequest(int templateType) 可以指定几种预设模板比如 TEMPLATE_PREVIEW、TEMPLATE_STILL_CAPTURE、TEMPLATE_RECORD、TEMPLATE_VIDEO_SNAPSHOT。这些模板是系统为典型场景调好的参数起点你在这个基础上微调会比从零构造所有控制字段靠谱得多。你在配置拍照请求时完全可以在预览参数基础上这样做CameraDevice device ...; CaptureRequest.Builder previewBuilder device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewBuilder.addTarget(previewSurface); previewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);这里模板的意义在于系统已经自动填好了预览常见的 3A自动对焦、自动曝光、自动白平衡策略、帧率上限、输出格式等参数你不需要关心每项的默认值。拍照时再用 TEMPLATE_STILL_CAPTURE 创建另一个带有完全独立参数的 Builder两者互不干扰。这才是推荐姿势不要在一个 Builder 上反复改来改去尤其在预览和拍照之间切换的时候。2.3 预览与拍照的数据流差异Session 如何区分“连续流”和“单帧流”理解数据流是 Camera2 和 Camera 老 API 的分水岭。预览走的是连续请求流通过 setRepeatingRequest 持续发送同参数请求Surface 里的数据不断更新TextureView / SurfaceView 不断消费。拍照则更像是一次性请求capture() 发一个单次请求数据进入 ImageReader拿到 JPEG 或 YUV 原始帧后转存成文件。这两个 Surface 是否可以同时存在于一个 Session可以。Session 可以绑定多个输出 Surface每个 Surface 对应一路 Stream。一个典型的双路流配置如下输出 Surface 归属用途类型TextureView 的 Surface实时预览画面重复请求流ImageReader 的 Surface拍照 / 帧处理单次请求流但需要注意不同 Surface 需要匹配 CameraDevice 支持的 格式和尺寸否则 createCaptureSession 会直接报错。比如预览用 SurfaceTexture需要 ImageFormat.PRIVATE拍照用 JPEG需要 ImageFormat.JPEG 或 YUV_420_888。尺寸方面则要参考 StreamConfigurationMap.getOutputSizes() 支持的列表直接写死一个 1920x1080 也可能在某些设备上报错。3. Camera2Demo 从零搭建全流程3.1 权限申请与相机可用性检查如果你是从零开始建工程第一步并不是写代码而是把权限和硬件检查做对。Camera2 需要 CAMERA 权限Android 6.0 以上还需要动态申请如果 App 声明了相机功能建议在 Manifest 里加一句uses-feature android:nameandroid.hardware.camera.any android:requiredtrue / uses-permission android:nameandroid.permission.CAMERA /然后在打开相机前检查 CameraManager.getCameraIdList()判断设备是否有摄像头避免拿到空列表直接崩溃。很多小白项目在真机上报 “CameraDevice unavailable” 时第一反应是权限弹窗没弹但其实还有可能是系统相机占用。这里我建议把设备打开的结果回调做细回调里的 ERROR_CAMERA_IN_USE、ERROR_MAX_CAMERAS_IN_USE、ERROR_CAMERA_DISABLED 分别对应不同场景提前给出用户可读的提示体验会好很多。3.2 打开相机与配置预览核心代码逐个拆解打开相机这一步我直接把常用流程给你走通。首先通过 CameraManager 获取摄像头列表选择一个后置摄像头 ID然后 openCameraCameraManager manager (CameraManager) getSystemService(Context.CAMERA_SERVICE); String cameraId manager.getCameraIdList()[0]; if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { return; } manager.openCamera(cameraId, stateCallback, backgroundHandler);StateCallback 的 onOpened 回调里不要急着创建预览先把 Surface 准备好再创建会话Override public void onOpened(NonNull CameraDevice camera) { cameraDevice camera; createPreviewSession(); } private void createPreviewSession() { try { SurfaceTexture texture textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewWidth, previewHeight); Surface previewSurface new Surface(texture); ListSurface targets new ArrayList(); targets.add(previewSurface); // 如果同时需要摄像头图像处理/拍照把 imageReader.getSurface() 也加进来 cameraDevice.createCaptureSession(targets, new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { cameraCaptureSession session; startPreview(); } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { // 常见原因是 Surface 尺寸或格式不受支持 } }, backgroundHandler); } catch (Exception e) { // 处理异常 } }这个流程里有个我想强调的点TextureView.getSurfaceTexture() 必须在 SurfaceTextureListener.onSurfaceTextureAvailable 回调之后调用否则拿到的 SurfaceTexture 是空的。很多黑屏问题就源于此。另外createCaptureSession 是一个异步过程你必须在 onConfigured 回调里再发预览请求private void startPreview() { try { CaptureRequest.Builder builder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(previewSurface); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); cameraCaptureSession.setRepeatingRequest(builder.build(), null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }3.3 拍照与实时帧获取ImageReader 的正确用法拍照的核心是把 CaptureRequest 发到 ImageReader 对应的 Surface 上。要保证 ImageReader 尺寸和模板匹配同时注意它的回调线程不能阻塞主线程。我这里给一个可直接套用的拍照代码private void captureStill() { try { CaptureRequest.Builder builder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); builder.addTarget(imageReader.getSurface()); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); builder.set(CaptureRequest.FLASH_MODE, isFlashOn ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF); CameraCaptureSession.CaptureCallback captureCallback new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull TotalCaptureResult result) { // 快门回调 } }; cameraCaptureSession.capture(builder.build(), captureCallback, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } private final ImageReader.OnImageAvailableListener onImageAvailableListener reader - { Image image reader.acquireLatestImage(); if (image ! null) { ByteBuffer buffer image.getPlanes()[0].getBuffer(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); // 保存 JPEG 或处理 YUV image.close(); } };这里有两个细节我踩过很多次坑必须记牢。第一是 ImageReader 的 onImageAvailableListener 回调线程和 cameraCaptureSession 的回调往往在不同线程不要在回调里直接更新 UI用 Handler 切回主线程。第二是 image.close() 一定要执行否则 ImageReader 的队列会被占满几秒钟后你发现画面卡住不动了就是缓冲区没释放。这其实是 Android 相机开发里最容易出现的隐蔽问题。如果要做实时滤镜或算法处理通常不建议用 JPEG 格式因为 JPEG 解码上采样开销太大。直接选择 ImageFormat.YUV_420_888在回调里转成 NV21 或直接传给 OpenGL / 算法库帧率损失会小很多。3.4 参数调优对焦、曝光、闪光灯的实战配置Camera2 的参数调优空间远大于 Camera 老 API但也不是所有参数在所有设备上都生效。常用的一组参数我总结如下控制项相关 Key推荐配置/说明自动对焦模式CONTROL_AF_MODE预览用 CONTINUOUS_PICTURE拍照场景可临时切 AUTO对焦区域CONTROL_AF_REGIONS配合 MeteringRectangle实现点击屏幕对焦自动曝光模式CONTROL_AE_MODE默认 ON低光可调整曝光补偿曝光补偿CONTROL_AE_EXPOSURE_COMPENSATION范围通过 CONTROL_AE_COMPENSATION_RANGE 查询闪光灯FLASH_MODEOFF / TORCH手电常亮/ SINGLE单次闪光白平衡CONTROL_AWB_MODEAUTO或按场景设置 DAYLIGHT、FLUORESCENT 等感光度SENSOR_SENSITIVITYISO仅在手动曝光模式下才真正生效曝光时长SENSOR_EXPOSURE_TIME单位纳秒仅在手动曝光模式下生效对焦这块很多教程都会写 ME region 然后设置 CONTROL_AF_MODE_AUTO但有一个隐藏点部分设备要求必须在 request 中同时设置 CONTROL_AF_TRIGGER_IDLE 并等待对焦状态变化才能触发一次完整的对焦扫描。我的经验是点击屏幕对焦时先设置 TRIGGER_START再在 onCaptureCompleted 里检测 CONTROL_AF_STATE等到状态稳定后再拍成功率明显更高。曝光补偿则是对付逆光场景的利器比如 CONTROL_AE_EXPOSURE_COMPENSATION 设置为 2 或 3画面立刻通透很多但要注意别超过设备支持的范围所以需要先读取 AE_COMPENSATION_RANGE。4. 常见问题与排查技巧实录4.1 预览黑屏与数据显示异常黑屏大概是我在 Camera2 项目里看到最高频的问题。归纳起来主要原因有三类一是 SurfaceTexture 时机不对前面已经说过必须在 onSurfaceTextureAvailable 里初始化二是预览尺寸设置不合理TextureView 的默认 buffer 大小和 CameraCharacteristics 支持的分辨率不匹配比如你直接 setDefaultBufferSize(1080, 1920)但相机输出列表里根本没有这个规格三是 createCaptureSession 的 targets 为空或 Surface 已无效。排查思路我建议用一个简单表格对照现象可能原因验证方式预览全黑但 App 不崩SurfaceTexture 未初始化 / Session 未触发重复请求检查 onSurfaceTextureAvailable 是否执行setRepeatingRequest 是否被调用预览画面拉伸变形预览尺寸宽高比和 TextureView 不匹配手动调整 TextureView 的 AspectRatio或设置固定比例onConfigureFailed 回调Surface 尺寸/格式不被支持打印 StreamConfigurationMap.getOutputSizes(ImageFormat.PRIVATE) 检查是否包含你设置的尺寸画面一直模糊自动对焦模式失效检查 CONTROL_AF_MODE 是否设备支持换成 AUTO 手动触发4.2 摄像头被占用与重新连接问题当系统相机或另一个 App 正在使用摄像头时你的 App 打开摄像头就会失败。你可以通过 CameraManager.AvailabilityCallback 监听摄像头可用状态变化manager.registerAvailabilityCallback(new CameraManager.AvailabilityCallback() { Override public void onCameraAvailable(NonNull String cameraId) { // 重新打开摄像头 } Override public void onCameraUnavailable(NonNull String cameraId) { // 提示用户摄像头被占用 } }, backgroundHandler);这个机制的实用价值不低尤其做扫码类 App 时用户经常在摄像头被占用或切换后台的边界场景里操作。我还建议在 onDisconnected 和 onError 回调里做资源释放并且把 cameraDevice.close() 放在 finally 里避免下一次打开时因为旧资源未释放而失败。连接状态的管理一定要把线程切回主线程因为 Camera2 的回调线程默认是 binder 线程直接操作 UI 或 TextureView 会出问题。4.3 闪退、OOM 与生命周期管理的几个隐藏坑Camera2 的 OOM 主要来自两块ImageReader 队列缓冲区和 YUV 图像处理时的临时数组。如果你用 ImageReader 做实时处理每个 Image 都是几百 KB 到几 MB 不等一旦不 close()很快就把堆内存吃满。这里我推荐一个实践ImageReader.setMaxImages() 设置为 2 或 3并在回调里始终使用 acquireLatestImage 而不是 acquireNextImage前者会自动丢弃旧帧避免处理速度跟不上帧率。生命周期管理的核心是在 onPause 里释放会话和相机设备。正确顺序是先关闭重复请求再关闭会话最后关闭相机设备Override protected void onPause() { super.onPause(); if (cameraCaptureSession ! null) { cameraCaptureSession.close(); cameraCaptureSession null; } if (cameraDevice ! null) { cameraDevice.close(); cameraDevice null; } }如果你把顺序颠倒比如先关 cameraDevice 再关 session很多设备会直接报 IllegalStateException。另外 TextureView 是非硬件加速的叠加层性能要求高的场景我建议直接用 SurfaceView但 SurfaceView 的动画、旋转、变形处理不如 TextureView 方便这是开发者需要权衡的地方。你也可以在 SurfaceView 和 TextureView 之间做个可切换的封装按场景动态选择。5. 最终的实践心得与后续可以扩展的方向5.1 我的真实体会Camera2 的学习曲线确实陡峭尤其在你从写业务代码切换到写这种“异步回调驱动”的代码时前期几乎每一步都在揣测“回调到底进没进、状态对不对”。我的建议是第一次做 Camera2Demo 千万别直接照抄官方示例就结束。官方代码为了保证通用性抽象得很厉害很难一眼看透核心逻辑。我自己是先抄了一遍官方源码然后又从零手写了一个精简版把 Session、Request、Surface 这三条线彻底理清之后再做滤镜、人脸检测、HDR 这些功能就轻松多了。另一个真实的感受是Camera2 的兼容性差异比文档描述的还要明显。同一个参数控制在 A 厂商手机上正常在 B 厂商手机上可能完全不生效或者控制范围不同。所以在做参数调优时永远要通过 CameraCharacteristics 去读取实际支持的能力列表不能写死任何假设。真机测试始终是 Camera2 开发的必修课模拟器上根本还原不了这些硬件细节。5.2 后续可以怎么扩展Camera2Demo 能延展的方向很多。如果目标是做美颜相机或滤镜相机可以在 ImageReader 的 YUV_420_888 帧上接入 RenderScript 或 OpenGL 做逐帧着色器处理如果目标是做扫码工具可以把预览帧直通 ZXing / ML Kit 的输入接口画面实时解码如果目标是做视频录制则需要同时绑定 MediaRecorder 的 Surface并配合 TEMPLATE_RECORD 的请求模板。还有一个我个人很推荐的方向先用 Camera2Demo 打通全流程再对比 CameraX 的封装接口你会发现 CameraX 把 Camera2 的 70% 复杂度藏了起来而 Camera2 教会你的是那 30% 的底层真相——这 30%恰恰决定了你在遇到疑难杂症时能不能靠自己定位问题。最后再分享一个小技巧开发时打开 logcat过滤关键字 “Cam2” 和 “Camera-JNI”很多硬件层面的报错会给到比应用层更精确的提示。掌握这个观察方法以后排查速度会快很多。本文还有配套的精品资源点击获取