Android7 U盘插拔链路源码全解析(八)实战调试与案例分析

系列目录第一篇:全景图与调用链路概览 | 第二篇:内核层---USB驱动与uevent | 第三篇:Native层---vold与NetlinkManager | 第四篇:Framework层(上)---UsbHostManager | 第五篇:Framework层(下)---MountService | 第六篇:广播分发与SystemUI响应 | 第七篇:应用层---MediaScanner与SAF | 第八篇:实战调试与案例分析


一、引言

前面七篇文章沿着"从硬件到应用"的方向,逐层拆解了整条 U 盘插拔链路。本文是系列收官之作,聚焦实战:从调试工具链到典型案例分析,帮助你在面对 "U 盘插入没反应" 这类问题时,能像剥洋葱一样一层层定位。


二、调试工具链

2.1 全线工具全景

复制代码
┌─────────────────────────────────────────────────────────────┐
│ 调试层          工具                    观察什么              │
├─────────────────────────────────────────────────────────────┤
│ 内核层         dmesg / sysfs            USB 枚举日志、设备树  │
│ ─────────────────────────────────────────────────────────── │
│ Native 层      logcat -s Vold           vold 事件日志        │
│                /dev/socket/vold         NDC 通信            │
│ ─────────────────────────────────────────────────────────── │
│ Framework 层   logcat -s                服务回调日志          │
│                dumpsys usb/mount        快照服务状态          │
│ ─────────────────────────────────────────────────────────── │
│ 应用层         logcat -s MediaProvider  MediaScanner 日志    │
│                Settings → 存储          UI 展示确认          │
└─────────────────────────────────────────────────────────────┘

2.2 dmesg ------ 内核层诊断第一入口

bash 复制代码
# 插入 U 盘后立即抓取内核日志
adb shell dmesg | tail -50

# 实时监控内核日志
adb shell dmesg -w

# 只过滤 USB 相关日志
adb shell dmesg | grep -iE 'usb|scsi|sd[a-z]|xhci'

正常插入 U 盘的 dmesg 输出示例:

复制代码
usb 1-1.2: new high-speed USB device number 6 using xhci-hcd
usb 1-1.2: New USB device found, idVendor=0781, idProduct=55a9
usb 1-1.2: Product:  SanDisk 3.2Gen1
usb 1-1.2: Manufacturer:  USB
usb 1-1.2: SerialNumber: 03001911102425174343

关键信息解读:

日志行 含义 对应阶段
new high-speed USB device USB 2.0 高速设备被检测到 Hub 端口检测
idVendor=0781, idProduct=55a9 识别为 SanDisk 设备 枚举完成
Product: SanDisk 3.2Gen1 产品名称 描述符获取

2.3 logcat ------ Framework 层诊断

bash 复制代码
# 观察 vold 日志
adb logcat -s Vold

# 观察 UsbHostManager 日志
adb logcat -s UsbHostManager

# 观察 MountService 日志
adb logcat -s MountService

# 观察 VoldConnector NDC 通信
adb logcat -s VoldConnector

# 一键抓取全线日志
adb logcat -s Vold VoldConnector UsbHostManager MountService StatusBarView USBdiskReceiver | tee usb_debug.log

正常插入 U 盘的 logcat 输出示例:

复制代码
D/UsbHostManager: Added device /dev/bus/usb/001/006 (SanDisk 3.2Gen1)

D/VoldConnector: RCV <- {640 disk:8,0 8}
D/VoldConnector: RCV <- {641 disk:8,0 123009761280}
D/VoldConnector: RCV <- {650 public:8,1 0 "disk:8,0" ""}
D/VoldConnector: SND -> {10 volume mount public:8,1 0 0}
D/VoldConnector: RCV <- {651 public:8,1 2}
D/VoldConnector: RCV <- {200 10 Command succeeded}

D/StatusBarView: onVolumeStateChanged USBCount = 0
D/StatusBarView: >>> setUSB USBCount=1 isUSBExist=true

I/USBdiskReceiver: android.hardware.usb.action.USB_DEVICE_ATTACHED
I/USBdiskReceiver: volume state change : 2 || isUdisk : true

