ARTICLE DETAIL

资讯详情

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

UVCCamera stopPreview崩溃:Native层SIGSEGV与生命周期修复实践

UVCCamera stopPreview崩溃:Native层SIGSEGV与生命周期修复实践 先说结论如果你在用 saki4510t 的 UVCCamera 库做 USB 摄像头预览遇到了 startPreview 正常、一调 stopPreview 就崩溃闪退的情况九成不是 Java 代码写错而是 UVC 底层释放流程和你 Activity/Surface 的生命周期在抢时间。上周我接手的一个收银机项目就是这样预览视频流稳定退出页面必现闪退Logcat 里只有一行 Fatal signal 11 (SIGSEGV)。这篇文章我会按一条完整的排查链路来写先分清楚你遇到的是 Java 层异常还是 Native 层段错误再讲 stopPreview 到底为什么会崩最后给出我实测有效的状态机化改造方案、Surface 销毁竞态的解法以及拔插、文件分享等边角场景的注意事项。适合正在做 USB 摄像头相关 Android 开发、被 UVCCamera 折磨过的同学参考也适合想搞懂 native crash 该怎么查的人。1. 崩溃现场Java抛异常和Native SIGSEGV是两码事1.1 先分清你遇到的是哪一类崩溃很多人在帖子里说“stopPreview 崩溃”但崩溃分两类处理方式完全不同。第一类是 Java 层异常。现象是 Logcat 里出现AndroidRuntime、FATAL EXCEPTION后面跟着 Java 堆栈。常见的有IllegalStateException、NullPointerException、FileUriExposedException之类。这类问题能 catch、能查堆栈相对好解决。第二类是 Native 层崩溃也就是段错误。现象是 Logcat 里出现类似这样的输出Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x8 Build fingerprint: ... pid: 12345, tid: 12345, name: your.package ... backtrace: #00 pc 0001a2e4 /data/app/.../lib/arm64/libuvc.so (uvc_stop_streaming...) #01 pc 0001b6e0 /data/app/.../lib/arm64/libuvc.so (uvc_stop...) #02 pc 00011f20 /data/app/.../lib/arm64/libjniuvccamera.so (...)这是我最常见到的 UVC stopPreview 崩溃形态。Java 层根本 catch 不住因为崩溃发生在 JNI 之下、libuvc 内部。为什么 stopPreview 会走到 native 层崩因为stopPreview()不是简单的 Java 方法调用它最终经过 JNI 一路向下调用uvc_stop_streaming、libusb_interrupt_transfer、uvc_stream_ctrl这一整套东西。底层任何一个结构体指针状态不对就直接段错误。UVC 相机本质上就是一个外部 USB 设备它的生命周期比你的 Activity 更不可控你退出页面的速度远快于底层 USB 流停止的速度崩溃就产生了。1.2 用 logcat 和 ndk-stack 固定现场遇到 stopPreview 闪退第一件事不是改代码而是把崩溃现场固定下来。抓日志我已经成了习惯动作几行命令就能干完# 抓全量日志过滤出崩溃关键字 adb logcat -v threadtime | grep -E FATAL|AndroidRuntime|DEBUG|libuvc|tombstone # 崩溃发生后导出 tombstone adb root adb shell ls -l /data/tombstones/ adb pull /data/tombstones/tombstone_00 ./tombstone_00拿到 tombstone 之后用 NDK 自带的 ndk-stack 解析ndk-stack -sym ./app/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a -dump ./tombstone_00如果你项目里没有保留带符号表的 so也可以用addr2line直接反查偏移aarch64-linux-android-addr2line -f -C -e ./libuvc.so 0x1a2e4之所以强调先定位崩溃栈是因为我见过太多人一看到 stopPreview 闪退就直接加 try-catch然后发现根本没用。原因很简单Java 层的 try-catch 拦不住 native 层的 SIGSEGV。先把崩溃栈打到 libuvc.so 的哪个函数后面才知道往哪个方向修。1.3 为什么单独搜 stopPreview 这个点“startPreview 没事stopPreview 崩”这个现象本身就说明问题资源的分配start是正向的一层层申请即可资源的释放stop是逆向的任何一个环节的顺序错了或者时机不对就会踩到悬空指针。你可以理解为搬家进场没人管你退租时房东要按流程验房——哪一步没按顺序来就卡那里。所以 stopPreview 崩溃本质上是在问释放流程里有没有竞态、有没有乱序、有没有重复执行。2. stopPreview崩溃的三个高发前提线程、重复调用、Surface生命周期2.1 你在哪个线程调了stopPreview很多人直接在 Activity.onPause 里调用mUVCCamera.stopPreview()这本身没有错但要注意 onPause 跑在 UI 线程。stopPreview 内部要等待 libuvc 的 stream 线程彻底停下来这个过程短则几十毫秒长则几百毫秒。UI 线程被卡住表面看只是掉帧、卡顿但如果你在等待期间系统已经把 SurfaceTexture 释放了等 UI 线程继续执行后面的 destroy 逻辑时底层 handle 已经失效崩溃就出现了。更危险的是在业务线程里同时调 start 和 stop。比如有人用 HandlerThread 做轮询检测到摄像头掉线就调 stopPreview而主线程的 onPause 也在调 stopPreview。这两个线程同时进入 libuvc 的 stream control block轻则状态错乱重则直接 use-after-free。一句话UVC 相关的所有操作最好是同一时间只有一个人在动它。2.2 重复调用stopPreview两连击这是我在代码 review 里看得最多的写法Override protected void onPause() { super.onPause(); if (mUVCCamera ! null) { mUVCCamera.stopPreview(); } } Override protected void onDestroy() { if (mUVCCamera ! null) { mUVCCamera.stopPreview(); // 第二次调用 mUVCCamera.destroy(); mUVCCamera null; } }如果 Activity 从 onPause 走到 onDestroy 中间隔了很久或者某些 ROM 的 Activity 重建机制导致两个回调在一瞬间连续触发这里就出现了 stopPreview 两次调用。原版库内部确实有mIsPreviewing标志位做判断按理说不会重复执行底层逻辑但国内很多 fork 分支改过代码有的甚至把判断条件改漏了。更坑的是拔插回调里的 stop 和页面退出的 stop 发生在不同线程标志位的读写并不是原子操作一样会出问题。我的建议很简单不要在多个生命周期回调里散落调用 stopPreview而是统一收口到一个 controller 里。2.3 Surface生命周期surfaceDestroyed与stopPreview互相扯皮这块是 UVC 崩溃的重灾区也是你排查时最难复现的一类。正常退出页面的时序是这样Activity.onPause - onStop - onDestroy同时 SurfaceFlinger 开始释放 surfaceTextureView 会回调onSurfaceTextureDestroyed(SurfaceTexture surface)。问题在于这几个回调的先后顺序在不同 Android 版本、不同 ROM 上并不是完全一致的。如果你的 onDestroy 里先做了mTextureView null或者把 TextureView 从 ViewGroup 中移除再调用mUVCCamera.stopPreview()那底层拿到的可能是一个已经被系统释放掉的 SurfaceTexture。UVC 底层在 stop 时要释放这个 texture 相关的资源发现指针悬空直接段错误。反过来如果先调了 stopPreview但底层还没来得及释放完SurfaceTexture 就已经被系统销毁同样会崩。这里的本质是Surface 的销毁是一个异步过程而 stopPreview 的 native 实现假设 Surface 还活着。2.4 还有一个最容易忽略的前提预览尺寸和帧格式别笑这个坑我踩过。当时项目里摄像头只支持 YUYV 格式代码里写死FRAME_FORMAT_MJPEGstartPreview 其实失败了但界面显示的是黑屏或者花屏没有直接崩。等页面退出时调用 stopPreview底层尝试清理一个没有完整建立的 stream 结构就崩了。如果你遇到的是“第一次进入正常第二次进入必崩”这种规律性闪退先别急着改线程模型先看预览配置// 打印摄像头支持的分辨率列表 ListSize supportedSizes mUVCCamera.getSupportedSizeList();对比一下你setPreviewSize的宽高和格式确认它确实在支持列表里。摄像头本身的驱动能力都满足不了后面 stop 再稳也白搭。3. 修复一给UVC操作加一个“状态机”把随时可调变成按状态执行3.1 为什么不靠if判空而要上状态机很多人的防御逻辑是if (mUVCCamera ! null) { mUVCCamera.stopPreview(); }判空只能解决“变量不存在”的问题解决不了“对象状态已经错误”的问题。比如 mUVCCamera 这个引用还在但底层 libuvc 上下文已经被 destroy 了你再去调 stopPreview该崩还是崩。状态机的作用是让代码明确知道“当前到底处于什么阶段这一步能不能做”。我设计的状态分五档够用也不用太复杂状态含义能执行的操作IDLE空闲未打开设备open、startOPENING正在打开设备stop取消打开PREVIEWING正在预览stop、destroySTOPPING正在停止等待不重复 stopDESTROYED已销毁无3.2 串行执行器单线程Executor把竞态堵死状态机只是判断真正防止并发还要靠“串行”。我把所有 UVC 操作丢进同一个单线程 Executor保证任意时刻只有一个人在动 libuvc。代码示意如下public class UvcPreviewController { public enum State { IDLE, OPENING, PREVIEWING, STOPPING, DESTROYED } private final Object mLock new Object(); private final ExecutorService mExecutor Executors.newSingleThreadExecutor(); private State mState State.IDLE; private UVCCamera mCamera; private SurfaceTexture mSurfaceTexture; public void requestStart(final SurfaceTexture surfaceTexture) { mExecutor.execute(() - { synchronized (mLock) { if (mState ! State.IDLE) return; mState State.OPENING; mSurfaceTexture surfaceTexture; } try { // 这里根据你自己的 open 流程补齐 // 一定是在串行线程里执行 mCamera new UVCCamera(); // mCamera.open(...); // mCamera.setPreviewSize(1280, 720, UVCCamera.FRAME_FORMAT_MJPEG); // mCamera.setPreviewTexture(mSurfaceTexture); // mCamera.startPreview(); synchronized (mLock) { mState State.PREVIEWING; } } catch (Exception e) { synchronized (mLock) { mState State.IDLE; } } }); } public void requestStop() { mExecutor.execute(() - { synchronized (mLock) { if (mState ! State.PREVIEWING mState ! State.OPENING) return; mState State.STOPPING; } try { if (mCamera ! null) { // 如果之前开过帧回调先关掉 mCamera.setFrameCallback(null, 0); mCamera.stopPreview(); mCamera.destroy(); } } catch (Exception e) { // Java 层异常该吞的吞掉 } finally { mCamera null; mSurfaceTexture null; synchronized (mLock) { mState State.IDLE; } } }); } public void requestDestroy() { mExecutor.execute(() - { try { requestStop(); } finally { synchronized (mLock) { mState State.DESTROYED; } } }); } }注意上面是简化示意接口名以你使用的库版本为准。核心思想是两层状态机判断 单线程串行。requestStop()里我先调setFrameCallback(null, 0)再调stopPreview()这个顺序很关键。如果你开了帧回调却没先关掉stop 的同时底层还在往 Java 层回调帧数据等 Java 层对象开始析构时回调线程还在往里写崩溃概率极高。3.3 stopPreview和destroy要不要合并我见过一些 fork 版本的 stopPreview 只是把 native 流停了并没有释放 USB 资源要真正释放还得调 destroy。这两步如果散落在不同地方中间只要插入一个异步操作就可能出问题。我的做法是把 stop 和 destroy 合在同一个 runnable 里执行中间不做任何等待因为大多数版本的 stopPreview 本身是同步等待 stream 停止的如果确认你的库版本没有同步等待那你需要在 stopPreview 之后加一个短延时100~200ms再 destroy让底层线程把资源吐干净。这里多说一句不要用Thread.sleep阻塞 Executor 线程之外还想着并发。既然已经串行了就在同一个任务里 sleep影响不大。3.4 状态机带来的额外收益上了状态机之后你还会发现一个好处拔插事件的重复回调、页面的重复退出都不会再触发重复 stop 了。因为 requestStop 的第一步做状态判断非 PREVIEWING/OPENING 状态直接 return。这比在外部写十个 if 判断靠谱得多。4. 修复二Surface销毁竞态与拔插事件崩溃最容易复现的场景4.1 surfaceDestroyed回调里不要直接调stopPreviewTextureView.SurfaceTextureListener 的onSurfaceTextureDestroyed(SurfaceTexture surface)是一个很微妙的回调。它被调用时SurfaceTexture 已经处于“正在销毁”的过程中。如果你在这个回调里同步调用 stopPreview等 stopPreview 从 native 层返回SurfaceTexture 可能已经被彻底回收。正确的姿势是把这个回调当成一个“通知”告诉 controller “surface 没了你后面别用这个 texture 了”然后异步执行 stopmTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { // 不能在这里同步 stopPreview mController.requestStop(); return true; } });关于返回值如果你返回 true系统会负责释放这个 SurfaceTexture返回 false则必须自己在合适时机释放。我的习惯是返回 true因为 controller 内部已经不再持有这个 texture 的引用让系统释放最省心。4.2 页面退出时的推荐操作顺序这段时间踩坑总结出的顺序照着做能规避大多数竞态onPause不要立刻 stop只把 UI 状态置为“退出中”如果需要停就调requestStop()异步不阻塞。onStop/onDestroy先把 TextureView 的 listener 置空再把 TextureView 从 ViewGroup 中移除。最后调requestStop()这个 Runnable 内部负责关帧回调、stopPreview、destroy。这样做的逻辑是先把 Java 层和 Surface 的绑定断开避免 UI 线程继续操作 texture再把 UVC 底层资源释放顺序是从上到下不会出现底层还在跑、上层引用已经没了的情况。4.3 拔插事件USB_DEVICE_DETACHED与onDisconnectUSB 摄像头最常见的物理异常就是拔出。当线被拔掉的瞬间底层 libusb 已经感知到设备断开此时再去 stop 一个已经断开的流基本等于访问悬空指针。很多人在广播接收器里直接写// 错误示范 if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { mUVCCamera.stopPreview(); }设备都断开几百毫秒了你才去 stop底层驱动早就把传输队列清掉了。正确做法同样是丢给 controller 异步处理Override public void onDisconnect(USBMonitor.UsbControlBlock ctrlBlock) { // 不要在这里同步 stop mController.requestStop(); }另外UVC 拔插还有一个隐藏问题拔掉之后系统可能立刻发出ACTION_USB_DEVICE_ATTACHED如果你在这里做了自动重连而 controller 的上一个 stop 还没执行完start 和 stop 就撞上了。串行执行器在这里是关键保障——start 请求排在 stop 请求后面等 stop 彻底完成才会执行 start。4.4 和FileProvider/分区存储有关的一个小坑UVC 摄像头项目通常不只是“显示预览”还涉及拍照、录像、文件分享。很多朋友看到 logcat 里出现content://com.xxx.fileprovider/external_path/android/data/com.xxx/...就懵了以为这是 stopPreview 崩溃的报错。这里我把话说透content://开头的是 Android 7.0 之后的 FileProvider 机制用于应用间安全共享文件。你如果还用file://绝对路径去分享图片/视频会直接抛FileUriExposedException这属于另一条崩溃链。但有一条线是交叉的很多项目在 onPause 里既保存文件又停预览磁盘 IO 又走了主线程导致 stop 被卡在 IO 操作后面等真正执行时 surface 已经没了最终依然是 stopPreview 附近的 native 崩溃。所以我的建议是把“保存/分享文件”和“停止 UVC 预览”拆成两条独立任务至少不要在主线程里用 MediaStore 写大文件。FileProvider 的 provider 路径配置也要提前检查好不然content://生成成功、对端解析失败一样闪退。5. 验证与回归怎么确认问题真的没了5.1 崩溃场景压测清单代码改完了别急着收工。按下面这张表逐项跑每项至少 20 轮测试场景操作方式关注点反复进出页面进入预览页 - 返回 - 再进入第二次/第三次进入是否正常快速前后台切换预览中按 Home 回桌面再回 ApponPause/onResume 反复触发预览中拔插 USB预览时直接拔线再插回拔插回调是否触发二次崩溃低内存回收开发者选项开启“不保留 Activity”Activity 重建时资源释放是否干净预览同时保存文件预览中录像/拍照然后立刻退出文件 IO 与 stopPreview 是否抢占5.2 用adb辅助观察真机压测时每轮操作完看一眼有没有新增 tombstoneadb shell ls -l /data/tombstones/或者直接过滤日志adb logcat -d | grep -E Fatal signal|SIGSEGV|libuvc如果之前 20 次必崩现在 20 次通过别急着庆祝。把 App 放到后台等 5 秒再杀进程观察最后这几秒有没有延迟触发的 crash。native 崩溃不一定当场炸有时候会在几秒后的析构阶段才爆出来。5.3 崩溃消失不等于问题修复我要强调一点“不再崩溃”只是充分条件不是必要条件。真正的修复是你能说清楚“为什么之前会崩、现在为什么不会崩”。如果只是把 stopPreview 包进 try-catch 就认为解决了下次换个机型还是会爆。我验证一个修复是否有效会再看两个点stopPreview 调用后操作系统的 USB 事件日志里有没有异常的中断传输错误App 进程是否在退出后 2 秒内被系统正常回收而不是出现“进程存在但没有响应”的状态。这两个点都正常才算真的稳了。5.4 多设备验证的意义不同 SoC 的 USB Host 控制器实现差异很大。高通的 EHCI/xHCI 和联发科、瑞芯微的驱动在断开流时的表现完全不同。同一个项目骁龙平板上 20 次不崩换到某国产 RK3288 收银机上可能第二次就崩。所以条件允许的话至少测两台不同芯片方案的设备尤其是你目标市场上量最多的那些机型。6. 还有几个容易踩的边角坑6.1 setFrameCallback 的开关时机如果你用setFrameCallback做帧数据处理stop 之前一定要先关掉回调。这个我在状态机代码里已经加了。社区里有人遇到 stopPreview 崩溃最后发现是回调线程还在跑而 Java 层已经把自己置空了。回调关掉之后等 1~2 帧的时间再 stop更稳。6.2 老版本libuvc的坑原版 saki4510t/UVCCamera 内置的 libuvc 版本偏老某些 UVC 设备在异常拔插时uvc_stop_streaming会在等待队列上卡住甚至崩溃。这个问题在源码层面需要更新 libuvc 并重新编译 JNI 层。如果项目时间紧优先使用社区维护的 fork比如国内某些团队维护的 AndroidUSBCamera 分支它们一般会升级 libuvc 并处理掉一批已知的 release 竞态。但记住换库之后必须重跑第 5 节的压测清单不能直接上线。6.3 定制ROM和后台限制国产 ROM 对 USB 权限弹窗和后台 Activity 生命周期的处理各不一样。我遇到过一台设备插上摄像头弹权限对话框时Activity 的 onPause 和 onResume 在一秒内被连续触发两次如果你在 onResume 里 start、onPause 里 stop摄像头就被反复软重启崩溃概率极高。这种场景下状态机 串行执行几乎是唯一解。6.4 区分“和库无关”的崩溃最后提醒一句不是所有 stopPreview 时的崩溃都是 UVC 库的锅。如果 tombstone 栈顶是libc.so的memcpy并且你确实开了大尺寸的帧回调 buffer那可能是你的 pixel buffer 分配过大导致底层 memcpy 越界。先看栈顶再怀疑库这能帮你少走很多弯路。这次排查之后我给自己定了一条规矩凡是接外部设备类 SDK第一版代码就必须把设备状态机和管理线程写好不能图省事直接调库。摄像头、打印机、扫码枪这类外设的生命周期比页面生命周期更不可控页面可以“退就退了”外设不行。最后再分享一个小技巧如果时间紧先把 stopPreview 和 destroy 合并到同一个 Runnable用单线程 Executor 跑起来绝大多数 stop 崩溃能立刻压下去但想根治还是要把线程模型和生命周期理清楚否则换个设备或者换个 ROM崩溃随时会回来。
返回列表