13、文件系统与存储架构:Vold、sdcardfs、FUSE、存储权限演进(Scoped Storage)
存储这块,说实话是Android系统里改动最频繁、坑也最多的模块之一。从早期的单用户分区,到后来的多用户、多存储设备,再到现在的分区存储,每一次改动都牵动着所有App开发者的神经。今天我就带大家把这块硬骨头啃下来。
13.1 存储架构的“三驾马车”
Android的存储架构,说白了就是三个核心组件在干活:Vold、sdcardfs和FUSE。它们各司其职,又相互配合。我当年刚接触这块时,也被绕得晕头转向,后来画了一张图才理清楚。
核心架构图:Android存储分层
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大致做了这么几件事:
- 内核检测到新设备,通过uevent发送消息
- Vold收到消息,调用
VolumeManager::handleBlockEvent - 解析分区表,找到可挂载的分区
- 如果启用了加密,先解密
- 挂载到
/mnt/media_rw/<volume_id> - 通过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") 就搞定了。但在分区存储下,这条路走不通了。
解决方案有两个:
- 推荐方案:使用
MediaStore.Downloads来创建文件 - 备选方案:申请
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