2.4 dumpsys ------ 快照当前状态

bash 复制代码
# 查看 USB 设备状态
adb shell dumpsys usb

# 查看存储卷状态
adb shell dumpsys mount

# 查看当前挂载点
adb shell mount | grep media_rw

2.5 vdc ------ 手动与 vold 交互

bash 复制代码
# 列出所有卷
adb shell vdc volume list

# 列出所有磁盘
adb shell vdc disk list

# 手动挂载指定卷
adb shell vdc volume mount public:8,1

# 手动卸载
adb shell vdc volume unmount public:8,1

三、典型案例:U 盘插入后状态栏图标不消失

3.1 问题现象

U 盘插入后,状态栏正常显示 U 盘图标。但一段时间后(可能由于 USB 总线复位),U 盘被系统重新枚举,应用层(USBdiskReceiver)正确收到了 USB_DEVICE_DETACHED 广播并清理了状态,但状态栏的 USB 图标仍然显示。

3.2 问题定位流程

复制代码
步骤 1:确认内核层正常
┌─────────────────────────────────────────────────────────────┐
│ $ adb shell dmesg | grep -iE 'usb|sd[a-z]'                 │
│                                                             │
│ [良好] 看到 "New USB device found" → 内核层正常               │
│ [注意] 设备地址变化(001/006 → 001/005)说明 USB 总线复位了    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
步骤 2:确认 vold 和 MountService 正常
┌─────────────────────────────────────────────────────────────┐
│ $ adb logcat -s VoldConnector MountService                  │
│                                                             │
│ [注意] 总线复位时,vold 可能没有 NDC 事件输出                  │
│       因为复位不经过 vold 的正常卸载流程                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
步骤 3:检查 USB 广播是否正常发送
┌─────────────────────────────────────────────────────────────┐
│ $ adb logcat -s USBdiskReceiver                             │
│                                                             │
│ [良好] 如果看到 USB_DEVICE_DETACHED 广播 → UsbHostManager 正常│
│ [良好] 如果看到 VOLUME_STATE_CHANGED state=5 → EJECTING      │
│        → 广播通道正常!                                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
步骤 4:检查 StatusBarView 的 StorageEventListener 回调
┌─────────────────────────────────────────────────────────────┐
│ $ adb logcat -s StatusBarView                               │
│                                                             │
│ [问题] 如果看不到任何 onVolumeStateChanged 或 onDiskDestroyed │
│        日志 → StorageEventListener 回调链断裂!               │
│                                                             │
│ 根因:USB 总线复位不走 vold 的 NDC 管道,导致                  │
│       StorageEventListener 的回调不被触发                      │
└─────────────────────────────────────────────────────────────┘

3.3 根因分析

问题的核心在于 两条通知通道的差异

复制代码
USB 总线复位
    │
    ├──→ UsbHostManager (JNI: libusbhost)
    │       │
    │       └──→ USB_DEVICE_DETACHED 广播 ✅
    │            → USBdiskReceiver 正确收到
    │
    └──→ vold
            │
            └──→ NDC 管道 ❌ (复位不经过 vold 卸载流程)
                 → StorageEventListener 回调不触发
                 → StatusBarView 不更新

此外,StatusBarView.isUSBExist() 方法存在设计缺陷:

java 复制代码
// 问题代码
private boolean isUSBExist() {
    final List<VolumeInfo> volumes = mStorage.getVolumes();
    for (VolumeInfo vol : volumes) {
        if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
            if ((vol.getId() != null) && (vol.getDiskId().contains("disk:8"))) {
                return true;  // ★ 不检查 Volume 状态!
            }
        }
    }
    return false;
}

即使 Volume 已经处于 STATE_UNMOUNTEDSTATE_BAD_REMOVAL 状态,只要它还在 getVolumes() 列表中,图标就仍然显示。

3.4 修复方案

方案一:isUSBExist() 增加状态校验

