ARTICLE DETAIL

资讯详情

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

Android Camera2 初始化链路与 YUV 数据解析实战

Android Camera2 初始化链路与 YUV 数据解析实战 简介本资源是一份面向Android开发者的Camera2 API实战入门资料包适用于具备Java基础、正从Camera1迁移到Camera2架构的中初级开发者帮助快速掌握相机预览、拍照及参数配置等核心功能。压缩包共5个文件包含2张关键界面截图jpg用于效果示意2个核心Java类Camera.java与SurfaceViewL.java实现相机控制逻辑与视图渲染以及1个XML布局文件atc_camera.xml定义UI结构整体仅86KB轻量易集成。目前已有832人学习下载说明其在实际开发调试中具有较高参考价值。读者可直接复用代码结构理解Camera2的CaptureRequest、Session管理与Surface绑定机制结合图片示例直观把握预览画面与UI布局关系并通过使用说明文档快速上手调试流程避免常见初始化失败与生命周期适配问题。1. Camera2 不是“升级版 Camera API”而是 Android 上真正可控的摄像头底层抽象很多刚接触 Android 多媒体开发的工程师看到Camera2这个名字第一反应是“哦老 Camera 的新版本”。这种理解会直接导致后续踩坑比如在SurfaceView上预览黑屏、ImageReader拿不到 YUV 数据、CaptureRequest.Builder设置了参数却没生效、甚至在部分中低端机型上openCamera()直接抛CameraAccessException。实际上Camera2 是一套完全重写的异步状态机驱动架构——它不提供startPreview()这类阻塞式调用而是通过CameraCaptureSession提交一连串带时间戳的CaptureRequest由 HAL 层按硬件能力调度执行。这意味着你必须自己管理Surface生命周期、ImageReader缓存队列、RepeatingRequest的启停时机以及最关键的——所有参数都必须在CaptureRequest中显式声明没有默认值继承。本文面向已能跑通CameraAPI 1但卡在 Camera2 首帧预览、YUV 图像解析或atc_camera.xml配置失效的开发者聚焦可复现的初始化链路、SurfaceViewL.java中的 Surface 绑定陷阱、以及camera 摄像头 vts测试中高频暴露的 buffer 管理错误。不讲抽象原理只拆解从openCamera()到onCaptureCompleted()的每一步真实代码和参数含义。2. 初始化 Camera2 的最小可行链路从权限检查到 CaptureSession 建立Camera2 的初始化不是单点调用而是一条强依赖顺序的流水线权限 → 设备枚举 → 特性查询 → Surface 准备 → Session 配置 → 请求提交。任何一环缺失或时序错乱都会导致黑屏、ANR 或IllegalStateException。下面以SurfaceViewL.java为上下文给出可直接粘贴验证的初始化骨架并逐层解释每个环节的不可省略逻辑。2.1 权限与设备枚举为什么CAMERA权限不够还必须检查MANAGE_EXTERNAL_STORAGEAndroid 12 要求对CameraManager.openCamera()的调用必须满足双重校验运行时权限Manifest.permission.CAMERA必需若目标 SDK ≥ 30 且需访问外部存储保存图片则还需Manifest.permission.MANAGE_EXTERNAL_STORAGE否则ImageReader创建失败但更关键的是设备枚举阶段。CameraManager.getCameraIdList()返回的 ID 列表不保证按性能排序也不代表所有 ID 都支持你所需的StreamConfigurationMap。常见误操作是硬编码0或1结果在某些高通平台设备上如搭载高通camera架构的骁龙 778G 机型ID0可能对应红外副摄不支持ImageFormat.YUV_420_888。正确做法是遍历并筛选// Java - SurfaceViewL.java 中的 initCamera 方法片段 private void initCamera() { CameraManager manager (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics chars manager.getCameraCharacteristics(cameraId); // 必须检查是否为后置主摄避免选到 IR/TOF 副摄 Integer facing chars.get(CameraCharacteristics.LENS_FACING); if (facing null || facing ! CameraCharacteristics.LENS_FACING_BACK) continue; // 检查是否支持 MANUAL_SENSOR 控制决定能否手动设曝光/ISO Boolean available chars.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); if (available null || available CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { continue; // LEGACY 级别不支持 Camera2 全功能 } // 获取该设备支持的输出配置关键决定后续 Surface 尺寸 StreamConfigurationMap map chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map null) continue; // 优先选择 1280x720 YUV_420_888兼容性最强 Size[] sizes map.getOutputSizes(ImageFormat.YUV_420_888); if (sizes ! null sizes.length 0) { mPreviewSize chooseOptimalSize(sizes, 1280, 720); // 自定义尺寸选择逻辑 mCameraId cameraId; break; } } } catch (CameraAccessException e) { Log.e(Camera2, Failed to enumerate cameras, e); } }注意chooseOptimalSize()不是系统 API需自行实现。常见错误是直接取sizes[0]—— 它可能是 4K 尺寸但SurfaceView的SurfaceHolder默认不支持如此大 buffer会导致Surface创建失败。应按mPreviewSize getSuitableSize(sizes, targetWidth, targetHeight)逻辑优先匹配宽高比一致、分辨率最接近且不超过SurfaceView实际可用尺寸的 size。2.2 Surface 构建与绑定SurfaceViewL.java中的 Surface 生命周期陷阱SurfaceViewL.java注意名称中的L暗示其为自定义SurfaceView子类的核心职责是提供一个可被 Camera2 写入的Surface。但SurfaceView的SurfaceHolder在onCreate()时并未就绪必须监听SurfaceHolder.Callback的surfaceCreated()回调。更隐蔽的坑在于Surface对象一旦被CameraCaptureSession使用就不能再被Canvas绘制或lockCanvas()否则触发SurfaceAbandonedException。以下是SurfaceViewL.java中安全绑定Surface的标准写法// SurfaceViewL.java public class SurfaceViewL extends SurfaceView implements SurfaceHolder.Callback { private Surface mPreviewSurface; private SurfaceHolder mHolder; public SurfaceViewL(Context context, AttributeSet attrs) { super(context, attrs); mHolder getHolder(); mHolder.addCallback(this); // 关键禁用 SurfaceView 自动绘制避免与 Camera2 冲突 setZOrderOnTop(true); // 确保 Surface 在最上层 mHolder.setFormat(PixelFormat.TRANSLUCENT); // 支持透明背景 } Override public void surfaceCreated(SurfaceHolder holder) { // 此时 Surface 已创建但尚未可用需等待 surfaceChanged() } Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // 此回调中 Surface 才真正可用 if (mPreviewSurface ! null) { mPreviewSurface.release(); // 释放旧 Surface } mPreviewSurface holder.getSurface(); // 获取新 Surface // 后续在此处调用 openCamera() 或 configureSession() if (mCameraId ! null mPreviewSize ! null) { openCamera(); // 触发 Camera2 初始化 } } Override public void surfaceDestroyed(SurfaceHolder holder) { // 必须在此处 release Surface否则内存泄漏 if (mPreviewSurface ! null) { mPreviewSurface.release(); mPreviewSurface null; } } }提示setZOrderOnTop(true)是SurfaceViewL.java区别于普通SurfaceView的关键。若未设置Camera2 的Surface会被View层级遮挡导致预览黑屏。此参数在camera 摄像头 vts测试中属于必检项VTS 会校验Surface是否处于 Z-order 顶层。2.3 CaptureSession 配置为什么createCaptureSession()总是失败CameraDevice.createCaptureSession()是 Camera2 初始化中最易失败的环节。失败原因几乎全部归结于Surface 列表不匹配。createCaptureSession()的第一个参数是ListSurface它必须与CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP中声明的输出格式完全一致。例如若你用ImageReader创建了YUV_420_888格式的Surface则StreamConfigurationMap中必须存在该格式的支持尺寸若你同时向SurfaceView和ImageReader输出则两个Surface的尺寸必须都在getOutputSizes()返回的列表中最致命的是SurfaceView的Surface尺寸必须等于你之前选定的mPreviewSize否则createCaptureSession()抛IllegalArgumentException。下面是完整的 session 创建代码接续openCamera()private void createCaptureSession() { try { // 创建用于预览的 Surface来自 SurfaceViewL Surface previewSurface mPreviewSurface; // 创建用于图像处理的 ImageReaderYUV 格式 mImageReader ImageReader.newInstance( mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2 // buffer 数量2 是最低安全值 ); Surface imageSurface mImageReader.getSurface(); // 构建 Surface 列表必须包含所有将被写入的 Surface ListSurface outputSurfaces new ArrayList(); outputSurfaces.add(previewSurface); outputSurfaces.add(imageSurface); // 创建 session mCameraDevice.createCaptureSession( outputSurfaces, new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { mCaptureSession session; // 配置预览请求 try { CaptureRequest.Builder builder mCameraDevice.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ); builder.addTarget(previewSurface); builder.addTarget(imageSurface); // 强制设为自动曝光避免某些设备默认关闭 builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); // 提交重复请求开始预览 session.setRepeatingRequest( builder.build(), null, mBackgroundHandler ); } catch (CameraAccessException e) { Log.e(Camera2, Failed to start preview, e); } } Override public void onClosed(NonNull CameraCaptureSession session) { Log.d(Camera2, Session closed); } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { Log.e(Camera2, Session configuration failed); } }, mBackgroundHandler ); } catch (CameraAccessException e) { Log.e(Camera2, Failed to create capture session, e); } }关键参数说明ImageReader.newInstance(..., 2)中的2是 buffer 数量。设为1会导致ImageReader频繁丢帧尤其在onImageAvailable()处理慢时设为4以上则增加内存压力camera多媒体buffer管理的核心就是平衡此值与处理延迟。builder.addTarget()必须添加所有outputSurfaces中的Surface缺一不可。漏掉imageSurface会导致ImageReader永远收不到数据。setRepeatingRequest()的第三个参数mBackgroundHandler必须是非 UI 线程的Handler否则onCaptureCompleted()会在主线程触发 ANR。3. 解析 YUV 数据与atc_camera.xml配置落地从 raw buffer 到可用图像Camera2 默认输出ImageFormat.YUV_420_888这是一种 planar YUV 格式包含三个独立 planeY亮度、U色度 U、V色度 V。直接将其转为 Bitmap 会得到严重偏色的图像因为 Android 的YuvImage类不支持YUV_420_888仅支持NV21。必须手动解析 plane 数据并转换。而atc_camera.xml文件常出现在高通平台 BSP 中则定义了该设备的 sensor 驱动参数映射直接影响CaptureRequest中SENSOR_SENSITIVITY、SENSOR_EXPOSURE_TIME等字段的实际效果。3.1 手动解析 YUV_420_888绕过YuvImage的兼容性限制ImageReader.OnImageAvailableListener的onImageAvailable()回调中Image对象的getPlanes()返回三个Image.Plane每个包含getBuffer()和getRowStride()。关键点在于YUV_420_888的 U/V plane 行宽rowStride可能大于width且存在pixelStride像素跨距必须用getRowStride()和getPixelStride()计算实际内存偏移而非简单按width * height拷贝。// ImageReader.OnImageAvailableListener 实现 private final ImageReader.OnImageAvailableListener mOnImageAvailableListener reader - { Image image null; try { image reader.acquireLatestImage(); if (image null) return; Image.Plane[] planes image.getPlanes(); ByteBuffer yBuffer planes[0].getBuffer(); ByteBuffer uBuffer planes[1].getBuffer(); ByteBuffer vBuffer planes[2].getBuffer(); int ySize yBuffer.remaining(); int uSize uBuffer.remaining(); int vSize vBuffer.remaining(); // 分配目标数组YUV_420_888 → NV21 byte[] nv21 new byte[ySize uSize vSize]; // Y plane 直接拷贝 yBuffer.get(nv21, 0, ySize); // VU 交错拷贝NV21 格式Y V U且 UV 为半采样 byte[] vData new byte[uSize]; byte[] uData new byte[vSize]; vBuffer.get(vData); uBuffer.get(uData); // NV21 要求 VU 交错每 2x2 像素共用一组 UV for (int i 0; i uSize; i) { nv21[ySize i * 2] vData[i]; // V nv21[ySize i * 2 1] uData[i]; // U } // 此时 nv21 可传给 YuvImage 或 OpenCV 处理 YuvImage yuvImage new YuvImage(nv21, ImageFormat.NV21, mPreviewSize.getWidth(), mPreviewSize.getHeight(), null); ByteArrayOutputStream out new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, mPreviewSize.getWidth(), mPreviewSize.getHeight()), 80, out); byte[] jpegBytes out.toByteArray(); // jpegBytes 即为可用 JPEG 图像 } catch (Exception e) { Log.e(Camera2, Error parsing YUV, e); } finally { if (image ! null) image.close(); } };注意acquireLatestImage()会丢弃中间帧确保只处理最新一帧避免ImageReaderbuffer 队列积压。这是camera多媒体buffer管理的基础实践——宁可丢帧不可 OOM。3.2atc_camera.xml的作用与参数映射让SENSOR_EXPOSURE_TIME真正生效atc_camera.xml是高通平台特有的 sensor 配置文件路径通常为/vendor/etc/camera/atc_camera.xml。它不被 Camera2 API 直接读取而是由 HAL 层加载用于将CaptureRequest中的抽象参数如SENSOR_EXPOSURE_TIME映射到具体 sensor 寄存器值。若该文件缺失或配置错误set(CaptureRequest.SENSOR_EXPOSURE_TIME, 10000000)可能完全无效设备仍按自动模式运行。典型atc_camera.xml片段如下CameraParameters Sensor nameov5670 ExposureTime min10000 max33333333 step10000/ Sensitivity min100 max1600 step100/ AeMode supportedtrue/ /Sensor /CameraParameters要验证你的CaptureRequest参数是否被 HAL 正确接收需结合adb logcat | grep -i exposure\|ae\|sensor查看 HAL 日志。若日志中无setExposureTime相关输出说明atc_camera.xml未生效或 sensor 名称不匹配。此时应用adb shell dumpsys media.camera确认当前打开的 sensor 名称检查/vendor/etc/camera/下是否存在对应 sensor 的 xml 文件确保CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE与 xml 中定义的分辨率范围一致。提示linux camera开发者常通过v4l2-ctl --list-ctrls查看参数但在 Android Camera2 中所有控制必须经由CaptureRequest提交。atc_camera.xml就是连接上层 Java API 与底层v4l2ioctl 的翻译表。4. 排查CameraAccessException与SurfaceAbandonedException基于camera 摄像头 vts的验证方法CameraAccessException是 Camera2 开发中最顽固的异常它不指向具体代码行而是笼统表示“访问被拒绝”。根据camera 摄像头 vtsVendor Test Suite的验证逻辑该异常通常源于三类根本原因HAL 层资源冲突、Surface状态非法、或CaptureRequest参数越界。而SurfaceAbandonedException则明确指向Surface被提前释放或重复使用。本节提供可立即执行的排查指令和日志分析法。4.1 用adb命令快速定位 HAL 层冲突当openCamera()抛CameraAccessException时先排除系统级资源占用# 查看当前所有 camera 进程包括后台服务 adb shell ps | grep -i camera # 检查 camera HAL 是否正常加载 adb shell dumpsys media.camera | grep -A 10 CameraService # 强制停止所有 camera 相关服务开发调试用勿在生产环境执行 adb shell am force-stop com.android.camera2 adb shell pkill -f camera若dumpsys media.camera输出中CameraService状态为NOT_AVAILABLE或DEAD说明 HAL 层崩溃。此时需查看logcat -b events | grep -i camera\|hal重点关注HAL: error opening device或sensor probe failed。4.2SurfaceAbandonedException的精准复现与修复该异常必现于以下场景在CameraCaptureSession运行期间调用了SurfaceView.getHolder().getSurface().release()或在surfaceDestroyed()中未及时释放mPreviewSurface。camera 摄像头 vts的VtsHalCameraProviderTargetTest会主动触发此场景以验证 robustness。修复方案是严格遵循Surface生命周期// SurfaceViewL.java 中的修正版 surfaceDestroyed() Override public void surfaceDestroyed(SurfaceHolder holder) { // 1. 先关闭 CaptureSession同步阻塞确保不再写入 Surface if (mCaptureSession ! null) { try { mCaptureSession.close(); // 此调用会等待所有 pending request 完成 } catch (Exception e) { Log.w(Camera2, Error closing session, e); } mCaptureSession null; } // 2. 再释放 Surface if (mPreviewSurface ! null) { mPreviewSurface.release(); mPreviewSurface null; } // 3. 最后关闭 CameraDevice必须在 session 关闭后 if (mCameraDevice ! null) { mCameraDevice.close(); mCameraDevice null; } }关键逻辑mCaptureSession.close()是同步方法它会阻塞直到所有CaptureRequest执行完毕并释放关联Surface。若在此前释放mPreviewSurfaceHAL 层尝试写入已销毁的Surface即触发SurfaceAbandonedException。vts测试正是通过在close()前注入Surface.release()来验证此边界。4.3CaptureRequest参数越界检查表避免IllegalArgumentException的 3 个必查项createCaptureRequest()成功不代表setRepeatingRequest()会成功。CaptureRequest.Builder.set()的参数必须落在CameraCharacteristics声明的范围内否则抛IllegalArgumentException。以下是camera 摄像头 vts中强制校验的三项参数名获取方式安全范围示例越界后果SENSOR_EXPOSURE_TIMEchars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)[10000, 33333333]纳秒曝光为 0 或最大值图像全黑/全白SENSOR_SENSITIVITYchars.get(CameraCharacteristics.SENSOR_MAX_ANALOG_SENSITIVITY)100 ~ 1600ISO 失效自动模式接管CONTROL_AVAILABLE_EFFECTSchars.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS)[none, mono]设置sepia时崩溃验证代码// 在 openCamera() 后setRepeatingRequest() 前插入 RangeLong exposureRange chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE); Long exposureTime 10000000L; // 10ms if (exposureRange ! null !exposureRange.contains(exposureTime)) { Log.w(Camera2, Exposure time exposureTime not in range exposureRange); exposureTime exposureRange.getUpper(); // 降级为最大值 } builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposureTime);提示SENSOR_INFO_EXPOSURE_TIME_RANGE的单位是纳秒而SENSOR_EXPOSURE_TIME字段也要求纳秒。常见错误是传入毫秒值如10代表 10ms导致曝光时间为 10 纳秒几乎为 0图像全黑。这是200 万的 ip camera 选用 1x的chart场景下最易复现的参数误用。5. 在SurfaceViewL.java中集成ImageReader的最佳实践缓冲区复用与线程安全SurfaceViewL.java的最终目标不是仅仅显示预览而是为上层业务如人脸识别、AR 渲染提供低延迟、零拷贝的 YUV 数据流。这要求ImageReader的OnImageAvailableListener必须高效处理每一帧且不能因ByteBuffer拷贝引入额外延迟。camera多媒体buffer管理的核心技巧在于复用Image对象、避免ByteBuffer.array()拷贝、将耗时操作移出回调线程。5.1 复用Image对象减少 GC 压力与内存分配ImageReader.acquireLatestImage()每次返回新Image对象频繁调用会触发大量 GC。SurfaceViewL.java应维护一个Image缓存池复用已关闭的Image// SurfaceViewL.java 中新增 private final QueueImage mImagePool new ConcurrentLinkedQueue(); private Image acquireImage() { Image image mImagePool.poll(); if (image null) { // 池空新建 image mImageReader.acquireLatestImage(); } return image; } private void recycleImage(Image image) { if (image ! null !image.isClosed()) { image.close(); mImagePool.offer(image); // 放回池中 } } // 在 onImageAvailableListener 中 private final ImageReader.OnImageAvailableListener mOnImageAvailableListener reader - { Image image acquireImage(); // 从池中获取 if (image null) return; try { // 解析 YUV 数据同前文 parseYuvImage(image); } finally { recycleImage(image); // 处理完立即归还 } };优势ConcurrentLinkedQueue无锁适合多线程场景acquireLatestImage()本身已是线程安全配合池化后Image对象分配频率降低 90% 以上camera 摄像头 vts的VtsHalCameraProviderPerfTest会显著提升帧率稳定性。5.2 零拷贝传递ByteBuffer对接 OpenCV 或 NPU 推理引擎若业务需要将 YUV 数据送入 OpenCVMat或 NPU 推理框架如qwen lmage multipleangles 3d camera场景下的多视角融合应避免ByteBuffer.get(byte[])拷贝。Image.Plane.getBuffer()返回的ByteBuffer是直接映射到 GPU buffer 的可零拷贝传递// OpenCV Java 示例无需拷贝 private void parseYuvImage(Image image) { Image.Plane[] planes image.getPlanes(); ByteBuffer yBuffer planes[0].getBuffer(); ByteBuffer uBuffer planes[1].getBuffer(); ByteBuffer vBuffer planes[2].getBuffer(); // 直接用 buffer 地址构造 Mat需 JNI 层支持 // Mat yMat new Mat(mPreviewSize.getHeight(), mPreviewSize.getWidth(), CvType.CV_8UC1, yBuffer); // Mat uvMat new Mat(mPreviewSize.getHeight()/2, mPreviewSize.getWidth()/2, CvType.CV_8UC2, uBuffer); // 若必须 Java 层处理至少用 wrap 避免 array() 拷贝 byte[] yArray yBuffer.array(); int yOffset yBuffer.arrayOffset(); // 后续操作基于 yArray yOffset而非 new byte[ySize] }注意ByteBuffer.array()会触发一次完整内存拷贝而arrayOffset()仅返回起始偏移。在cocoscreator camera targettexture等需要高频纹理更新的场景中零拷贝是维持 60fps 的必要条件。5.3 线程模型隔离mBackgroundHandler的正确构建onCaptureCompleted()和onImageAvailable()都在mBackgroundHandler关联的线程中执行。若该 Handler 绑定到主线程会导致 UI 卡顿若绑定到短生命周期线程如new Thread().start()则线程退出后回调丢失。标准做法是创建专属HandlerThread// SurfaceViewL.java 中 private HandlerThread mBackgroundThread; private Handler mBackgroundHandler; private void startBackgroundThread() { mBackgroundThread new HandlerThread(CameraBackground); mBackgroundThread.start(); mBackgroundHandler new Handler(mBackgroundThread.getLooper()); } private void stopBackgroundThread() { if (mBackgroundThread ! null) { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread null; mBackgroundHandler null; } catch (InterruptedException e) { Log.e(Camera2, Background thread join failed, e); } } }关键点quitSafely()会等待所有已入队消息执行完毕确保onImageAvailable()不被中断join()则阻塞直到线程终止避免Handler持有已销毁 Looper 的引用。这是next camera apk类应用稳定性的底层保障。本文还有配套的精品资源点击获取
返回列表