13、文件系统与存储架构:Vold、sdcardfs、FUSE、存储权限演进(Scoped Storage)

存储这块,说实话是Android系统里改动最频繁、坑也最多的模块之一。从早期的单用户分区,到后来的多用户、多存储设备,再到现在的分区存储,每一次改动都牵动着所有App开发者的神经。今天我就带大家把这块硬骨头啃下来。

13.1 存储架构的“三驾马车”

Android的存储架构,说白了就是三个核心组件在干活:Vold、sdcardfs和FUSE。它们各司其职,又相互配合。我当年刚接触这块时,也被绕得晕头转向,后来画了一张图才理清楚。

核心架构图:Android存储分层

Android 存储架构分层 应用层 (App) 通过 ContentResolver / File API 访问存储 FUSE (Filesystem in Userspace) 用户态文件系统,实现权限过滤与路径映射 sdcardfs (内核文件系统) 内核态文件系统,处理权限与多用户隔离 Vold (Volume Daemon) + 底层存储 管理分区挂载、加密、多用户存储映射 /data /sdcard /mnt/media_rw 等 用户态 内核态 守护进程

13.2 Vold:存储设备的“大管家”

Vold,全称Volume Daemon,是Android存储系统的核心守护进程。它负责管理所有存储设备的生命周期:检测、挂载、卸载、加密、解密。

我记得在Android 4.4时代,Vold还比较简单,主要处理SD卡的热插拔。到了Android 8.0以后,Vold的工作量暴增——要处理多用户、多分区、FBE(基于文件的加密)等等。

Vold的核心职责

  • 设备检测:通过uevent监听内核发出的设备插拔事件
  • 分区管理:处理MBR/GPT分区表,挂载指定分区
  • 加密处理:FBE(File-Based Encryption)和FDE(Full Disk Encryption)的密钥管理
  • 多用户隔离:每个用户有独立的存储空间,通过Vold创建映射
  • 存储卷管理:管理内部存储、SD卡、USB OTG等不同卷

小提示:Vold的日志通常在 logcat -b events | grep vold 中查看。遇到存储挂载问题,第一件事就是看Vold有没有报错。

Vold的工作流程

当用户插入一张SD卡时,Vold大致做了这么几件事:

  1. 内核检测到新设备,通过uevent发送消息
  2. Vold收到消息,调用 VolumeManager::handleBlockEvent
  3. 解析分区表,找到可挂载的分区
  4. 如果启用了加密,先解密
  5. 挂载到 /mnt/media_rw/<volume_id>
  6. 通过sdcardfs或FUSE映射到用户空间
// Vold 核心事件处理(简化伪代码)
void VolumeManager::handleBlockEvent(NetlinkEvent* evt) {
    std::string devPath = evt->findParam("DEVPATH");
    std::string action = evt->findParam("ACTION");
    
    if (action == "add") {
        // 检测到新设备
        auto vol = createVolume(devPath);
        vol->mount();  // 挂载
    } else if (action == "remove") {
        // 设备移除
        unmountVolume(devPath);
    }
}

13.3 sdcardfs:内核态的“守门员”

sdcardfs是Android内核中的一个文件系统,它是在内核态实现的。说白了,它的作用就是在内核层面做权限检查和路径映射。

为什么需要sdcardfs?我举个例子你就明白了。早期Android没有sdcardfs时,App可以直接通过文件路径访问SD卡上的任何文件。这显然不安全。sdcardfs就是在内核层加了一道“安检门”。

sdcardfs的核心特性

特性 说明
权限过滤 根据App的UID和GID,决定是否允许访问某个文件
路径映射 /sdcard 映射到实际物理路径 /data/media/0
多用户隔离 用户0看到的是 /data/media/0,用户10看到的是 /data/media/10
派生挂载 每个App可以有独立的挂载命名空间,实现更细粒度的控制

注意:sdcardfs是内核模块,需要内核编译时开启 CONFIG_SDCARD_FS。我曾经在移植Android到一款国产芯片时,发现内核默认没开这个选项,结果所有App都读不到SD卡,折腾了两天才找到原因。

13.4 FUSE:用户态的“翻译官”

FUSE(Filesystem in Userspace)允许在用户态实现文件系统。Android从某个版本开始,用FUSE替代了部分sdcardfs的功能。

