8、USB大容量存储设备开发:MSC协议原理、Android端读取U盘/移动硬盘、文件系统挂载(Vold与StorageManager)、OTG模式下文件管理实战

好,咱们今天聊点硬核的——USB大容量存储设备开发。说白了,就是让Android设备能读写U盘、移动硬盘这些东西。我当年第一次在手机上插U盘时,心里还嘀咕:这玩意儿真能认出来?结果还真行。但背后的门道,远比插上去就能用要复杂得多。

8.1 MSC协议原理:别被名字吓到

MSC,全称Mass Storage Class,是USB协议里专门给存储设备用的。你想想看,U盘、移动硬盘、甚至老式MP3,它们都遵循这套规则。MSC协议的核心思想很简单:把存储设备当成一块“大硬盘”,通过SCSI命令来读写。

我个人习惯把MSC协议拆成三层理解:

  • 物理层:USB总线传输数据,Bulk传输模式为主
  • 协议层:Bulk-Only Transport (BOT) 负责命令、数据、状态交互
  • 命令层:SCSI命令集,比如READ(10)、WRITE(10)、INQUIRY

嗯,这里要注意:MSC协议里有个经典的“命令-数据-状态”三阶段模型。主机发一个SCSI命令,设备执行,然后返回状态。我在项目中遇到过一个问题:某款U盘在状态阶段返回了错误码,但数据其实已经写完了。后来发现是设备固件bug,但Android端的驱动没做容错,直接报I/O错误。从那以后,我写代码都会加个重试机制。

核心要点:MSC协议不关心文件系统,它只负责读写扇区。文件系统是上层的事。

8.2 Android端读取U盘/移动硬盘:从插上到识别

当用户把U盘插到OTG线上,Android系统会经历什么?我画了张图,帮你理清整个流程。

Android OTG U盘识别流程 USB设备插入 USB Gadget驱动枚举 MSC驱动绑定 Vold检测到设备 Vold解析分区表 挂载文件系统 StorageManager广播通知 App收到挂载事件 App读写文件 整个过程从硬件到软件,层层递进 Vold是核心,StorageManager是桥梁

你看,从硬件插入到App能读写,中间经过了这么多层。我刚开始做这块时,以为直接读/dev/block/sda就行了,结果发现权限、挂载点、文件系统类型,哪个没处理好都白搭。

8.3 Vold与StorageManager:系统层的秘密

Vold(Volume Daemon)是Android系统里专门管理存储设备的守护进程。它负责检测设备、解析分区、挂载文件系统。说白了,Vold就是存储设备的“管家”。

我曾经在调试一个项目时,发现U盘插上去后Vold没反应。查了半天,原来是内核的USB驱动没把设备节点创建出来。Vold监听的是uevent事件,如果内核没发事件,Vold就傻等着。嗯,这坑我踩过。

StorageManager是Android Framework层给App用的API。它封装了Vold的底层操作,让开发者能方便地监听存储事件。举个例子:

// 注册存储事件监听
StorageManager storageManager = getSystemService(StorageManager.class);
storageManager.registerListener(new StorageEventListener() {
    @Override
    public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
        if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
            if (newState == VolumeInfo.STATE_MOUNTED) {
                // U盘已挂载,可以读写
                File mountPoint = vol.getPath();
                // 你的业务逻辑
            } else if (newState == VolumeInfo.STATE_EJECTING) {
                // 正在卸载,别读写
            }
        }
    }
});

我个人习惯在onVolumeStateChanged里加个日志,打印出vol.getPath()和vol.getFsType()。这样调试时一眼就能看出挂载点和文件系统类型。你想想看,如果挂载点是/mnt/media_rw/XXXX,那说明是FUSE挂载,读写性能会差一些。

小技巧:在Android 10及以上,Vold使用FUSE(Filesystem in Userspace)来挂载外部存储。这样做的好处是权限控制更严格,但代价是读写性能下降。如果你的App需要高性能读写,可以考虑直接访问底层块设备,但需要root权限。

8.4 OTG模式下文件管理实战

好了,理论说完了,咱们来点实战。OTG模式下的文件管理,核心就是两件事:检测设备挂载、读写文件。

先说说检测设备。我建议用StorageManager的getVolumesList()方法,遍历所有VolumeInfo,找到TYPE_PUBLIC且STATE_MOUNTED的卷。代码长这样:

List<VolumeInfo> volumes = storageManager.getVolumes();
for (VolumeInfo vol : volumes) {
    if (vol.getType() == VolumeInfo.TYPE_PUBLIC 
        && vol.getState() == VolumeInfo.STATE_MOUNTED) {
        File mountPoint = vol.getPath();
        String fsType = vol.getFsType();
        Log.d("USB", "发现U盘: " + mountPoint + ", 文件系统: " + fsType);
        
        // 读取U盘根目录
        File[] files = mountPoint.listFiles();
        for (File f : files) {
            // 处理文件
        }
    }
}

这里有个坑:有些U盘是MBR分区表,有些是GPT。Vold都能处理,但如果你直接读块设备,就得自己解析分区表。我曾经手写过一个MBR解析器,后来发现Vold已经做好了,何必重复造轮子?

再说说文件读写。OTG模式下,文件读写和普通文件操作没区别,但要注意几点:

  • 权限:Android 6.0以上需要动态申请存储权限
  • 路径:挂载点通常是/mnt/media_rw/XXXX,App不能直接访问,需要通过StorageAccessFramework(SAF)
  • 卸载:操作完要调用StorageManager的unmount()方法,否则U盘直接拔出可能损坏数据

警告:千万不要在U盘读写过程中直接拔出!我见过太多人因为这个习惯导致U盘文件系统损坏。正确的做法是:先卸载,再拔出。在代码里,监听onVolumeStateChanged的STATE_EJECTING状态,然后停止所有读写操作。

最后,分享一个我常用的文件管理工具类片段:

public class UsbFileManager {
    private StorageManager storageManager;
    
    public List<File> getMountedUsbDevices() {
        List<File> usbDevices = new ArrayList<>();
        List<VolumeInfo> volumes = storageManager.getVolumes();
        for (VolumeInfo vol : volumes) {
            if (vol.getType() == VolumeInfo.TYPE_PUBLIC 
                && vol.isMountedReadable()) {
                usbDevices.add(vol.getPath());
            }
        }
        return usbDevices;
    }
    
    public boolean safelyUnmount(VolumeInfo vol) {
        try {
            storageManager.unmount(vol.getId());
            return true;
        } catch (Exception e) {
            Log.e("USB", "卸载失败: " + e.getMessage());
            return false;
        }
    }
}

嗯,这个工具类虽然简单,但够用。我在多个项目里都用过,没出过问题。你如果要做OTG文件管理,可以直接拿过去改改。

好了,这一章的内容就到这。MSC协议、Vold、StorageManager、OTG实战,这些知识点串起来,你就能在Android上搞定U盘读写。记住,遇到问题多看看logcat,Vold的日志通常以vold或storage为tag,能帮你快速定位问题。


公众号:蓝海资料掘金营,微信deep3321