Android Camera2相机开发实战:从预览到拍照的完整链路与踩坑指南

发布时间:2026/9/2 2:11:20
Android Camera2相机开发实战:从预览到拍照的完整链路与踩坑指南 简介对于Android开发者而言Camera2 API功能强大但使用门槛较高。Camera2Demo是一份面向需快速集成拍照功能的开发者的封装示例工程通过将相机初始化、预览、拍照与保存逻辑从Activity中剥离帮助降低代码耦合、简化调用流程。资源共含1087个文件涵盖Java源码、xml布局与配置、Gradle构建脚本、apk安装包及调试class文件等压缩包约20.81MB可从源码、配置到成品层级完整理解Camera2的封装思路。核心模块包括CameraManager设备管理、SurfaceView/TextureView预览、CaptureCallback回调处理以及ImageSaver图片存储便于读者直接借鉴或二次开发。目前已有1291人学习浏览适合希望避开Camera2底层细节、快速落地相机功能的中高级Android开发者。通过阅读和运行此Demo能更清晰地掌握相机权限处理、会话创建与图像保存等关键环节有效提升开发效率。 做 Android 开发这么久我觉得相机模块是少数几个“看着简单、碰了就头大”的方向。第一次拿到 Camera2Demo 这个题目时我还挺乐观的——无非就是打开相机、显示预览、点一下拍照嘛。真正动手之后才发现光是理解 Camera2 那套回调流程就够我对着 Logcat 熬好几个晚上。这篇博客不打算写成 API 文档翻译我就以这个 Camera2Demo 为线索把它背后的设计逻辑、关键代码链路和我在实战中填过的坑一次讲清楚。如果你是刚接触 Camera2 的 Android 开发者或者正准备做自定义相机、扫码、文档拍摄这类底层预览需求这篇文章应该能帮你省下不少试错时间。1. Camera2 这套 API 到底在说啥1.1 Camera1 到 Camera2 的跳跃太大了很多老 Android 开发者对 Camera1 还保留着感情毕竟它简单new 一个 Camera 对象setPreviewDisplay 绑定 SurfacestartPreview 就能出画面takePicture 直接回调 JPEG 数据。一套下来三五十行代码搞定。但它的上限也摆在那里——对焦模式、曝光补偿、感光度这些参数能动的空间非常有限更别说多摄像头、RAW 输出、逐帧控制这类高级能力了。Camera2 从根上换了一套设计思路。它把相机当成一条“请求-处理-输出”的流水线你向系统提交一个 CaptureRequest描述“我想要什么样的画面”相机子系统去执行然后把数据送到你指定的 Surface 上。预览、拍照、录像这三件事本质上是同一条流水线的不同配置而已。我刚接触这套概念时最大的困惑是为什么非要搞这么多回调和状态后来想明白了因为相机的硬件资源是有限的全局资源系统必须用严格的状态机来管理“谁在用相机、用到了哪一步、什么时候可以换人”。Camera2 的复杂度有一半是在为这种资源管理负责。1.2 四个核心成员先记住CameraManager系统级管理器负责枚举相机、打开相机。你说的“让我看看手机上有几个摄像头”就是它干的活。CameraDevice代表一台已经打开的物理相机。它本身不输出画面只是你与硬件对话的通道。CaptureSession一次捕获会话。它把 CameraDevice、一组输出 Surface 和请求队列绑定在一起相当于为这条流水线搭好了管道。CaptureRequest一次具体的拍摄请求。你要预览还是要拍照要什么对焦模式要不要开闪光灯全写在这里面。打个比方CameraManager 是相机租赁公司的前台CameraDevice 是你租到的一台单反CaptureSession 是你搭好的摄影棚和灯光架CaptureRequest 就是你给摄影师下的具体拍摄指令。想明白这四个角色后面看代码就不会晕了。2. Demo 工程的骨架模块划分与线程模型2.1 别把回调全扔主线程Camera2 的 API 大量使用回调像 onOpened、onDisconnected、onCaptureCompleted 这些默认跑在调用线程的 Looper 上。如果直接在 Activity 里调用 openCamera 不给 Handler回调就会挤在主线程一旦相机服务响应慢很容易卡 UI。我在 Demo 里单独开了一个 HandlerThread取名 cameraThread让它专门承载相机回调。这样主线程只管界面刷新和用户交互相机内部逻辑全部丢给后台线程两者互不干扰。private lateinit var cameraThread: HandlerThread private lateinit var cameraHandler: Handler override fun onResume() { super.onResume() cameraThread HandlerThread(CameraBackground).also { it.start() } cameraHandler Handler(cameraThread.looper) } override fun onPause() { cameraThread.quitSafely() super.onPause() }这里有个容易被忽略的细节closeCamera 也要在 cameraThread 里排队执行。因为相机的 open 和 close 都是异步操作如果你开线程 A 打开、线程 B 关闭回调之间就会出现竞态比如打开还没完成就收到关闭指令表现为各种诡异的 IllegalStateException。2.2 Demo 的模块划分我没有把代码全堆在 Activity 里而是按职责拆成了四个文件CameraPermissionHelper运行时权限申请和检查。CameraPreviewFragment界面层持有 TextureView 和拍照按钮。Camera2Controller核心控制类封装打开相机、创建会话、预览和拍照。ImageSaver后台保存图片的任务避免拍照后 UI 线程做 IO。这个拆法在 Demo 阶段看着有点过度设计但到了后面想扩展录像、切换摄像头、加滤镜时你就知道好处了——你只需要改 Camera2Controller界面层完全不用动。2.3 权限和特性声明在 AndroidManifest 里除了声明 CAMERA 权限我还加了uses-feature android:nameandroid.hardware.camera.any android:requiredtrue /用 camera.any 而不是 camera是因为前置摄像头也算设备。如果你只声明后置 camera某些只有前置摄像头的平板会被 Play 商店过滤掉。权限请求这里没太多好说的运行时权限用 ActivityResultLauncher 申请一次即可拒绝后要给出明确的引导提示。3. 打开相机到预览上屏核心链路逐段拆3.1 先用 TextureView 还是 SurfaceView预览画面需要一块 SurfaceAndroid 里常用的有 SurfaceView、TextureView、SurfaceHolder。Demo 里我选了 TextureView原因是它可以直接在 View 树里做变换和动画而且可以被 UI 控件覆盖叠加。SurfaceView 的预览画面是独立于 View 层级的一个窗口有些国产 ROM 上会出现预览被控件遮挡异常或者截屏黑屏的问题。不过 TextureView 也有个坑它内部依赖 SurfaceTexture必须等 SurfaceTexture 真正 available 之后才能开始建立相机会话。所以第一步是先给 TextureView 设置监听。textureView.surfaceTextureListener object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { openCamera() } // 其他回调省略 }3.2 打开相机并配置预览尺寸打开相机的方式是调用 CameraManager.openCamera(cameraId, stateCallback, handler)。在 stateCallback 的 onOpened 里拿到 CameraDevice然后进入创建会话阶段。这里有个很关键的步骤TextureView 拿到的 SurfaceTexture 必须先 setDefaultBufferSize 设置缓冲区尺寸不然后面 createCaptureSession 创建的 Surface 尺寸和实际预览画面尺寸不一致很多人预览模糊或者变形都是栽在这里。private fun chooseOptimalSize( choices: ArraySize, textureViewWidth: Int, textureViewHeight: Int ): Size { // 这里做了简化核心逻辑是找到宽高比和 TextureView 最接近、且不超过最大预览尺寸的那个 return choices.maxBy { it.width * it.height } } private fun setUpCameraOutputs() { val manager getSystemService(Context.CAMERA_SERVICE) as CameraManager cameraId manager.cameraIdList[0] val characteristics manager.getCameraCharacteristics(cameraId) val map characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! previewSize chooseOptimalSize( map.getOutputSizes(SurfaceTexture::class.java), textureView.width, textureView.height ) textureView.surfaceTexture.setDefaultBufferSize(previewSize.width, previewSize.height) }3.3 建立会话和连续预览请求拿到 CameraDevice 之后把它和预览 Surface 一起交给 createCaptureSession。这个回调里再创建请求先构建一个 CaptureRequest.Builder模板用 TEMPLATE_PREVIEW。把预览 Surface addTarget 进去。调用 session.setRepeatingRequest让相机持续输出画面。private fun createCameraPreviewSession(device: CameraDevice) { val texture textureView.surfaceTexture texture.setDefaultBufferSize(previewSize.width, previewSize.height) val surface Surface(texture) val previewRequestBuilder device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(surface) } device.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { captureSession session previewRequest previewRequestBuilder.build() session.setRepeatingRequest(previewRequest, null, cameraHandler) } override fun onConfigureFailed(session: CameraCaptureSession) { // 提示用户创建会话失败 } }, cameraHandler) }注意setRepeatingRequest 是持续不断地向相机发出同一份请求用于预览。而拍照用的 capture 则是一次性请求。很多人只记得拍照用 capture忘了预览后要重新 setRepeatingRequest结果拍完照画面卡死。3.4 预览画面的方向修正Camera2 的预览方向问题让无数新手抓狂。传感器默认的坐标原点是横屏方向如果不加任何处理竖屏下你能看到预览画面横着或者转了 90 度。标准做法是获取 SENSOR_ORIENTATION再结合前置/后置摄像头来旋转 TextureViewprivate fun configureTransform(viewWidth: Int, viewHeight: Int) { val rotation display.rotation val matrix Matrix() val viewRect RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) val bufferRect RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) bufferRect.offset(viewRect.centerX() - bufferRect.centerX(), viewRect.centerY() - bufferRect.centerY()) matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) textureView.setTransform(matrix) }上面的代码只是把预览画面放进 View 的布局里。方向修正要拿到 sensorOrientation 后根据前置后置做不同角度的旋转。我实测下来后置摄像头通常需要转 90 度前置是 270 度还附带镜像但这在不同 OEM 机型上不完全一致稳妥的办法是把 SENSOR_ORIENTATION 打印出来逐一验证。4. 拍照链路和踩坑实录4.1 用 ImageReader 接 JPEG 输出预览链路跑通后拍照其实是在预览基础上加一个 ImageReader。先注册一个 JPEG 格式的 ImageReader在创建 CaptureSession 时把它的 Surface 一起加进去然后构建 TEMPLATE_STILL_CAPTURE 请求触发拍照。imageReader ImageReader.newInstance( previewSize.width, previewSize.height, ImageFormat.JPEG, 1 ).apply { setOnImageAvailableListener({ reader - val image reader.acquireLatestImage() val buffer image.planes[0].buffer val bytes ByteArray(buffer.remaining()) buffer.get(bytes) saveImage(bytes) image.close() }, cameraHandler) }这里必须强调Image 对象用完一定要 close。ImageReader 内部是一个有限的缓冲区池如果你不 close后面的帧就一直拿不到新的 Image轻则拍照变慢重则直接阻塞相机管线。4.2 对焦模式的选择预览时我建议把对焦模式设为 CONTROL_AF_MODE_CONTINUOUS_PICTURE这样相机会持续自动对焦出片成功率最高。如果你用 CONTROL_AF_MODE_AUTO要自己先触发对焦等 CONTROL_AF_STATE 变成 FOCUSED_LOCKED 再拍照否则很容易拍出糊片。previewRequestBuilder.set( CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE )拍照请求里的对焦模式最好保持和预览一致或者至少在拍照前发一次对焦触发否则你按下快门的瞬间相机可能还在进行上一次对焦扫描。4.3 拍照后的状态管理这是我 Demo 里踩得最狠的坑之一拍完照后画面卡住不动了。原因是拍照请求执行完相机会暂时停止向预览 Surface 输送帧数据如果我在 onCaptureCompleted 里没有重新提交 setRepeatingRequest预览就停在那儿了。正确流程是拍照请求的 callback 里onCaptureCompleted 触发后再次调用 session.setRepeatingRequest 恢复预览。如果做了自动对焦锁定还要重新回到 CONTINUOUS_PICTURE 模式。override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) { // 拍照完成后重新开启预览 session.setRepeatingRequest(previewRequest, null, cameraHandler) }还有一个细节拍照请求不要和预览请求共用同一个 CaptureRequest.Builder。因为预览请求会持续运行你把拍照参数改到同一个 builder 上下一次预览周期就会带着这些参数跑导致后续画面白平衡、曝光全部错乱。4.4 闪屏、黑屏和镜像问题黑屏最常见原因是 createCaptureSession 用的 Surface 绑定的 SurfaceTexture 还没 available或者 setDefaultBufferSize 设的尺寸有问题。我排查时习惯在 onSurfaceTextureAvailable 里打日志确认尺寸合法再往下走。闪屏如果 TextureView 的 SurfaceTexture 因为 Activity 重建被销毁了而相机会话还挂在旧 Surface 上就会出现闪屏或黑屏。所以 onPause 里必须把相机全部关掉等 onResume 重新走完整流程。镜像前置摄像头预览默认是镜像的这是 Android 的默认行为。如果你不想镜像需要从 characteristics 里拿到 LENS_FACING如果是 FRONT就用 setScaleX(-1f) 翻转或者直接在 SurfaceTexture 的 transform 矩阵里处理。这些坑表面上看都是“画面异常”之类的表象实际定位时别急着改代码先确认自己是卡在哪个状态是 Surface 没准备好还是请求没提交还是 ImageReader 的 Buffer 没释放。顺着这个思路打日志十分钟就能定位。5. 从 Demo 走向实战还需要补哪些功课5.1 多摄像头和摄像头切换现在手机基本都是双摄、三摄甚至更多。Camera2 的 CameraManager.cameraIdList 会返回所有摄像头但区分广角、超广角、长焦不能只看 LENS_FACING要结合每个摄像头的 FOV物理视场角和焦距判断。好的做法是用 getCameraCharacteristics 检查 SENSOR_INFO_PHYSICAL_SIZE 和 LENS_INFO_AVAILABLE_FOCAL_LENGTHS筛选出你想要的目标镜头。切换摄像头的本质是重新走一遍 openCamera 到 createCaptureSession 的完整流程所以 Demo 里把这一套封装好后面切换就只是换一个 cameraId 的事。5.2 录像和实时帧分析如果你要录像只是把输出目标从 ImageReader 换成 MediaRecorder 的 Surface或者用 MediaCodec 的输入 Surface。核心链路不变。实时帧分析比如扫码、人脸检测更简单把 ImageReader 的 ImageFormat 换成 YUV_420_888在 onImageAvailableListener 里接帧做分析就行。但注意 YUV 数据量大ImageReader 的 maxImages 参数要调高一些一般设 3 到 5不然处理速度跟不上相机帧率会掉帧。5.3 参数控制的进阶玩法Camera2 最迷人的地方就是手动控制。你可以调节 ISO、快门速度、曝光补偿甚至输出 RAW 格式。Demo 里如果只做自动模式等于浪费了这套 API 的一半能力。建议在 Demo 基础上加一个手动模式入口把这两个参数接上 UI 滑块SENSOR_EXPOSURE_TIME单位纳秒一般 1000000010ms到 3000000030ms之间比较常用。SENSOR_SENSITIVITYISO 值不同设备范围差异很大最好从 SENSOR_INFO_SENSITIVITY_RANGE 里读。调手动参数时有个坑你手动设置曝光时间后系统可能不再自动调整帧率预览会出现卡顿。需要在请求里同时设置 CONTROL_AE_MODE 为 OFF并配合 SENSOR_FRAME_DURATION 把帧时长设为一个稳定值这样才能保证预览和成片的帧率一致。5.4 厂商兼容性说实话Camera2 在不同品牌机型上的表现差异比官方文档写的要复杂得多。有些国产 ROM 对 createCaptureSession 的最大 Surface 数量有限制你同时挂预览、拍照、录像三个 Surface 就可能失败。有些设备对 JPEG 缩略图尺寸处理有 bug生成的照片在相册里显示为黑图。遇到这类问题最直接的办法是抓 CameraDevice 的异常回调以及 logcat 里搜索 CameraService 的报错然后针对具体机型和 Android 版本做降级处理。我在做这个 Camera2Demo 时最深的体会是Camera2 的知识点不是线性的而是一张网。你要同时理解状态机、线程模型、Surface 生命周期、图像格式任何一个环节断了表现出来都是黑屏或卡死。但一旦把这张网织起来后面接 Vision、接滤镜、接多摄都只是在这个骨架上加模块而已。所以我建议你把这篇博客里的代码链路亲手敲一遍最好再故意制造几个错误看看 Logcat 里会给出什么样的报错信息——这些报错信息以后就是你排查线上问题时的老朋友了。本文还有配套的精品资源点击获取