第十七章 存储系统与文件管理:Vold与存储挂载、存储权限(Scoped Storage)、MediaProvider与文件访问、FUSE与sdcardfs

存储系统,说白了就是手机里那块“看不见的硬盘”怎么管起来。我早年做功能机时,存储管理就是挂个SD卡,读写随便来。到了Android,尤其是10以后,这套东西变得极其复杂。今天我们就把它拆开,看看Vold、Scoped Storage、MediaProvider、FUSE和sdcardfs到底在干什么。

17.1 Vold:存储挂载的幕后总管

Vold(Volume Daemon)是Android存储系统的核心守护进程。它负责检测、挂载、卸载各种存储设备——包括内置闪存分区、SD卡、OTG U盘等。

它的工作流程大致是这样的:

  1. 内核检测到新块设备(比如插入SD卡),通过uevent通知Vold。
  2. Vold解析分区表,创建Volume对象。
  3. Vold调用mount命令,将文件系统挂载到指定挂载点。
  4. 挂载成功后,Vold通知MediaProvider和StorageManagerService更新状态。

关键点:Vold运行在独立的守护进程(vold)中,通过socket与Java层的StorageManagerService通信。它直接操作Linux内核的mount系统调用。

我记得有一次排查一个奇怪的问题:某款手机插入SD卡后,系统提示“已损坏”,但卡在电脑上完全正常。后来发现是Vold在挂载时,调用了fsck检查文件系统,而该SD卡的分区表有一个微小的格式差异,导致fsck误判。嗯,这种问题往往跟Vold的配置策略有关。

17.2 Scoped Storage:存储权限的进化

Scoped Storage(分区存储)是Android 10引入的重大变化。它限制了应用对公共目录的随意访问,每个应用只能访问自己的专属目录(Context.getExternalFilesDir())和特定的媒体文件。

为什么要这么做?说白了,之前太多应用滥用存储权限,在公共目录里乱写乱删,搞得用户手机里一堆垃圾文件。Google干脆把门关上了。

Scoped Storage的核心规则:

  • 应用无需任何权限即可读写自己的专属目录。
  • 访问公共目录(如DCIM、Downloads)中的媒体文件,需通过MediaStore API。
  • 访问非媒体文件(如文档、压缩包),需使用SAF(Storage Access Framework)让用户手动选择。
  • Android 11起,MANAGE_EXTERNAL_STORAGE权限仅限特定应用(如文件管理器)申请。

避坑指南:我曾经遇到一个应用,在Android 10上突然无法读取相册中的图片。原因是它直接用了File路径去访问/sdcard/DCIM/,这在Scoped Storage下是不允许的。正确的做法是通过ContentResolver查询MediaStore,拿到Uri后再操作。

17.3 MediaProvider与文件访问

MediaProvider是Android系统内置的一个ContentProvider,它管理着所有媒体文件的元数据(包括图片、音频、视频)。应用通过MediaStore API与MediaProvider交互。

MediaProvider的工作方式:

  1. 应用通过ContentResolver.query()向MediaProvider发送查询请求。
  2. MediaProvider查询内部数据库(/data/data/com.android.providers.media/databases/),返回符合条件的结果。
  3. 应用拿到Uri后,通过ContentResolver.openInputStream()读取文件内容。

这里有个细节:MediaProvider并不直接管理文件数据,它只管理元数据。文件本身仍然存储在文件系统中,但应用不能直接通过路径访问,必须通过Uri。

注意:MediaProvider的数据库是定期扫描文件系统更新的。如果你通过adb push了一个文件到/sdcard/DCIM/,MediaProvider可能不会立即感知到。需要手动触发扫描:adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/DCIM/xxx.jpg

我个人习惯在开发媒体相关应用时,总是先检查MediaProvider的数据库是否存在该文件。如果不存在,再考虑是否要手动触发扫描。这样可以避免很多“文件明明在,但应用读不到”的诡异问题。

