ARTICLE DETAIL

资讯详情

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

Android相机开发实战:深入Camera2 API架构与调用流程

Android相机开发实战:深入Camera2 API架构与调用流程 简介面向中高级Android开发者的相机系统深度解析文档围绕Camera架构演进、Google Camera API2的完整调用流程、HAL3实现与V4L2框架展开可帮助解决相机方向/大小适配、调试手段、功耗优化及多摄像头支持等实际问题。全部内容收录于1份docx文档共1个文件压缩包大小8.86MB内含大量代码流程图与调试工具说明便于系统查阅与对照实践。已有131人学习浏览适合希望深入framework层与HAL交互细节的研发人员。文档从全景、夜景、HDR等常见模式切入逐步拆解Google官方demo、3A模式、元数据控件和运动跟踪并给出ADB TAG、底层调试工具、dumpsys等常用方法同时对比Camera1与Camera2、新版架构变化并结合HAL3开发要点、多摄像头性能和功耗分析让读者能按图索骥地掌握Camera2 API使用、HAL调优与相机应用优化技巧。整体结构清晰、层层递进对调试和优化思路也有完整梳理。1. Android相机系统架构一条从硬件到像素的完整链路刚开始接触Android相机开发的人往往会被一堆命名绕晕CameraManager、CameraDevice、CameraCaptureSession、CaptureRequest……再加上厂商定制化的各种“增强模式”很容易让人误以为Android相机是一个黑盒只能照着官方Demo抄抄完了还不一定能跑起来。但实际上Android相机系统是一个设计得非常清晰、多层次协作的架构。从你按下快门那一刻到最终屏幕上的预览帧或JPEG图片中间经过了App进程、系统服务、硬件抽象层HAL三大层级的协同工作。我建议所有做相机相关开发的人先不要急着去看API的具体用法而是先把这条链路在脑子里画出来。这里的核心关键词是“Google Camera API2”也就是我们常说的Camera2 API。它是Android从5.0Lollipop开始正式引入的相机接口体系取代了老旧且设计混乱的Camera1 API。如果你想做专业级的相机应用比如手动曝光、RAW输出、多摄协同绕不开这套框架。这篇文章适合三类人一是刚要从0开始做Android相机应用的朋友二是已经在做图片、扫码、直播类项目偶尔需要调相机的开发者三是对Android系统架构感兴趣想搞清楚上层API和底层硬件之间关系的技术爱好者。2. 系统架构全景从App到Sensor的每一层都在干什么1.1 分层架构应用层、Framework层、Service层、HAL层先给你一个整体分层图景记住这张图后面所有的调用流程都是在这条链路上进行的。最上层是App应用层。在这一层我们直接面向的API集合包括CameraManager、CameraDevice、CameraCaptureSession以及各种Surface等。这里负责的事情很简单发指令、收数据。指令包括打开相机、配置输出流、提交请求数据包括预览帧、照片帧、元数据。第二层是Framework层位于应用和系统服务之间。这一层主要完成权限校验、参数校验和Binder封装。当你调用CameraManager的openCamera方法时应用进程并不会直接和相机驱动打交道而是通过Binder IPC把请求发给系统服务。第三层是Camera Service相机服务它运行在独立的系统进程中cameraserver。这一层是真正的“大脑”负责维护摄像头设备列表、管理打开状态、协调多个进程的相机访问权限还要负责将上层请求转换为底层HAL能理解的格式。最底层就是Camera HALHardware Abstraction Layer这是厂商高通、联发科、华为等需要实现的硬件抽象层接口。HAL直接控制Sensor、ISP图像信号处理器、镜头马达等物理器件。Google定义了标准的HAL接口Camera HAL3厂商按照这套规范去实现自己的驱动逻辑。对上层开发者来说HAL的细节是透明的你只管发需求。1.2 为什么Camera2 API能杀进历史舞台你可能会好奇Camera1用得好好的为什么Google要大费周章搞一套Camera2核心原因是Camera1的架构设计存在致命缺陷它把所有操作都堆在一个Camera对象上包括预览设定、图片参数、对焦方式且几乎没有任何帧级别的控制能力。在Camera1里你对每一个画质参数的控制非常有限很多能力比如RAW输出、按帧调整曝光时间干脆没有提供接口。Camera2的核心设计理念是“请求-结果”模型Capture Request / Capture Result。你可以把它理解成给相机下一张订单请求里写清楚你想要的输出尺寸、图像格式、曝光时间、ISO、对焦距离、白平衡等参数然后相机按单生产返回一张结果回执Metadata和一组图像数据。这种机制相比Camera1的好处非常明显每次拍照都是可编程的、独立的你可以针对不同光线条件构建不同的请求可以实现多路输出比如同时出预览流和高分辨率的JPEG流还能拿到RAW格式做后期处理。这也是Google所谓的“相机管道Camera Pipeline”概念。补充一点在Android 12之后Google又引入了EffectRenderer、CameraX扩展等新能力但底层架构依然沿用了Camera2这套设计所以把Camera2搞透受益期会很长。3. Camera2 API核心概念七件套必须了然于心2.1 核心对象与职责分工Camera2涉及到的主要类我习惯把它整理成“七件套”。你要在做项目前把它们各自的职责弄清楚不然写代码的时候很容易在“该用哪个类”这件事上卡壳。类名职责生命周期CameraManager相机“接线员”负责枚举设备、打开设备全局单例风格CameraCharacteristics相机静态能力描述比如支持的分辨率列表只读CameraDevice已经打开的一个摄像头逻辑设备由openCamera回调获得CameraCaptureSession输出流的集合一个“会话”由createCaptureSession创建CaptureRequest一次拍摄请求指定目标Surface和参数构建后提交CaptureResult每一次请求的返回元数据异步回调ImageReader用于接收YUV/JPEG/RAW等图像数据的缓冲区配置时创建这么说可能有点抽象我用岗位来比喻一下。CameraManager相当于前台接待你问它“咱们公司有几个摄像头”它帮你查getCameraIdList、getCameraCharacteristics。你说“我要找2号摄像头”它帮你呼叫openCamera。CameraDevice就是被叫到的那个2号摄像头它是一个已经接通了的设备对象。CameraCaptureSession是会议室你把预览的Surface和拍照的ImageReader Surface都放进会议室。CaptureRequest就是你的会议议程接下来用什么参数拍什么内容。每个议程执行完了CaptureResult就是会议纪要记录实际使用的曝光、对焦状态等。2.2 请求队列机制与帧准同步Camera2的“请求-结果”模式里有一个非常关键但容易被忽视的机制请求是串行排队的。你可以把CaptureRequest想象成一条流水线上的工单。相机硬件一次只能处理一帧但上层可以连续不断地提交多张工单硬件会按顺序逐一执行。底层每处理完一帧就会回调一个CaptureResult同时把对应的图像数据写到你在请求里指定的Surface上。这意味着你可以同时提交“预览帧请求”TargetSurfaceView和“拍照请求”TargetImageReader而不需要等预览完全停止。这就是与老式Camera1最大的不同点也是实现“零黑屏拍照”的基础。但这里有一个细节要注意同一时刻普通类型的请求不能过度堆积否则会导致预览延迟。Google建议控制in-flight的请求数不要太多。你可以通过请求回调里的延迟信息如SENSOR_TIMESTAMP来评估当前管线的负载。4. 完整调用流程实战从0到1跑通Device-Session-Request3.1 权限和准备工作在代码阶段之前先把准备工作做全。这部分的坑非常多尤其是在不同Android版本上的表现差异很大。CAMERA权限需要在Manifest中声明这是基础操作uses-permission android:nameandroid.permission.CAMERA /注意如果你的App只声明了权限但没有做运行时申请在Android 6.0API 23及以上会直接闪退。所以动态申请逻辑必须写上。我自己惯用的做法是在Activity的onCreate里判断并申请if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); }到了Android 10API 29系统加入了摄像头隐私开关用户可以单独关闭某个应用的摄像头访问权限。这意味着即使你之前拿到了运行时权限用户仍然可能在系统设置里把相机权限关掉。因此在实际场景中每次打开相机前都建议补充一个权限可用性检查而不仅仅是启动时申请一次。Android 11之后如果用户在弹窗中连续拒绝两次系统会直接不再显示请求弹窗需要引导用户到设置页手动开启。这也是一个很容易踩中的体验问题。3.2 枚举摄像头与打开CameraDevice准备工作完成后第一步是找出设备上有哪些摄像头以及它们的能力如何。public void openCamera(Context context, String cameraId) { CameraManager manager (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); try { // 检查相机权限 if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { return; } // 获取设备特征判断是否支持请求能力 CameraCharacteristics characteristics manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); // 打印可用的预览尺寸和拍照尺寸 Size[] jpegSizes map.getOutputSizes(ImageFormat.JPEG); Size[] previewSizes map.getOutputSizes(SurfaceTexture.class); manager.openCamera(cameraId, stateCallback, null); } catch (CameraAccessException e) { e.printStackTrace(); } }这里我补充一个重要心得不要在拿到cameraId列表后盲目选第一个。前置、后置、广角、潜望……不同设备上的ID编排并无统一标准。最稳妥的做法是遍历getCameraIdList()通过CameraCharacteristics.LENS_FACING判断是前置还是后置。另一个常见的坑是异常类型CameraAccessException会在相机被其他应用占用或者系统相机服务异常时抛出必须做捕获处理否则就是崩溃现场。openCamera方法是异步的状态通过CameraDevice.StateCallback回调返回。你真正能使用的CameraDevice是在onOpened里拿到的那一个private CameraDevice.StateCallback stateCallback new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice cameraDevice) { // 保存设备对象开始创建Session camera cameraDevice; createCameraSession(); } Override public void onDisconnected(NonNull CameraDevice cameraDevice) { cameraDevice.close(); } Override public void onError(NonNull CameraDevice cameraDevice, int error) { // 根据错误码做相应处理 cameraDevice.close(); } };3.3 创建CameraCaptureSession输出流的配置细节拿到CameraDevice后下一步是创建CameraCaptureSession。这一步的核心是把你想要输出的所有Surface列出来告诉系统“我要把这些数据发到这些地方”。常见的输出流大致有两类一类是预览用的Surface通常是SurfaceView的Surface或TextureView的SurfaceTexture另一类是拍照用的ImageReader生成的Surface。private void createCameraSession() { // 1. 为预览创建SurfaceTexture SurfaceTexture texture textureView.getSurfaceTexture(); texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); Surface previewSurface new Surface(texture); // 2. 为拍照创建ImageReader imageReader ImageReader.newInstance(jpegSize.getWidth(), jpegSize.getHeight(), ImageFormat.JPEG, /*maxImages*/ 2); // 3. 将两个Surface放入集合 ListSurface surfaces Arrays.asList(previewSurface, imageReader.getSurface()); try { camera.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { Override public void onConfigured(NonNull CameraCaptureSession session) { // 会话配置成功才能开始预览请求 captureSession session; startPreview(); } Override public void onConfigureFailed(NonNull CameraCaptureSession session) { // 配置失败检查Surface尺寸是否合法、是否被复用 } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } }这个阶段最容易出问题的点是Surface的尺寸、格式与设备实际支持的能力不匹配。比如你在ImageReader里指定了ImageFormat.RAW_SENSOR但是设备Sensor并不支持输出RAW那Session配置就会直接失败。因此最稳妥的做法是先从StreamConfigurationMap中取出设备支持的尺寸列表再选择一个合适的尺寸去创建Surface不要拍脑袋定尺寸。另外有一个隐藏版本坑在Android 7.0API 24之前createCaptureSession采用列表方式传Surface集合从API 28开始系统还提供了SessionConfiguration方式可以指定执行线程和session类型。在Android 12以上部分设备对老接口的兼容行为不一致我建议新项目直接用SessionConfiguration方式运行时基本无碍。3.4 提交CaptureRequest预览与拍照的切换逻辑Session配置成功后就到了最常用也是最核心的步骤提交CaptureRequest。预览的请求模式是REPEATING。它表示相机硬件持续按该请求的参数输出帧直到你调用stopRepeating为止private void startPreview() { try { // 构建预览请求目标Surface是TextureView对应的Surface CaptureRequest.Builder builder camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(previewSurface); // 可选设置自动对焦模式 builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); // 可选设置自动曝光 builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); CaptureRequest request builder.build(); captureSession.setRepeatingRequest(request, callback, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }拍照的请求模式是单次CAPTURE。它只会执行一次执行完后该帧数据会被写入你指定的ImageReader Surfaceprivate void capturePhoto() { try { // 构建拍照请求目标Surface是ImageReader的Surface CaptureRequest.Builder builder camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); builder.addTarget(imageReader.getSurface()); // 拍照时同步设置对焦、曝光参数避免使用和预览不一致的状态 builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); // 拍照期间暂停预览的连续请求防止画面卡顿 captureSession.stopRepeating(); captureSession.capture(builder.build(), new CameraCaptureSession.CaptureCallback() { Override public void onCaptureCompleted(NonNull CameraCaptureSession session, NonNull CaptureRequest request, NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); // 拍照完成后需要重新开启预览 startPreview(); } }, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }这里有一个新手经常踩到的坑在capturePhoto里你会遇到预览Surface和拍照Surface同时存在的情况。如果你把previewSurface从请求目标里移除预览就会因为收不到新帧而黑屏如果你保留previewSurface同时又添加imageReader的Surface那么拍照那一个瞬间预览流和拍照数据流同时输出。你需要在业务层面做好取舍经验法则是拍照请求保留预览Surface这样可以避免黑屏但代价是拍照瞬间相机模块会同时处理两个输出流对性能有一定要求。所以我在正式项目中一般会做一个小队列管理拍照时将CaptureRequest的Target设为PreviewSurface ImageReaderSurface拍照完成后把ImageReaderSurface从请求里移除恢复纯预览请求。这需要你在Request的Target集合上做动态调整而不是一次性写死。3.5 图像数据获取ImageReader的监听机制拍照完成的数据是异步的通过ImageReader.OnImageAvailableListener回调取出private ImageReader.OnImageAvailableListener onImageAvailableListener new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { Image image reader.acquireLatestImage(); ByteBuffer buffer image.getPlanes()[0].getBuffer(); byte[] bytes new byte[buffer.remaining()]; buffer.get(bytes); image.close(); // 这一步必须做 // 然后进行bitmap解码或保存文件 } };关于这个回调我特别要强调几点。acquireLatestImage和acquireNextImage是有差别的。如果连续拍到多张方式不当会导致内存暴涨。我推荐用acquireLatestImage它会把最近一帧取出来同时释放掉缓冲区中未消费的旧帧效率更高、内存占用更小。另外Image用完之后一定要调用close()释放资源否则ImageReader内部的SurfaceBuffer会被一直占用到达maxImages设定的上限后相机设备会停止填充新帧最终导致拍照卡死。这个坑几乎每个做Camera2开发的都踩过而且是线上才会暴露的问题。5. 常见问题与排查技巧几个能救命的实战经验4.1 典型问题速查表做Camera2开发最让人崩溃的就是问题随机性很强。有些问题在同一台设备上复现换一台就没事了有些问题只在某些特定分辨率下出现。我整理了一份高频问题速查表现象可能原因解决方案onConfigureFailed回调输出流的尺寸/格式与设备能力不匹配从StreamConfigurationMap读取支持列表后选值预览画面方向不对没有处理Sensor方向与显示方向用CameraCharacteristics.SENSOR_ORIENTATION做旋转补偿拍照黑屏拍照请求未包含预览Surface且预览请求被stopRepeating拍照请求中同时添加预览Surface拍照卡顿或无图ImageReader的Image对象未及时close在onImageAvailable中立刻处理并closeActivity重建后相机报错未在onPause中正确释放CameraDevice和SessiononPause里关闭设备onResume重新打开某些设备预览颜色偏色未设置合适的ColorCorrection模式使用CONTROL_MODE_AUTO并开启AE/AWB4.2 方向和画面的适配问题这是中国开发者做相机App最常遇到的问题之一而且它一旦出现就是“功能性Bug”不是细节粗糙的问题。Android手机在设计时没有统一Sensor安装方向所以取景的画面方向和屏幕方向可能不同有的手机摄像头Sensor横向安装有的竖向安装。要让预览画面和屏幕显示方向一致必须获取Sensor方向角然后对预览的Surface做旋转或对TextureView做矩阵变换。一个基础的计算方式是预览方向 (SensorOrientation - 屏幕方向 360) % 360。对于前置摄像头这个值需要加上镜像处理否则你会看到“照镜子”和“对方看到的你”不一致的效果。我见过很多项目在测试机上跑得好好的一上线问题就来了原因就是开发手机恰好是Sensor安装方向和屏幕方向一致的机型。应对的办法是在TextureView的onSurfaceTextureUpdated回调里动态计算旋转角度或者直接用CameraX的PreviewView它会自动帮你处理好这个适配逻辑。4.3 性能优化与设备兼容性心得关于性能和兼容性我分享几点从项目里沉淀出来的经验。第一尽量少创建新Surface。CameraCaptureSession的创建成本非常高耗时可以达到几百毫秒。如果你每次拍照都重新创建Session用户体验会非常差。我推荐的做法是一次创建Session时就把所有可能用到的Surface加进去比如把不同分辨率的ImageReader的Surface都提前声明好运行时切换只需更改Target集合而不必重建Session。第二针对不同设备的Session配置要有兜底策略。部分国产ROM在Camera HAL上做了大量定制有可能在System Camera服务上做过裁剪。如果你的Session配置失败可以先尝试更小的预览尺寸、更常规的ImageFormat比如YUV_420_888或JPEG作为兜底方案再往下走不要一次性把参数开到最大。第三后台线程的Handler不能乱用。createCaptureRequest、setRepeatingRequest、capture这些操作都要求线程模型清晰。Google官方Demo用的是BackgroundThread机制也就是一个单独的非UI线程Handler。不要直接在UI线程里处理所有Camera回调那样你的界面会卡成幻灯片。原理很简单onCaptureCompleted回调里如果做了耗时操作预览帧就会停滞Camera底层并不会帮你缓存太多帧。6. 写在最后一个小建议如果你没有强需求要去操作RAW、手动曝光、双路输出这些“硬核能力”只是想做一个拍照清晰的App我建议你直接考虑CameraX它是Google在Camera2之上封装的Jetpack库用起来省心得多。但如果你要做的是专业相机、直播拍摄、图像算法集成这类项目那Camera2这套调用流程你今天无论如何都要吃透。我自己在实际项目中最大的体会是Camera2的API坑不在于API本身有多难而在于它和硬件、厂商适配绑得太紧。你永远不能假设一台手机上验证通过的逻辑在另一台手机上也能跑所以一定要把获取设备能力CameraCharacteristics这一步看得比写业务流程更重。先把设备的“脾气”摸清楚再去发请求能替你省下大把排Bug的时间。本文还有配套的精品资源点击获取
返回列表