Android多SDK摄像头资源仲裁设计与实战
1. 为什么“两个 SDK 抢一个摄像头”不是 Bug而是 Android 系统设计的必然结果在 Android 开发中当你的 App 集成了视频会议 SDK、扫码 SDK、AR 渲染 SDK 或智能硬件控制 SDK突然发现前置摄像头打不开、预览黑屏、或者调用Camera.open()直接抛出RuntimeException: Fail to connect to camera service——很多人第一反应是“SDK 冲突了”“是不是哪个 SDK 没释放资源”然后开始翻文档、查日志、逐个注释 SDK 测试。我做过不下 12 个带多路视觉能力的工业级 Android 项目从车载 DMS 到 AGV 导航终端再到医疗内窥镜辅助系统几乎每个项目都踩过这个坑。但真相是这不是某个 SDK 的质量问题而是 Android Camera API 架构层面对“独占式硬件访问”的刚性约束所引发的必然仲裁问题。Android 自 Camera1 API 时代起就将摄像头设备抽象为系统级单例资源。无论你用的是android.hardware.Camera还是androidx.camera.coreCameraX底层最终都要通过CameraService向 HAL 层申请设备句柄。而 HAL 层对物理摄像头如/dev/video0的访问控制是排他性的——同一时刻只能有一个客户端持有该设备的 open fd。这就像一台打印机不能同时被 Word 和 Excel 同时发送打印指令摄像头也一样不能同时被扫码 SDK 和美颜 SDK 同时初始化 sensor、配置 ISP 参数、启动 streaming pipeline。更关键的是绝大多数第三方 SDK 并不遵循 Android 官方推荐的“CameraX Lifecycle-aware”实践。它们往往在自己的 Service 或 Activity 中直接调用Camera.open(0)并在onDestroy()或onStop()中才调用release()。一旦 SDK 内部存在异常退出、线程卡死、或未正确监听生命周期那个Camera实例就永远卡在“已打开未释放”状态。此时哪怕你的主 App 调用Camera.getNumberOfCameras()返回 2Camera.open(0)依然会失败——因为设备句柄已被另一个 SDK “锁死”。这也是为什么你在 Logcat 里常看到类似这样的报错E/CameraClient: Cannot connect to camera service E/Camera: Error 2 W/System.err: java.lang.RuntimeException: Fail to connect to camera service这里的 “Error 2” 对应CAMERA_ERROR_SERVER_DIED它根本不是说相机服务崩溃了而是指当前进程尝试获取的 camera device 已被其他客户端独占且该客户端未按预期释放。这是 Android CameraService 在内核层返回的明确拒绝信号。所以“两个 SDK 抢一个摄像头”本质上是一场没有裁判的资源争夺战。而所谓“相机仲裁”不是让 SDK 之间互相协商而是由 App 主动建立一套中心化、可监控、可干预的资源调度机制——把原本散落在各 SDK 内部的相机生命周期收归到一个统一的 Coordinator 手中。这不是锦上添花的优化而是保障多视觉模块共存的基础设施。提示不要试图用try-catch包裹Camera.open()来“绕过”错误。捕获异常只能掩盖问题无法解决资源争抢的本质。真正的解法是让所有相机使用者都向同一个“调度中心”申请和归还资源。2. CameraCoordinator 的核心职责不是“转发调用”而是“状态建模与冲突消解”很多团队在第一次设计相机仲裁时会本能地写一个CameraManager类里面封装open()、startPreview()、takePicture()等方法再把参数原样透传给底层 SDK。这种做法看似简洁实则埋下巨大隐患——它把仲裁逻辑降级成了“API 路由器”完全忽略了相机资源最核心的三个维度设备状态、使用意图、优先级时效。真正健壮的CameraCoordinator必须是一个有状态的协调者它要能回答以下五个关键问题当前摄像头设备如CAMERA_FACING_BACK是否处于IDLE、OPENING、OPENED、STREAMING、ERROR中的哪一种状态哪个模块Module A / Module B / SDK X正在使用它它的使用场景是什么扫码需要高帧率低延迟视频通话需要自动对焦人脸追踪AR 渲染需要精确时间戳如果 Module A 正在扫码Module B 突然发起视频通话请求是立即抢占还是排队等待还是降级为仅使用后置广角副摄当 Module A 因网络超时主动放弃扫码它是否真的调用了release()如果没调用Coordinator 是否能检测到并强制回收如果系统触发了onConfigurationChanged()比如横竖屏切换、或用户切到后台又切回Coordinator 如何保证预览流不中断、不重建 Surface我见过太多 Coordinator 实现只做了第一层“加锁”用synchronized包裹open()方法认为“同一时间只有一个线程能进就不会抢”。但这是典型的线程安全幻觉。Android 的 Camera 调用本身跨进程App → CameraService → HAL锁住 Java 层方法完全无法阻止两个不同 SDK 分别在不同线程、不同进程里同时向 CameraService 发起 open 请求。真正的状态建模必须基于 Binder 通信层的反馈与设备实际运行态。我们目前在产线设备上稳定运行的CameraCoordinator架构采用三层状态机设计层级名称职责关键数据结构L1Device State Layer监听CameraDevice.StateCallback实时同步物理设备真实状态Opened / Closed / DisconnectedAtomicReferenceDeviceStateConcurrentHashMapString, Long记录各模块 lastActiveTimeL2Session Manager Layer为每个使用方创建CameraSession绑定其生命周期、使用策略如SCAN_ONLY,VIDEO_CALL,AR_TRACKING、超时阈值默认 30sCopyOnWriteArrayListCameraSessionPriorityBlockingQueueCameraSession按 priority timestamp 排序L3Policy Engine Layer定义仲裁规则同类型 Session 可复用高优先级 Session 可驱逐低优先级驱逐前执行 graceful shutdown如先 pause preview 再 releaseenum Policy { COEXIST, PREEMPT, DEGRADE }PolicyRule[] rules可动态加载举个真实案例某物流分拣终端需同时运行“条码扫描 SDK”和“AI 货物识别 SDK”。前者要求 60fps、无自动对焦后者要求 30fps、开启 AF/AE。若两者同时请求后置主摄Coordinator 不会简单拒绝第二个请求而是启动DEGRADE策略让扫码 SDK 继续使用主摄同时为 AI SDK 分配副摄OV2640进行粗定位待扫码完成释放主摄后再无缝切换至主摄进行精识别。这种“分级响应”能力远超简单的“谁先到谁用”。注意CameraSession必须携带WeakReferenceActivity或LifecycleOwner而非强引用。否则极易因 Activity 泄漏导致 Coordinator 持有已销毁页面的引用进而阻塞资源回收。我们在 v2.3 版本中曾因此导致整机重启后摄像头永久不可用排查耗时 3 天。3. 从 Camera1 到 CameraX不同 API 层级下的仲裁实现差异与兼容陷阱Android 相机 API 的演进不是平滑升级而是一次次推倒重来的架构重构。Camera1API 1–20、Camera2API 21、CameraXJetpackAPI 21三者底层调用链、生命周期管理、错误恢复机制完全不同。这意味着你的CameraCoordinator如果只适配其中一种就会在混合 SDK 场景下彻底失效。我整理了三者在仲裁设计中最关键的 5 个差异点并附上实测验证过的兼容方案。3.1 Camera1最危险的“裸奔模式”Camera1 是纯 Java 层 APICamera.open()返回一个Camera对象release()必须显式调用。它的致命缺陷在于没有任何生命周期感知能力也没有设备状态回调。SDK 只要拿到Camera实例就可以随意调用startPreview()、setParameters()甚至在SurfaceView销毁后继续往已释放的SurfaceHolder写帧——这会导致SIGSEGV崩溃。更麻烦的是Camera1 的open()是阻塞调用且无超时机制。当设备被占用时它会在 Binder 层无限等待直到TransactionTooLargeException或 ANR。我们曾在一个车载项目中发现扫码 SDK 卡在Camera.open()上长达 8 秒导致整个导航界面卡死。兼容方案必须为 Camera1 封装一层SafeCameraWrapper内部使用HandlerThreadCountDownLatch实现超时控制public class SafeCameraWrapper { private static final long OPEN_TIMEOUT_MS 3000; private final CountDownLatch latch new CountDownLatch(1); private volatile Camera camera; public Camera open(int cameraId) throws TimeoutException { HandlerThread thread new HandlerThread(CameraOpenThread); thread.start(); new Handler(thread.getLooper()).post(() - { try { camera Camera.open(cameraId); } catch (RuntimeException e) { // 记录具体错误码用于后续分析 Log.e(SafeCamera, open failed: e.getMessage()); } finally { latch.countDown(); } }); if (!latch.await(OPEN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { thread.quitSafely(); throw new TimeoutException(Camera.open() timeout after OPEN_TIMEOUT_MS ms); } return camera; } }同时在 Coordinator 的onSessionReleased()中必须调用camera.stopPreview(); camera.release();并置空引用。绝不能依赖 GC 回收——Camera 对象持有 native fdGC 不会自动 close。3.2 Camera2状态驱动但回调地狱Camera2 引入了CameraDevice.StateCallback和CaptureCallback理论上可以精准感知设备状态。但它的复杂度呈指数级上升你需要管理CameraCharacteristics、CameraCaptureSession、CaptureRequest.Builder、TotalCaptureResult……稍有不慎createCaptureSession()就会返回null且无明确错误提示。最大的陷阱在于Camera2 的close()是异步操作且没有回调通知。当你调用cameraDevice.close()后设备并非立即释放而是进入CLOSED状态期间仍可能收到onClosed()回调。如果 Coordinator 在close()后立刻允许新 Session 获取设备就会触发IllegalStateException: CameraDevice was already closed。兼容方案在 Coordinator 中为 Camera2 设备维护一个AtomicBoolean isClosing标志位并在onClosed()回调中清除所有 session 引用private final CameraDevice.StateCallback stateCallback new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice camera) { currentDevice camera; isClosing.set(false); notifyStateChange(DeviceState.OPENED); } Override public void onClosed(NonNull CameraDevice camera) { currentDevice null; isClosing.set(false); // 清理所有待处理的 CaptureRequest pendingRequests.clear(); notifyStateChange(DeviceState.IDLE); } Override public void onClosed(NonNull CameraDevice camera) { // 必须在此处清理 session否则可能残留 activeSessions.removeIf(session - session.getCameraId() cameraId); } };3.3 CameraX最友好但“太友好”反而成坑CameraX 的ProcessCameraProvider.bindToLifecycle()看似完美自动绑定生命周期、自动释放、自动处理配置变更。但它的“自动”恰恰是多 SDK 场景下的最大风险源。因为bindToLifecycle()内部会创建独立的Camera实例和PreviewUseCase不同 SDK 各自 bind等于各自创建了一个 Camera 实例系统层面仍是抢资源。我们曾接入一个 AR SDK它内部使用 CameraXPreviewView而主 App 也用 CameraXImageAnalysis。结果是AR SDK 的 PreviewView 黑屏ImageAnalysis的onOutputSurface()回调永远不触发。Logcat 显示W/CameraX: Camera is busy—— 这正是 CameraX 在底层检测到设备被占用后的静默降级。兼容方案必须禁用所有 SDK 内部的 CameraX 初始化强制它们使用 Coordinator 提供的PreviewView和ImageAnalysis实例。具体做法是在Application.onCreate()中提前初始化ProcessCameraProvider通过ContentProvider或Service将ProcessCameraProvider实例暴露给其他 SDK注意跨进程需用 AIDL要求 SDK 提供setCameraProvider(ProcessCameraProvider)接口由 Coordinator 统一注入所有UseCasePreview/Analysis/Capture均由 Coordinator 创建并管理生命周期。实测心得CameraX 的enableTorch()在多 Session 场景下极易冲突。我们的解决方案是只允许最高优先级 Session 控制闪光灯其他 Session 的setFlashMode()调用会被 Coordinator 拦截并返回false同时记录 warning 日志。这样既避免硬件冲突又让调用方明确感知到权限被接管。4. 真实踩坑全链路复盘从 ANR 到热重启一次完整的相机资源死锁排查去年 Q3我们为某智能巡检机器人交付固件上线第三天客户反馈“设备运行 4 小时后摄像头全部失效重启也无法恢复”。现场抓取 logcat 后发现核心线索只有两行E/CameraService: Disconnecting camera client (pid 1234) due to error -38 W/CameraClient: notifyError E38-38对应BAD_VALUE是 HAL 层返回的通用错误码毫无指向性。接下来 72 小时我们完成了从表象到根因的完整排查链路过程极具代表性这里完整还原。4.1 第一阶段现象锁定与范围收缩首先排除硬件故障。我们用adb shell dumpsys media.camera查看系统级相机状态adb shell dumpsys media.camera | grep -A 5 -B 5 device输出显示Device 0 (id0): State: OPENED Client: com.xxx.robot (pid1234) Stream configs: [PREVIEW: 1280x72030, RECORD: 1920x108030] Device 1 (id1): State: IDLE说明后置主摄device 0确实被com.xxx.robot占用且状态为OPENED但无任何 preview stream 正在运行。这很反常——OPENED状态却无流意味着设备句柄被持有着但无人消费帧数据。接着检查该进程的线程栈adb shell kill -3 1234 # 触发 ANR trace adb shell cat /data/anr/traces.txt | grep -A 20 -B 5 Camera关键线索浮现CameraThread prio5 tid15 Runnable at android.hardware.Camera._startPreview(Native method) at android.hardware.Camera.startPreview(Camera.java:823) at com.xxx.sdk.ScannerCore.startCamera(ScannerCore.java:142) at com.xxx.sdk.ScannerCore$1.onOpened(ScannerCore.java:98)线程卡在startPreview()且堆栈显示是扫码 SDK 的onOpened()回调里。这说明SDK 成功open()了相机但在startPreview()时卡死。而startPreview()是 JNI 调用卡死原因只能是 HAL 层或驱动层。4.2 第二阶段HAL 层日志与驱动状态验证普通logcat无法看到 HAL 层细节需启用vendortag 日志adb shell setprop persist.vendor.camera.hal.debug 3 adb shell setprop persist.vendor.camera.hal.log 1 adb logcat -b vendor | grep -i ov5647\|isp\|stream日志中反复出现E/OV5647: stream_on failed: Device or resource busy (-16) E/ISP: Failed to start streaming for sensor 0-16是 Linux 的EBUSY错误直指设备忙。但dumpsys media.camera显示只有com.xxx.robot在用 device 0。难道有隐藏的客户端我们用adb shell lsof | grep video查看/dev/video*的文件描述符占用adb shell lsof | grep video0 # 输出 # camera 1234 1234 12u CHR 81,0 0t0 123456 /dev/video0 # camera 5678 5678 15u CHR 81,0 0t0 123456 /dev/video0果然PID 5678 也在占用/dev/video0。ps -p 5678查得它是com.yyy.aiAI 识别 SDK。但dumpsys media.camera没显示它——因为它用的是 Camera2 API且在onDisconnected()后未正确关闭设备导致 fd 泄漏。4.3 第三阶段根因确认与修复验证我们写了一个最小复现脚本模拟两个 SDK 的行为// SDK A (Camera1) Camera cam Camera.open(0); cam.startPreview(); // 成功 // SDK B (Camera2) cameraManager.openCamera(0, new CameraDevice.StateCallback() { Override public void onOpened(CameraDevice camera) { // 故意不调用 camera.close() } }, null);运行后lsof确认/dev/video0被两个进程持有startPreview()卡死。至此根因明确Camera2 SDK 未正确释放设备导致 fd 泄漏Camera1 SDK 在startPreview()时因设备忙而无限等待最终触发 ANR而 ANR 处理机制又不会主动 kill 卡死的 Camera 线程形成死锁。修复方案分三级紧急止血在 Coordinator 的onSessionAcquired()中增加 fd 检查private boolean isDeviceBusy(int cameraId) { String cmd lsof | grep video cameraId | wc -l; int count executeShell(cmd); // 执行 shell 命令 return count 1; // 除自己外还有其他进程占用 }若检测到 busy则拒绝新 Session并触发forceReleaseAll()。中期加固为 Camera2 SDK 注入WeakReferenceCameraDevice并在 Coordinator 的onTrimMemory()中遍历所有弱引用对已销毁的CameraDevice强制close()。长期治理推动 SDK 厂商升级要求其 Camera2 实现必须在onClosed()回调中确保close()被调用并提供isClosed()接口供 Coordinator 查询。修复后设备连续运行 72 小时无异常lsof显示/dev/video0始终只有 1 个 fd。踩坑总结Android 相机死锁极少由单一 SDK 引起往往是“一个 SDK 不释放 另一个 SDK 不超时 系统无兜底机制”三重叠加。排查时必须跳出 App 层日志深入到lsof、dumpsys media.camera、vendor log三层缺一不可。5. 生产环境落地 checklist从开发测试到 OTA 升级的 12 项硬性要求CameraCoordinator不是写完就能上线的玩具组件它直接关系到设备的核心功能可用性。我们在 5 个量产项目中沉淀出一份覆盖全生命周期的落地 checklist每一条都来自血泪教训必须逐项验证。5.1 开发阶段代码即契约序号检查项为什么重要验证方式1所有 SDK 的相机初始化入口必须通过 Coordinator 的acquireCamera()获取CameraInstance禁止直接调用Camera.open()或CameraManager.openCamera()防止绕过仲裁造成资源竞争静态代码扫描grep -r Camera.open|openCamera --include*.java . | grep -v CameraCoordinator2CameraSession必须包含timeoutMs字段且默认值 ≤ 30000ms30秒超时后 Coordinator 自动release()并回调onTimeout()避免某个 SDK 卡死导致整个相机系统瘫痪单元测试mock 一个永不返回的CameraDevice.StateCallback验证超时后是否触发 release3Coordinator 必须实现androidx.lifecycle.DefaultLifecycleObserver并在ON_DESTROY时调用forceReleaseAll()确保 Activity 销毁时资源彻底释放防止内存泄漏LeakCanary 检测启动 Camera 页面快速旋转屏幕 10 次观察是否有 Activity 泄漏5.2 测试阶段覆盖极端场景序号检查项为什么重要验证方式4模拟“扫码中切后台 → 启动视频通话 → 切回前台”全流程验证预览流是否中断、是否自动恢复多任务切换是 Android 最常见场景也是状态同步最易出错的环节使用adb shell input keyevent KEYCODE_HOMEadb shell am start -n com.xxx/.VideoCallActivity组合压测5同时启动 3 个不同 SDK扫码/AR/录像持续运行 2 小时监控dumpsys media.camera中的Stream configs是否稳定长时间运行会暴露内存泄漏、fd 泄漏、状态不同步等问题自动化脚本每 5 分钟抓取一次dumpsys比对State和Stream configs是否变化6强制 kill 正在使用相机的 SDK 进程adb shell kill -9 pid验证 Coordinator 是否能在 5 秒内检测到并forceRelease()模拟 SDK 崩溃场景检验 Coordinator 的容错能力adb shell ps | grep com.xxx.scanner→adb shell kill -9 pid→adb logcat | grep forceRelease5.3 发布与运维阶段可监控、可回滚序号检查项为什么重要验证方式7Coordinator 必须提供getActiveSessions()接口返回 JSON 格式状态快照含 module name、priority、duration、state并通过adb shell dumpsys xxx.camera暴露运维人员无需看代码即可快速定位是哪个模块占着摄像头不放adb shell dumpsys xxx.camera应输出类似{sessions:[{module:scanner,priority:10,durationMs:12450,state:STREAMING}]}8所有acquireCamera()失败必须记录EventLog包含errorCode、callerPackage、stackTraceHash并上报到远程监控平台快速定位高频失败模块为 SDK 升级提供数据支撑查看监控后台筛选camera_acquire_failed事件按callerPackage分组统计9OTA 升级包中CameraCoordinator的版本号必须写入build.prop如ro.camera.coordinator.version2.4.1且升级脚本需校验旧版本 ≥ 2.3.0 才允许覆盖防止低版本 Coordinator 覆盖高版本导致新特性丢失adb shell getprop ro.camera.coordinator.version在升级前后对比5.4 硬件适配阶段拒绝“一套代码走天下”序号检查项为什么重要验证方式10针对不同 SoC高通/瑞芯微/全志必须提供CameraHalAdapter封装setParameters()中的 vendor-specific 参数如qcom.hw.ae.target、rkisp.awb.mode同一Camera.Parameters在不同 HAL 下含义不同硬编码会导致预览偏色、对焦失效在 RK3399 板子上运行验证setFocusMode(continuous-video)是否生效在骁龙 865 上验证setRecordingHint(true)是否降低功耗11对双摄/三摄设备Coordinator 必须支持logicalCameraId并能根据CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA自动选择主摄或融合模式避免在多摄设备上错误使用副摄导致扫码失败或画质下降adb shell dumpsys media.camera | grep logical确认是否识别到 logical camera group12所有Surface创建SurfaceView.getHolder().getSurface()、ImageReader.getSurface()必须通过 Coordinator 的createSurface()统一创建并添加Surface#isValid()检查防止 SDK 使用已销毁的 Surface导致IllegalArgumentException: surface is invalid在SurfaceView的onDetachedFromWindow()中调用Coordinator.destroySurface(surface)再触发acquireCamera()验证是否抛出预期异常这份 checklist 我们已固化为 Jenkins 流水线中的 gate check。任何一项不通过CI 就会阻断发布。它不是束缚开发的枷锁而是保护用户不被“摄像头突然失灵”困扰的最后一道防线。最后分享一个小技巧在CameraCoordinator的onSessionAcquired()回调中加入一行Debug.waitForDebugger()仅 Debug 版本。当某个 SDK 死活拿不到相机时你可以 attach debugger直接看到是哪个模块在acquire()时被阻塞以及当前所有 active sessions 的完整状态——这比看 1000 行 log 有效得多。