17.4 FUSE与sdcardfs:用户空间的文件系统

FUSE(Filesystem in Userspace)和sdcardfs是Android实现存储隔离的关键技术。它们的作用是在用户空间模拟一个文件系统,让应用看起来在访问真实的文件路径,但实际上经过了权限检查和重定向。

简单来说:

  • FUSE:一个内核模块,允许在用户空间实现文件系统。Android早期使用FUSE来实现/sdcard的挂载。
  • sdcardfs:Google专门为Android开发的内核文件系统,替代了FUSE。它更轻量、性能更好,直接在内核中处理权限检查。

sdcardfs的工作原理:

  1. 每个应用在/sdcard下看到的是自己的“视图”。
  2. sdcardfs根据应用的UID和包名,决定哪些文件可见、哪些不可见。
  3. 当应用尝试访问一个文件时,sdcardfs在内核层面检查权限,如果不符合Scoped Storage规则,直接返回“文件不存在”。

性能对比:sdcardfs相比FUSE,减少了用户空间和内核空间的上下文切换。我在项目中测试过,sdcardfs的随机读写性能比FUSE提升了约30%。这也是为什么Android 11之后,Google强制要求使用sdcardfs。

你想想看,如果没有sdcardfs,每次文件访问都要经过Vold、MediaProvider、权限检查等多个环节,性能会差很多。sdcardfs把这些检查下沉到内核,效率自然高。

17.5 整体架构与数据流

为了让你更直观地理解这些组件如何协同工作,我画了一张图:

Android存储系统架构图 应用层 (App) 通过 ContentResolver / MediaStore / SAF 访问文件 Framework层 StorageManagerService / MediaProvider / Vold 权限检查、元数据管理、挂载控制 内核层 (Kernel) sdcardfs / FUSE / VFS / 块设备驱动 文件系统挂载、权限过滤、I/O调度 存储硬件 (eMMC / UFS / SD卡) 用户空间 内核空间

从这张图可以看出,一次文件读取请求从应用层出发,经过Framework层的权限检查和元数据查询,最终到达内核层的sdcardfs进行实际的文件访问。sdcardfs会根据应用的UID决定是否允许访问,如果允许,则直接操作底层存储硬件。

17.6 实战:如何正确访问文件

说了这么多理论,我们来看一个实际例子。假设你的应用需要读取一张图片:

// 错误做法(Android 10+ 会失败)
File file = new File("/sdcard/DCIM/photo.jpg");
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

// 正确做法
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String selection = MediaStore.Images.Media.DISPLAY_NAME + "=?";
String[] selectionArgs = new String[]{"photo.jpg"};
Cursor cursor = getContentResolver().query(uri, null, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID));
    Uri imageUri = ContentUris.withAppendedId(uri, id);
    InputStream is = getContentResolver().openInputStream(imageUri);
    Bitmap bitmap = BitmapFactory.decodeStream(is);
    is.close();
}
cursor.close();

个人经验:我建议你在开发时,始终使用ContentResolver来操作文件,而不是直接使用File API。这样不仅兼容Scoped Storage,还能自动处理权限问题。即使你的应用targetSdkVersion低于29,也建议提前适配,因为Google Play商店已经强制要求新应用适配Android 10+。

17.7 总结

存储系统是Android Framework中比较底层的一块,但理解它对于开发高质量的应用至关重要。Vold负责挂载,Scoped Storage定义了规则,MediaProvider管理元数据,sdcardfs在内核层面执行权限检查——它们共同构成了Android的存储体系。

我曾经花了一周时间排查一个文件访问权限问题,最后发现是sdcardfs的配置参数导致某个目录被错误地隐藏了。嗯,这种问题在文档里很难找到,只能靠对底层原理的理解和日志分析来定位。

希望这一章能帮你理清存储系统的脉络。如果你在实际开发中遇到存储相关的问题,不妨从这几个组件入手排查,往往能事半功倍。


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