ARTICLE DETAIL

资讯详情

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

Android UVCCamera stopPreview崩溃根因分析与安全释放实践

Android UVCCamera stopPreview崩溃根因分析与安全释放实践 1. 项目背景与问题现象做Android USB摄像头应用开发的朋友一定对UVCCamera这个库不陌生。它是目前Android平台上最主流的UVCUSB Video Class摄像头接入方案底层基于libusb和libjpeg做了JNI封装让应用层能够通过USB Host模式直接驱动免驱摄像头、内窥镜、USB显微镜这类外设。我最早接触它是做一款工业平板上的外接摄像头检测工具后来又陆续在医疗内窥镜、车载视频采集、扫码硬件等项目里跟它打交道。整体来说设备接入、预览、拍照这些流程都比较顺唯独一个高频问题困扰了我很长时间——调用stopPreview()时各种崩溃、闪退日志还经常指向Native层。这个崩溃的诡异之处在于不是必现有时候连续进退出几十次都没事有时候第一次退出就崩了不是机型相关手头几台标配测试机都能复现但概率和堆栈各不一样不是单点问题既有Java层空指针也有JNI的SIGSEGV还有直接Abort的。后来我只能把问题拆开从调用链、权限管理、生命周期、USB热插拔几个角度逐个排查才算把这类崩溃的规律摸清楚。如果你正在做Android UVC外设相关开发或者准备用UVCCamera接入USB摄像头这篇内容应该能帮你省下不少调试时间。我会从崩溃现场讲起把根因拆开再给出一套可以直接落地的安全释放方案最后补充一些疑难问题的定位技巧。尤其是那些“重启之后好了过几天又崩”的玄学问题大概率也能在下面的内容里找到答案。2. 崩溃现场stopPreview到底是怎么崩的2.1 三类高发崩溃形态把网上能搜到的、以及我自己实测遇到的stopPreview崩溃日志归类基本就是下面三种形态。崩溃类型典型日志触发场景Java层空指针NullPointerException: Attempt to invoke virtual method boolean com.serenegiant.usb.UVCCamera.stopPreview() on a null object referencemUVCCamera为null时直接调用非法状态异常IllegalStateException: Couldnt stop preview. statusUSB设备已断开、相机未open成功就调用Native层崩溃A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x0JNI层访问了已释放的usb资源或SurfaceTexture第一种最常见很多人在onDestroy里不做判空就直接写mUVCCamera.stopPreview()但退出时camera helper已经先一步把mUVCCamera置空了。第二种多多少少和UVC设备的UVCCamera内部状态有关底层libuvc返回非0状态码Java层只记了日志但没做处理最终在Native往下走时崩掉。第三种最头疼日志里看不到Java层堆栈只有一段Native crash信息看起来像是系统底层问题实际上十有八九是上层释放顺序不对导致的。2.2 一次典型的崩溃复现用官方Demo做基准设备是某款基于RK3566的Android 11工控平板外接一个免驱USB摄像头。操作流程是正常打开预览然后直接按返回键退出页面。Override public void onDestroy() { super.onDestroy(); mUVCCamera.stopPreview(); mUVCCamera.destroy(); }看起来没问题对吧实际上崩溃日志长这样E/UVCCamera: stopPreview: status-5 A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0问题就出在status-5上面这是libuvc返回的错误码对应的含义是资源不可用。底层流没正常停止紧接着调用destroyNative层直接访问了一块失效的内存于是SIGSEGV。这说明stopPreview这个动作远远不是“Java层调一个方法”那么简单它背后牵扯到USB传输通道、SurfaceTexture、底层缓冲队列等多个资源的协同释放。3. 根因拆解为什么会偏偏在stopPreview崩溃3.1 UVCCamera的预览机制简述要理解stopPreview为什么容易崩先要搞清UVCCamera的预览链路。USB摄像头通过USB管线传输视频数据UVC协议里分为控制接口和流接口。UVCCamera库做的事情本质上是通过控制接口设置视频格式参数然后从流接口读取等时传输Isochronous数据再把原始数据交给SurfaceTexture或回调处理。预览的Java调用顺序一般是// 打开设备 mUVCCamera.open(usbDevice); // 创建预览 mUVCCamera.setPreviewSize(1280, 720); mUVCCamera.setPreviewTexture(mSurfaceTexture); mUVCCamera.startPreview();setPreviewTexture这一步非常关键它把Camera和SurfaceTexture绑定在一起。一旦SurfaceTexture在释放时先于Camera的Native层或者绑定关系已经失效stopPreview去访问这块GPU纹理资源就会撞上要么空指针、要么SIGSEGV的坑。3.2 stopPreview的JNI调用链看一个简化版的JNI层逻辑JNIEXPORT jint JNICALL Java_com_serenegiant_usb_UVCCamera_nativeStopPreview(JNIEnv *env, jobject thiz) { int result 0; UVCContext *context getContext(env, thiz); if (context context-stream) { result uvc_stop_streaming(context-stream); } return result; }重点是最后那行uvc_stop_streaming。libuvc在停止流传输时要做的事情包括关闭等时传输管道、回收URB缓冲、通知底层驱动停止数据读取。如果USB设备已经物理断开或者底层UsbDeviceConnection被系统回收这些操作就会操作一个非法地址Native层直接崩掉。更尴尬的是很多崩溃其实不是stopPreview本身第一现场而是它在释放资源的瞬间把之前积压的问题引爆了。3.3 五个常见的深坑我在项目中陆续踩到的根因归纳起来是下面五类。第一类是生命周期不对齐。UVCCamera的释放逻辑和Activity/Fragment的销毁时机没有对齐。比如Fragment的View已经销毁SurfaceTexture没了但Camera还在跑或者onDestroy里释放一次系统又帮你释放一次形成重复释放。第二类是USB设备热插拔未处理。很多工控场景里用户直接拔掉USB摄像头或者USB接触不良导致设备断开此时上层没有及时监听UsbManager.ACTION_USB_DEVICE_DETACHED后续stopPreview必然踩坑。第三类是SurfaceTexture提前被回收。这个是内存压力下的常见问题。退出页面前某些View的onDetachedFromWindow里提前调用了SurfaceTexture.release()Camera的Native层还拿着这个纹理引用stopPreview再走一遍释放直接就Segfault。第四类是CameraHelper的内部状态没判断。UVCCameraHelper封装了camera的开关但很多开发者只记住了stopPreview()方法名没有判断helper内部是否真的初始化成功。比如权限没授权、设备没open成功内部mUVCCamera还是null或者处于未知状态这时候调用任何方法都可能抛异常。第五类是高版本Android权限变化。Android 12以上对USB设备访问的权限管理有调整有些定制ROM还会在后台自动回收USB设备连接。如果targetSdkVersion调整后没做对应策略表面上看起来还是老代码实际行为已经完全不同。4. 修复方案一套可以直接落地的安全释放模板4.1 最小改造判空、加锁、吞异常解决崩溃的第一个动作是让释放代码“皮实”起来。所谓皮实就是任何一步出错都不能让崩溃扩散。我最终使用的这套代码经历过多轮重构每一步都有明确目的。public class UvcCameraSafeManager { private static final String TAG UvcCameraSafeManager; private final Object mReleaseLock new Object(); private UVCCameraHelper mCameraHelper; private boolean mReleased false; /** * 安全停止预览并释放所有资源 * 整个释放过程加锁避免多个线程同时进入 */ public void safeStopPreviewAndRelease() { synchronized (mReleaseLock) { if (mReleased) { return; } mReleased true; // 1. 先停预览捕获所有可预期异常 try { if (mCameraHelper ! null) { mCameraHelper.stopPreview(); } } catch (Throwable t) { Log.e(TAG, stopPreview failed: t.getMessage()); } // 2. 关闭设备 try { if (mCameraHelper ! null) { mCameraHelper.closeCamera(); } } catch (Throwable t) { Log.e(TAG, closeCamera failed: t.getMessage()); } // 3. 解绑USB监听 try { if (mCameraHelper ! null) { mCameraHelper.unregisterUSB(); } } catch (Throwable t) { Log.e(TAG, unregisterUSB failed: t.getMessage()); } } } /** * Camera打开成功后将helper设置进来 */ public void setCameraHelper(UVCCameraHelper helper) { synchronized (mReleaseLock) { mCameraHelper helper; mReleased false; } } }我在这里有两个设计考量一是用Throwable而不是Exception来捕获因为Native层崩溃前有时会先抛一个Error用Exception会兜不住二是增加mReleased标志位防止重复释放。4.2 用Lifecycle组件绑定生命周期靠开发者记得在onDestroy里调用释放方法永远会有遗漏。Android提供了现成的Lifecycle组件可以让释放逻辑跟着Activity/Fragment的生命周期自动走。public class MainActivity extends AppCompatActivity { private UvcCameraSafeManager mCameraManager; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCameraManager new UvcCameraSafeManager(); getLifecycle().addObserver(mCameraManager); } }接下来把释放逻辑放进LifecycleObserverpublic class UvcCameraSafeManager implements LifecycleObserver { // ... 上述方法 ... OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void onStop() { // 页面不可见时先停预览保留设备连接便于返回时快速恢复 stopPreviewOnly(); } OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) public void onDestroy() { safeStopPreviewAndRelease(); } }这里有一个开发中值得参考的小技巧ON_STOP只停预览、不释放设备。因为很多时候用户只是点了Home键或者切到了别的页面等回来还要继续用摄像头。如果你在onDestroy才一次性释放次数一多USB设备的重新打开是比较耗时的。实测下来分离这两个阶段能明显降低重连失败概率。4.3 USB热插拔处理别让设备断开打你个措手不及USB设备被物理拔掉的那一刻底层UsbDeviceConnection已经失效但上层Camera对象还认为自己在正常工作。这时候调用stopPreview基本就是撞枪口。正确做法是注册一个USB设备断开广播在设备断开时先把Camera资源清理干净。private final BroadcastReceiver mUsbReceiver new BroadcastReceiver() { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device ! null) { Log.w(TAG, USB device detached: device.getDeviceName()); // 这里不要做耗时操作把释放丢到主线程 mCameraManager.safeStopPreviewAndRelease(); } } } };注册时机放在Camera要开始时注销时机放在释放Camera资源时IntentFilter filter new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED); registerReceiver(mUsbReceiver, filter);这样处理之后设备热拔插导致的stopPreview崩溃基本被拦住了。另一个容易被忽视的点是断开广播触发时UI线程可能正在执行某个SurfaceView等相关的绘制逻辑直接在onReceive里做Camera释放偶尔会引发并发问题。所以我在注释里特意标了“丢到主线程”执行实际推荐用Handler或者主线程Handler.post()来调用。4.4 正确的启动、释放时序很多崩溃表面上在stopPreview实际是启动顺序就埋了雷。一个完整的连接预览流程如下private void startUvcCamera(UsbDevice device) { // 1. 初始化helper绑定USB监听 mCameraHelper UVCCameraHelper.getInstance(); mCameraHelper.initUSBMonitor(this, new UVCCameraHelper.OnMyDevConnectListener() { Override public void onAttachDev(UsbDevice device) { // USB摄像头插入回调 if (mCameraHelper ! null) { mCameraHelper.requestPermission(0); } } Override public void onDettachDev(UsbDevice device) { // 设备断开回调这里做安全释放 mCameraManager.safeStopPreviewAndRelease(); } Override public void onConnectDev(UsbDevice device, boolean isConnected) { if (isConnected) { // 3. 设备连接成功后再开预览 startPreviewInternal(); } } Override public void onDisConnectDev(UsbDevice device) { // 连接断开回调 mCameraManager.safeStopPreviewAndRelease(); } }); mCameraHelper.registerUSB(); mCameraManager.setCameraHelper(mCameraHelper); } private void startPreviewInternal() { if (mCameraHelper null || !mCameraHelper.isCameraOpened()) { return; } // 检查SurfaceTexture是否有效必要时重建 if (mSurfaceTexture null) { mSurfaceTexture new SurfaceTexture(10); } mCameraHelper.startPreview(mSurfaceTexture); }释放时严格遵循“先停预览、再关设备、再注销监听”的顺序。这个顺序不是拍脑袋定的是因为libuvc在停止流传输时需要用到UsbDeviceConnection如果先关设备底层连接就断了再停流传输必然出问题。4.5 引入Google官方USB权限回调USB权限其实也是stopPreview崩溃的高频源头之一。很多定制ROM上权限弹窗还没点用户就切了页面onDestroy触发stopPreview而底层UsbDeviceConnection还没有权限或不完整。Google推荐在申请权限回调成功后再建立连接这个逻辑应该严格执行。mUsbManager.requestPermission(device, mPermissionIntent);申请结果通过PendingIntent回传一定要在onNewIntent里把授权结果接住并且只有授权成功才允许后边的startPreview逻辑运行。这样能避免“半初始化的Camera对象”被stopPreview直接命中的情况。5. 疑难排查日志、Native崩溃和兼容性问题5.1 快速定位logcat里最关键的几行遇到崩溃先别急着改代码。我的习惯是抓取完整logcat重点关注三处UVCCameratag下的日志会带stopPreview: statusxxx这样的返回码libusb或者libuvctag下的Native日志DEBUGtag下的tombstone信息如果看到stopPreview: status-5说明USB管道已经失效优先检查设备是否断开、权限是否还在。如果看到status0但后面还是崩了多半是SurfaceTexture或内存缓冲的问题。把status作为首个排查线索能大幅缩小定位范围。5.2 Native崩溃的堆栈还原Native崩溃日志只有一行地址信息时用addr2line工具可以还原出对应的C/C函数名称。# 以arm64设备为例 # 从崩溃日志中找类似 # Abort message: JNI DETECTED ERROR IN APPLICATION: use of deleted local reference ... # 或者 # signal 11 (SIGSEGV)code 1 (SEGV_MAPERR)fault addr 0x0 # 用ndk自带工具链中的addr2line转换 $NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-androidadb2line -f -e app.so 0x0000000000abc123不过纯Native崩溃往往只能定位到libuvc的uvc_stop_streaming这一类函数到这个层级基本可以确定根因不在Native层而是Java层调用时机有误。5.3 设备兼容性测试清单UVC设备兼容性是这个领域躲不开的话题。我在项目里维护了一张测试矩阵每台新设备进来先跑一遍省得后面线上崩溃看不懂。设备类型测试重点常见问题品牌免驱摄像头分辨率、帧率切换高分辨率下带宽不足stopPreview偶发异常工业内窥镜长时间连续预览内存泄漏累积触发SurfaceTexture回收USB显微镜手动对焦时反复预览频繁start/stop异步线程竞争车载摄像头点火启动瞬间设备枚举慢设备未就绪时调用API状态判断失效这个表里每一条我都在实际项目里遇到过对应崩溃。做USB摄像头应用千万不要只在一台设备上验证通过就以为自己搞定了不同UVC设备的枚举速度、带宽占用、驱动兼容性天差地别。5.4 兜底方案重启摄像头通道即使上面全部处理完仍可能遇到极低概率的Native层崩溃。这时候我给项目加的最后一层保护是摄像头通道自恢复机制。具体做法是启动一个定时任务每隔30秒检查Camera状态如果发现预览线程已死或者底层连接失效自动走一遍完整的安全释放然后重新打开设备。这个方案在无人值守的工控设备上尤为实用能有效降低现场维护成本。// 用Handler做一个简单的状态巡检 private final Runnable mCameraHealthCheck new Runnable() { Override public void run() { if (mCameraHelper ! null mCameraHelper.isCameraOpened()) { if (!isPreviewThreadAlive()) { Log.w(TAG, Preview thread dead, restart camera...); mCameraManager.safeStopPreviewAndRelease(); // 重新初始化摄像头 initCameraAfterDelay(); } } mHealthHandler.postDelayed(this, 30_000); } };不过要提醒一句设置巡检任务前先明确业务场景真的需要7x24小时运行如果是普通App不需要这么重的保护。毕竟定时任务本身也可能带来额外的资源开销和复杂度。6. 个人实操心得跑了几轮实验下来我对stopPreview崩溃这件事最大的体会是它不是单一原因造成的而是生命周期管理、USB状态判断、Native资源释放顺序这几个维度上的小隐患叠加后的爆发点。我个人的开发习惯是所有Camera相关操作都走一个独立的Manager类收口不把UVCCamera对象直接暴露给Activity或Fragment所有释放操作都在锁里做并且用Throwable级别捕获异常所有USB设备断开事件都监听并同步清理资源。这样做虽然多写了一些模板代码但项目跑到量产阶段反馈回来的崩溃率确实降得很明显。再分享一个平时不太好发现的细节如果你的App里有高清预览需求比如1280x720甚至1080p建议在stopPreview之前先把SurfaceTexture上残留的buffer清掉或者干脆在startPreview和stopPreview之间不做频繁的SurfaceTexture复用。实测在高分辨率下旧的SurfaceTexture释放和新纹理绑定之间的竞态是偶发Native崩溃的一个重要诱因。遇到这种场景新建一个SurfaceTexture再去startPreview比复用旧纹理要稳得多。这套方案我后来在多个基于UVCCamera的项目里复用过针对不同品牌的UVC摄像头和Android版本核心思路都没变过。如果你还在被stopPreview崩溃折磨建议按这条链路逐级排查大概率能把问题按死在很早的阶段。
返回列表