ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Camera2 API 实战:从会话与请求到高效相机开发

Android Camera2 API 实战:从会话与请求到高效相机开发 简介面向Android开发者的Camera2相机开发学习资料包围绕Camera2 API的基本概念、底层架构与实战配置展开。内容详细讲解CameraManager的摄像头管理、CameraDevice的会话创建、CaptureRequest的请求构建以及ImageReader的图像回调并覆盖曝光补偿、ISO、白平衡、对焦模式、多流同步和录像参数等专业控制方法。压缩包共1520个文件以xml界面布局与配置、png图像资源、class字节码、java源码及json数据为主同时包含jar依赖库、apk安装包、gradle构建脚本等工程文件整体约44.72MB适合直接导入Android工程研读或二次开发。已有1820人学习下载。通过学习包内的可运行示例、源码注释和工程结构读者能系统掌握Camera2的工作机制与最佳实践快速实现自定义相机应用并规避设备兼容性等常见问题。 Camera2 API 这篇文章我拖了很久。不是不会写是这东西真要讲清楚牵扯的前置概念实在太多。用过Camera1的老开发者应该都有印象Camera.open()拿设备、setPreviewDisplay()绑画面、startPreview()开预览逻辑挺顺但一碰到前置后置切换预览尺寸拉伸人脸检测这种需求就开始捉襟见肘。Google 在 Android 5.0 推出 Camera2用一整套管道化的设计把这些能力全部重写了一遍。它的学习曲线确实是陡的但一旦你把会话和请求这对核心概念吃透后面无论是做扫码、美颜、连拍还是自定义滤镜都是在同一个框架里加东西而已。这篇文章我按自己从零趟过来的顺序写尽量让没接触过 Camera2 的人也能一步步把相机跑起来。1. Camera2 本质上在干什么把拍照从同步调用变成异步管道先说个反直觉的结论Camera2 里根本没有拍照这个动作。你找不到一个takePicture()之类的方法有的只是往设备里提交一个请求然后等它异步回传结果。旧版 Camera1 的工作方式非常摄像头式你一喊takePicture传感器就开始曝光曝光完了回调给你一帧 JPEG整个过程基本是黑盒你没法干预每一帧的处理细节。Camera2 把这块彻底拆开了它的核心叫捕获管道Capture Pipeline。数据从传感器出来之后会依次经过 3A 算法自动对焦、自动曝光、自动白平衡、图像处理单元最后分发给多个输出目标比如TextureView、ImageReader、SurfaceView。每一步你都能通过CaptureRequest里的参数去干预这就是它能实现各种专业相机功能的原因。打个比方。Camera1 像你下楼买个盒饭只能选固定搭配Camera2 像你进了自助餐厅从拿餐盘到选菜到付钱每个环节都能自己控制代价就是流程变长、步骤变多。这个变长是值得的因为你换来了三样 Camera1 时代很难给到你的东西对每一帧的精细控制、多路输出、高速连拍和 RAW 格式采集。还有一个初学者最容易忽略的点管道是串行的。一节管道的处理速度取决于整个链路里最慢的一环。你给ImageReader设置了一个巨大的分辨率比如 4000x3000那么每一帧输出到预览画面的速度也会被拖慢因为硬件要先把这帧图完整处理完才能给所有 Surface 派发。这就是为什么后面讲预览和拍照的时候我会反复强调预览尺寸别设太高——它不是偏好问题是性能问题。2. 动手前必懂的核心组件CameraManager、CameraDevice、Session、CaptureRequest你可能在别的博客里零零碎碎见过这些类名但想要代码不靠抄、自己写得出来必须把这四个角色的分工和它们的生命周期捋清楚。CameraManager是整个相机的服务台。它本身不干拍照的活只负责两件事一是列出设备上有哪些摄像头cameraIdList二是查询每一颗摄像头的硬件能力CameraCharacteristics。诸如这个设备能不能拍 RAW支持哪些输出尺寸有没有闪光灯前后摄分别在哪一侧全部要从这里拿。CameraDevice是你拿到的那颗摄像头的遥控器。你通过manager.openCamera()开启设备成功后拿到CameraDevice实例。注意openCamera()是异步的结果通过StateCallback返回。这个设计是为了避免在 UI 线程上卡住等待硬件响应。CameraCaptureSession是拍摄现场。它把摄像头硬件和你预先准备的一组Surface绑定在一起告诉系统我这次要往哪些目标上输出画面。这个绑定发生在createCaptureSession()里绑定完成后你会得到一个会话对象。你往这个会话里提交的所有请求都会被送到同一组 Surface 上。CaptureRequest是拍摄指令单。它描述了一帧画面要怎么写以什么分辨率输出、用什么对焦模式、曝光时间和 ISO 多少、要不要开闪光灯。指令单通过session.setRepeatingRequest()提交硬件就会按这套配置连续出帧通过session.capture()提交就只出单帧。CaptureResult则是硬件执行完指令后的回执告诉你这一帧实际是用什么参数拍出来的。这四个组件的关系可以记成一句话Manager 开 DeviceDevice 建 SessionSession 收 Request硬件还你 Result。我在带新人的时候发现只要能把这句话背下来Camera2 的骨架就立住了剩下的全是在这个骨架上填肉。3. 第一步实现相机预览TextureView 方案的全流程预览是几乎所有相机功能的地基。下面我用TextureView方案把流程完整走一遍代码会拆开讲保证你能跟着跑起来。3.1 权限前置与拍照权限检查相机是运行时权限所以AndroidManifest.xml里先声明uses-permission android:nameandroid.permission.CAMERA /然后还要在代码里用checkSelfPermission()判断、requestPermissions()申请。有一点经常被新手漏掉没有摄像头权限时CameraManager.getCameraIdList()会抛异常因为系统不让你知道这台设备有没有摄像头。所以在申请权限的回调里要等授权成功后再去初始化和枚举设备。3.2 TextureView 与 Surface 的配置布局文件里放一个TextureView然后在onResume()里判断它是否可用if (textureView.isAvailable()) { openCamera(); } else { textureView.setSurfaceTextureListener(surfaceTextureListener); }SurfaceTextureListener里的onSurfaceTextureAvailable()回调才是真正可以安全初始化相机的时机。这一步千万别省否则你会遇到莫名其妙打不开设备的状况。关键一步在开摄像头之前设置缓冲区的默认尺寸。TextureView本身只负责显示真正给相机用的缓冲区由它内部的SurfaceTexture提供。SurfaceTexture texture textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());这个尺寸如果不设系统会用TextureView的默认分辨率大概率跟相机支持的分辨率对不上后果就是预览画面比例奇怪或者直接黑屏。3.3 打开摄像头并创建会话选择相机 ID通常CameraCharacteristics.LENS_FACING_BACK的值是后置LENS_FACING_FRONT是前置。我的做法是写一个方法遍历所有摄像头挑出第一个朝向匹配的。private void openCamera() { CameraManager manager (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { String cameraId manager.getCameraIdList()[0]; if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { return; } manager.openCamera(cameraId, stateCallback, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }stateCallback的onOpened回调里拿到CameraDevice后就该建会话了private final CameraDevice.StateCallback stateCallback new CameraDevice.StateCallback() { Override public void onOpened(CameraDevice camera) { cameraDevice camera; createCameraPreviewSession(); } Override public void onDisconnected(CameraDevice camera) { cameraDevice.close(); cameraDevice null; } Override public void onError(CameraDevice camera, int error) { cameraDevice.close(); cameraDevice null; } };创建会话的代码要特别注意Surface必须是由有效的SurfaceTexture创建的而且所有输出 Surface 要在同一个List里一次传给系统。private void createCameraPreviewSession() { try { SurfaceTexture texture textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface surface new Surface(texture); previewRequestBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewRequestBuilder.addTarget(surface); cameraDevice.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { Override public void onConfigured(CameraCaptureSession session) { if (cameraDevice null) { return; } cameraCaptureSession session; previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); previewRequest previewRequestBuilder.build(); try { cameraCaptureSession.setRepeatingRequest(previewRequest, null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } Override public void onConfigureFailed(CameraCaptureSession session) { Toast.makeText(MainActivity.this, 创建会话失败, Toast.LENGTH_SHORT).show(); } }, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }onConfigured回调是关键中的关键只有走到这里真正开始setRepeatingRequest预览才会出画面。我见过太多人把setRepeatingRequest写在createCaptureSession调用之后那个位置会话还没就绪提交请求会被直接忽略。3.4 预览的线程模型与关闭顺序预览跑着不卡、切后台不崩靠的是正确的线程和收尾逻辑。我的习惯是单独维护一个HandlerThread名字叫camera_background所有涉及相机的回调全部切换到这个线程执行UI 线程只管接收最终结果做展示。关闭顺序同样有讲究。正确的顺序是cameraCaptureSession.close()—— 先停会话设备不再出帧cameraDevice.close()—— 再关设备backgroundThread.quitSafely()—— 最后退出后台线程如果顺序反了先 close 设备再 close 会话可能触发底层资源未释放的警告严重时会导致下一次打开相机失败报CAMERA_IN_USE。4. 拍照功能落地JPEG 输出的正确姿势预览跑通之后拍照反而很简单因为你只是往同一个会话里提交一个单帧请求然后把输出从 TextureView 换到 ImageReader 上。但简单的前提是ImageReader用对了。4.1 ImageReader 的创建与生命周期坑ImageReader imageReader ImageReader.newInstance( pictureSize.getWidth(), pictureSize.getHeight(), ImageFormat.JPEG, /* maxImages */ 2);几个参数都别随手填。pictureSize必须从StreamConfigurationMap.getOutputSizes(ImageFormat.JPEG)里挑硬编码一个不支持的尺寸会直接抛异常。maxImages表示缓冲区最多排队几张未取的图我填 2 是为了在快速连拍时留一点余量但不要填太高否则内存压力大。JPEG 格式一帧约耗时是 BMP 之外的压缩输出数据集会比YUV_420_888小。有个经典坑ImageReader必须声明成成员变量。如果你在方法内new出一个局部变量JPEG 回调确实还能触发因为底层线程持有引用但如果你在回调里close()了它下一次拍照就会因为缓冲区已全部被关闭而收到ImageReader的onImageAvailable永远不回调的结果。我当时的症状是第一张照片正常第二张开始进度条一直转没有输出。排查了半天才发现是局部变量在某次 GC 后被回收了。4.2 拍照请求锁定自动对焦再触发拍照不能和预览用同一套请求参数。预览用的是TEMPLATE_PREVIEW拍照要用TEMPLATE_STILL_CAPTURE同时要做一套触发自动对焦→等待对焦完成→拍摄的流程不然拍出来的片子常常是糊的。private void captureStillPicture() { try { CaptureRequest.Builder captureBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(imageReader.getSurface()); captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); // 旋转信息在这里写JPEG 会携带 EXIF 旋转标签 captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getJpegOrientation()); CaptureRequest request captureBuilder.build(); CameraCaptureSession.CaptureCallback captureCallback new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { // 拍照完成可以暂停连续预览或恢复预览 } }; cameraCaptureSession.stopRepeating(); cameraCaptureSession.capture(request, captureCallback, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }注意我在拍照前先调了stopRepeating()。这不是必须的很多 App 直接capture()也能出图但先停连续预览可以避免边预览边单帧模式下偶尔出现的曝光参数互相打架尤其在小分辨率传感器上画面会闪一下。拍完后如果想继续预览要重新setRepeatingRequest。4.3 JPEG 回调里怎么拿字节ImageReader.OnImageAvailableListener里的标准取数据姿势Image image reader.acquireLatestImage(); if (image ! null) { ByteBuffer buffer image.getPlanes()[0].getBuffer(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); image.close(); // bytes 就是完整 JPEG可以直接写文件或上传 }这里有两个容易翻车的点。第一acquireLatestImage()会比acquireNextImage()好因为前者会自动丢弃排队的旧帧拿到最新一帧减少 GPU 内存积压。第二用完必须image.close()。ImageReader的缓冲池是有限的你不关闭缓冲槽不释放拍几张之后就没缓冲可用了onImageAvailable就再也不触发。这个坑在低端机上十几张照片就会暴露调试的时候很难发现。5. 尺寸、旋转、对焦——三大最容易翻车的细节照前面的代码跑你应该已经能出图了。但真正把这些功能打磨到能上线的状态你需要处理下面这三个我几乎在每一个相机项目里都会遇到的细节。5.1 尺寸选择算法别用第一大分辨率系统返回的Size[]是按从大到小排列的吗不一定各家厂商实现不同不能赌顺序。正确做法是把尺寸转成ListSize自定义一个评分逻辑宽度接近目标值、长宽比接近屏幕、分辨率尽量不要超过某个上限三者加权选最优。预览尺寸的核心原则是够用即可。预览的输出分辨率只影响你看的画面清晰度拍出来的照片清晰度由拍照尺寸决定两者可以不同。为了减少功耗和发热预览尺寸选到 1080p 左右就够了不需要在两三千分辨率的手机上跑去匹配 4K 预览。拍照尺寸反而是越大越好——前提是你要处理大图的 OOM 问题这我在后面讲。有一段非常实用的选择逻辑可以记录一下private Size chooseOptimalSize(Size[] choices, int targetWidth, int targetHeight) { ListSize bigEnough new ArrayList(); ListSize notBigEnough new ArrayList(); for (Size option : choices) { if (option.getWidth() targetWidth option.getHeight() targetHeight) { bigEnough.add(option); } else { notBigEnough.add(option); } } if (bigEnough.size() 0) { return Collections.min(bigEnough, new CompareSizesByArea()); } else { return Collections.max(notBigEnough, new CompareSizesByArea()); } }CompareSizesByArea就是按宽乘高比面积的Comparator。这段逻辑的核心思想是优先选大于等于目标值的可选尺寸中面积最小的那个避免选到过于巨大、造成性能浪费的尺寸。5.2 旋转问题横竖屏的 EXIF 和预览拉伸竖屏状态下很多手机会出现预览画面是横的或者拍出来的照片在相册里是旋转的。原因很简单传感器本身安装方向和屏幕方向不一致。传感器输出的原始画面通常是横着的需要根据设备当前的显示方向做一次旋转校正。处理分成两层。预览层面如果你用TextureView需要调用textureView.setTransform(matrix)对显示做矩阵变换让画面方向正确。拍照层面就是在CaptureRequest.JPEG_ORIENTATION里写旋转角度。标准做法是通过Display.getRotation()换算private int getJpegOrientation() { int sensorOrientation characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); int displayRotation getWindowManager().getDefaultDisplay().getRotation(); // 根据前摄/后摄判断是否需要镜像计算最终 JPEG 旋转角 // 具体公式各家有细微差异但思路是统一的 }这个传感器方向 显示方向 输出旋转的组合是相机项目里最容易让人崩溃的部分。我在项目里吃过亏后来干脆写了一个独立的CameraOrientationHelper类把所有旋转计算收拢到一个地方再配合ExifInterface在保存时确保照片带正确的 EXIF 标签。5.3 对焦模式的选择与触发CONTROL_AF_MODE_CONTINUOUS_PICTURE是拍照场景最常用的连续对焦模式它会让镜头持续自动对焦到画面主体上。但有两个场景需要你改成手动触发一是扫码场景连续对焦经常在你把相机对准二维码时反复拉风箱这时用CONTROL_AF_MODE_AUTO在检测到二维码时手动触发一次CONTROL_AF_TRIGGER_START对焦成功后再拍照成功率会高很多。二是文档拍摄文档通常是平面的最近对焦距离附近拉风箱概率大也是推荐AUTO模式配合手动触发。手动触发对焦的完整套路是先发一个CaptureRequest其CONTROL_AF_TRIGGER设置为START在onCaptureCompleted回调里检查CaptureResult.CONTROL_AF_STATE如果变为FOCUSED_LOCKED或NOT_FOCUSED_LOCKED说明对焦流程结束这时再发一个AF_TRIGGER_CANCEL重置状态然后拍摄。注意触发对焦的请求也要设置AF_MODE并且常用TEMPLATE_STILL_CAPTURE作为基础模板否则对焦行为不可控。6. 实战中绕不开的坑黑屏、线程、缓冲池最后这一节聊几个不遇到还好、一遇到就逼疯人的问题都是我实际踩过的。6.1 黑屏的三种常见原因第一种会话创建失败但你没看回调。onConfigureFailed很多时候不是直接崩溃而是静默失败界面黑屏Logcat 里只有一行configureFailed。原因多半是你传进去的Surface尺寸/格式不被支持或者要绑定的 Surface 集合里包含了一个已经被释放的 Surface。我会在代码里专门打印失败时传递的Surface列表方便排查是哪一个出问题。第二种TextureView 未就绪。还是在onSurfaceTextureAvailable之前就尝试openCamera()虽然openCamera不一定失败但创建的Surface是基于未就绪的SurfaceTexture的导致画面永远显示不出来。解决办法就是前面讲的严格在onSurfaceTextureAvailable回调里初始化。第三种setRepeatingRequest没生效。如果是连续预览黑屏、但拍照出图正常几乎可以确定是setRepeatingRequest的请求没有addTarget对应的预览 Surface或者请求在会话onConfigured之前提交了。逐条检查这三处黑屏基本都能解决。6.2 线程没处理好导致的闪退Camera2 的所有回调StateCallback、CaptureCallback、ImageReader回调都发生在你传入的Handler线程上。这意味着回调里不能直接更新 UI必须runOnUiThread或者用Handler切回主线程。这一点初学者第一周必踩。反过来还有一个隐蔽问题因为回调在后台线程所以回调里对cameraDevice、session等对象的访问要加同步或保证线程可见性。我的做法是给这两个成员变量加volatile修饰并在关闭流程里把引用置 null 前用synchronized包裹。否则在多线程环境下可能出现关闭期间回调访问已关闭设备的崩溃。6.3 拍照存图的内存峰值拍照尺寸选到 4000x3000 时一张 JPEG 解码成位图在内存里的开销大约是 4000 * 3000 * 4 字节也就是 48MB。如果还要做缩略图、人脸检测内存峰值很容易把 App 压垮。我的建议是大图存盘走BitmapFactory.Options.inJustDecodeBounds先取宽高再按需采样解码上传场景直接传 JPEG 字节不经过 Bitmap 解码如果需要后续编辑再按实际需要的尺寸解码一次。总之永远不要在内存里同时持有原图 Bitmap 和缩放后 Bitmap。6.4 ImageReader 的格式选择JPEG 的好处是省事缺点是慢。连拍场景下JPEG 编码成为瓶颈每帧要压缩帧率自然上不去。想要真正的连拍能力应该用ImageFormat.YUV_420_888或者RAW_SENSOR。YUV 格式出的是未压缩的原数据ImageReader的onImageAvailable频率能拉满后续再自己用 GPU 或算法转成 JPEG。当然这属于进阶玩法普通项目用 JPEG 就够了。另外我想吐槽一句网上那些新建一个 Surface 同时加入预览 Surface 列表的写法。如果你本来就要ImageReader拍照直接把imageReader.getSurface()也 addTarget 进会话的 Surface 列表就不需要每次拍照重新配会话这个细节能省掉大量代码还能避免会话重建带来的顿挫感。7. 性能优化方向和一些个人心得预览流畅度和相机启动速度是拍照体验的及格线。Camera2 提升这两项的核心在于尽早给传感器喂请求。我实测下来把createCaptureSession的回调里第一帧setRepeatingRequest提前到onOpened后立刻执行、同时把 UI 初始化并行去做启动时间能压掉两三百毫秒。这也是 Google 官方 demo 里能在onConfigured里立即开始预览的原因——会话一旦配置好越早提请求预览首帧越快。最终再提醒一件小事测试 Camera2 一定要用真机而且最好找个中低端机专门挂机型适配。Android 厂商对 Camera2 的实现虽然整体遵循 CTS 测试但在尺寸列表、旋转方向、部分CaptureRequest参数支持度上仍有差异。同一段尺寸选择逻辑在高端旗舰上和低端机上可能选出完全不同的结果这些只有跑真机才暴露得出来。我做了大半年 Camera2 之后最深的体会是千万别想着把官方 demo 的代码粘过来改改就跑。Camera2 的 API 设计是组件化的你得先理解每个组件为什么存在然后在自己的代码结构里给它们安排合理的位置。等你某天突然发现换一颗摄像头、切一个分辨率、改一种对焦策略都不需要重写逻辑只是换参数、换 Surface 时你就真正掌握这套 API 了。本文还有配套的精品资源点击获取
返回列表