你可能会问:为什么不用sdcardfs搞定一切?原因很简单——FUSE更灵活。sdcardfs在内核态,改起来麻烦,而且一旦出问题就是内核panic。FUSE在用户态,可以随时更新,出问题也只是进程崩溃,不会影响整个系统。

FUSE在Android中的角色

  • 权限代理:App对文件的访问请求,先经过FUSE,由FUSE决定是否放行
  • 路径重写:把App看到的虚拟路径,翻译成实际物理路径
  • 存储空间统计:FUSE可以精确统计每个App占用的存储空间
  • 媒体文件索引:配合MediaStore,实现媒体文件的快速检索
// FUSE 请求处理示例(简化)
int fuse_handler_read(const char* path, char* buf, size_t size) {
    // 1. 检查权限
    if (!check_permission(getuid(), path)) {
        return -EACCES;
    }
    
    // 2. 路径映射
    char real_path[PATH_MAX];
    map_virtual_to_real(path, real_path);
    
    // 3. 实际读取
    int fd = open(real_path, O_RDONLY);
    return read(fd, buf, size);
}

13.5 存储权限演进:从“野蛮生长”到“分区存储”

这部分是大家最关心的。存储权限的演进,说白了就是Google和App开发者之间的一场“猫鼠游戏”。

阶段一:Android 1.0 - 4.4(无限制时代)

App可以读写SD卡上的任何文件。只要声明 WRITE_EXTERNAL_STORAGE 权限,就能为所欲为。那时候的App,喜欢在SD卡根目录创建一堆文件夹,卸载了也不清理。

阶段二:Android 5.0 - 6.0(运行时权限)

引入了运行时权限,用户可以在App运行时拒绝存储权限。但App一旦拿到权限,依然可以访问整个SD卡。

阶段三:Android 7.0 - 9.0(多用户隔离)

sdcardfs开始发挥作用,每个用户只能看到自己的存储空间。但App之间仍然可以互相访问文件。

阶段四:Android 10+(分区存储 / Scoped Storage)

这是最狠的一次改动。App只能访问自己的专属目录(Context.getExternalFilesDir()),以及通过MediaStore访问媒体文件。想访问其他目录?要么用SAF(Storage Access Framework),要么申请 MANAGE_EXTERNAL_STORAGE 特殊权限。

核心变化:分区存储后,App不能再直接使用 /sdcard/xxx 这样的路径。必须通过MediaStore或SAF来访问公共目录的文件。

避坑指南

我曾经帮一个客户迁移App到Android 11,遇到了一个典型问题:他们的App需要在 /sdcard/Download 目录下创建临时文件。在Android 10之前,直接 new File("/sdcard/Download/tmp.txt") 就搞定了。但在分区存储下,这条路走不通了。

解决方案有两个:

  1. 推荐方案:使用 MediaStore.Downloads 来创建文件
  2. 备选方案:申请 MANAGE_EXTERNAL_STORAGE 权限(但Google审核很严)
// 分区存储下的正确写法
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, "tmp.txt");
values.put(MediaStore.Downloads.MIME_TYPE, "text/plain");
values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
// 通过uri写入文件内容

13.6 性能与兼容性考量

说实话,分区存储虽然提升了安全性,但也带来了性能问题。FUSE层增加了额外的上下文切换开销,每次文件操作都要从用户态到内核态再回到用户态。我实测过,分区存储下的文件读写性能比直接访问慢了约15%-20%。

另外,兼容性也是个头疼的问题。很多老App还在用 File API直接操作路径,在Android 10+上会直接崩溃。Google也意识到了这个问题,所以在Android 10上提供了 requestLegacyExternalStorage 兼容模式,但Android 11上这个模式被移除了。

我的建议:新开发的App,从一开始就使用MediaStore和SAF。老App迁移时,优先考虑使用 ContentResolver 操作文件,而不是直接操作路径。这样既能兼容新系统,又能避免性能损失。

嗯,存储这块内容确实不少。从Vold到sdcardfs,再到FUSE和分区存储,每一步都是Google在安全性和易用性之间做的权衡。理解这些底层机制,你才能在遇到存储相关的问题时,快速定位到根因。


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