java 复制代码
private boolean isUSBExist() {
    final List<VolumeInfo> volumes = mStorage.getVolumes();
    for (VolumeInfo vol : volumes) {
        if (vol.getType() == VolumeInfo.TYPE_PUBLIC
                && vol.getId() != null
                && vol.getDiskId().contains("disk:8")
                && vol.getState() == VolumeInfo.STATE_MOUNTED) {  // ★ 必须处于已挂载状态
            return true;
        }
    }
    return false;
}

方案二:onVolumeStateChanged 对所有状态变化都刷新图标

java 复制代码
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
    if (vol.getState() == VolumeInfo.STATE_CHECKING) {
        USBCount++;
    }
    setUSB();  // ★ 无论什么状态都刷新图标
    Log.d(TAG, "onVolumeStateChanged USBCount = " + USBCount);
}

方案三(兜底):增加 USB_DEVICE_DETACHED 广播监听

java 复制代码
// 在 createView() 中注册 USB 广播
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);

// 在 receiver.onReceive() 中处理
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
    setUSB();  // 刷新 USB 图标
}

四、常见问题速查表

现象 可能根因 排查方法 修复方向
插入 U 盘完全没反应 供电不足/硬件故障 dmesg 无 USB 日志 检查硬件/供电
识别设备但无法挂载 文件系统不支持 dmesg 中 "unknown filesystem" 集成 exFAT/NTFS 驱动
挂载后卡在"正在检查" fsck 超时 vold 日志 "fsck failed" 增大 fsck 超时或修复 U 盘
通知栏无反应 广播未发出 MountService 日志 检查 MountService 是否正常运行
U 盘图标不消失 StorageEventListener 回调断裂 StatusBarView 日志 增加广播监听或状态校验
应用看不到文件 MediaScanner 未完成 MediaScanner 日志 等待扫描完成或手动触发扫描
U 盘反复断开重连 供电不足/信号完整性 dmesg 中反复 reset 使用带外部供电的 Hub

五、排查流程总结

复制代码
U 盘插入问题排查四步法:

1. 内核层检查
   dmesg | grep -iE 'usb|sd[a-z]'
   → 有 "New USB device found"?→ 硬件层正常,进入下一步
   → 没有?→ 检查供电/接触/USB 模式

2. vold 层检查
   logcat -s Vold VoldConnector
   → 有 "{640 disk:8,0 ...}"?→ vold 正常,进入下一步
   → 没有?→ 检查 uevent 是否被过滤,或 vold 是否启动

3. MountService 层检查
   logcat -s MountService
   dumpsys mount
   → 有 "STATE_MOUNTED"?→ 挂载正常,进入下一步
   → 没有?→ 检查文件系统类型是否支持,或 fsck 是否超时

4. 应用层检查
   logcat -s MediaScanner USBdiskReceiver StatusBarView
   → 检查广播是否送达、图标是否更新、扫描是否完成

六、关键源码文件索引

复制代码
调试工具:
  adb shell dmesg                # 内核日志
  adb logcat -s <TAG>            # 各层日志
  adb shell dumpsys usb          # USB 状态快照
  adb shell dumpsys mount        # 存储状态快照
  adb shell vdc volume list      # vold 卷列表

关键日志 TAG:
  UsbHostManager    # USB 设备感知
  VoldConnector     # NDC 通信
  Vold              # vold 守护进程
  MountService      # 存储挂载服务
  StatusBarView     # 状态栏 USB 图标
  USBdiskReceiver   # 应用层 USB 广播处理
  MediaScanner      # 媒体文件扫描

七、小结

本文作为系列收官之作,通过实战案例展示了如何利用调试工具链定位 U 盘问题:

  1. 调试工具链:dmesg → logcat → dumpsys → vdc
  2. 典型案例:USB 总线复位导致的 StatusBarView 图标不消失问题
  3. 根因分析:两条通知通道的差异(广播 vs StorageEventListener 回调)
  4. 修复方案:增加状态校验、全状态刷新、广播兜底
  5. 排查流程:内核层 → vold 层 → MountService 层 → 应用层的四步排查法

掌握了本系列八篇文章的知识,你应该能够从容应对 Android 系统中绝大多数的 U 盘相关问题。