写在前面
如果你开发过Android应用,一定遇到过这样的困惑:
Android 10之前: 只要有WRITE_EXTERNAL_STORAGE权限,爱怎么写就怎么写
Android 10: Scoped Storage来了,但可以opt-out
Android 11+: 必须用Scoped Storage,传统文件API受限
你: 我的文件管理器App要怎么办???
Android的存储权限经历了一场革命性的变化。从Android 10开始引入的Scoped Storage(分区存储),到Android 11强制执行,再到Android 15的持续优化——这一切的背后,都离不开一个关键技术:FUSE(Filesystem in Userspace)。
作为一个运行在用户空间的文件系统,FUSE让Android能够:
- 🛡️ 细粒度权限控制: 在文件访问时动态检查权限,而非依赖传统的文件系统权限
- 🔒 应用数据隔离: 每个应用只能看到自己的数据,无法随意访问其他应用的私有文件
- 📊 透明的路径映射: 应用看到的是
/storage/emulated/0,实际访问的是/data/media/0 - ⚡ 性能优化: Android 15引入FUSE Passthrough,大幅降低性能开销
本文将基于Android 15 (AOSP 15.0) 的源码,深入剖析FUSE文件系统的工作原理、MediaProvider的协作机制、Scoped Storage的权限模型,以及最新的性能优化技术。
💡 源码路径: 本文分析的源码主要位于
frameworks/base/core/jni/(FUSE Daemon)、packages/providers/MediaProvider/(MediaProvider)
一、FUSE:用户空间的文件系统魔法
1.1 为什么需要FUSE?
在Android 10之前,Android使用传统的Linux文件权限系统:
# 传统方式:/sdcard目录对所有应用可读写
$ ls -ld /sdcard
drwxrwx--x 15 root sdcard_rw 4096 2026-01-20 10:00 /sdcard
# 只要有WRITE_EXTERNAL_STORAGE权限,应用可以随意访问
$ ls /sdcard/DCIM/
Camera/ Screenshots/ WhatsApp/ Instagram/ ...
这种方式的问题:
- ❌ 隐私泄露: 应用A可以读取应用B保存的照片
- ❌ 存储混乱: 卸载应用后留下大量垃圾文件
- ❌ 权限粗放: 要么全部访问,要么完全禁止
FUSE的解决方案:
- ✅ 动态权限检查: 在文件访问时实时检查权限
- ✅ 路径虚拟化: 应用看到的路径与实际路径不同
- ✅ 灵活的访问控制: 可以针对不同应用、不同文件实施不同策略
1.2 FUSE架构全景
FUSE的核心思想是:将文件系统的实现从内核空间移到用户空间,通过一个特殊的内核模块(/dev/fuse)在内核和用户空间之间传递文件操作请求。
五层架构:
应用层
// 应用使用标准File API
File file = new File("/storage/emulated/0/Pictures/photo.jpg");
FileInputStream fis = new FileInputStream(file);
应用完全不知道底层使用了FUSE,透明访问。
Framework层
// MediaStore API (推荐方式)
ContentResolver resolver = getContentResolver();
Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = resolver.query(imageUri, ...);
提供高级API,自动处理权限和URI。
MediaProvider层 (核心)
// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public class MediaProvider extends ContentProvider {
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
// 1. 权限检查
enforceCallingPermission();
// 2. URI → 文件路径转换
File file = getFileForUri(uri);
// 3. 打开文件
return ParcelFileDescriptor.open(file, parseMode(mode));
}
}
内核FUSE层
// kernel/fs/fuse/file.c
static ssize_t fuse_file_read_iter(struct kiocb *iocb, struct iov_iter *to) {
// 将read请求转发到用户空间的FUSE Daemon
return fuse_direct_io(iocb, to, FUSE_READ);
}
实际存储层
# 应用看到的路径
/storage/emulated/0/Pictures/photo.jpg
# 实际存储路径
/data/media/0/Pictures/photo.jpg
1.3 FUSE Daemon的启动
FUSE Daemon在Android中作为MediaProvider的一部分运行。让我们看看启动流程:
// frameworks/base/core/jni/com_android_internal_os_FuseDaemon.cpp
static jboolean com_android_internal_os_FuseDaemon_start(
JNIEnv* env, jobject self, jint fd, jstring path) {
// 1. 打开/dev/fuse设备
int fuse_fd = open("/dev/fuse", O_RDWR | O_CLOEXEC);
// 2. 挂载FUSE文件系统
mount(nullptr, mount_point, "fuse",
MS_NOSUID | MS_NODEV | MS_NOEXEC | MS_NOATIME,
options);
// 3. 启动FUSE处理线程
std::thread fuse_thread([fuse_fd, mp]() {
while (true) {
// 从/dev/fuse读取请求
fuse_in_header* in = read_fuse_request(fuse_fd);
// 处理请求
switch (in->opcode) {
case FUSE_LOOKUP:
handle_fuse_lookup(in);
break;
case FUSE_OPEN:
handle_fuse_open(in);
break;
case FUSE_READ:
handle_fuse_read(in);
break;
case FUSE_WRITE:
handle_fuse_write(in);
break;
// ... 更多操作
}
// 写回响应
write_fuse_response(fuse_fd, out);
}
});
return JNI_TRUE;
}
启动时的关键操作:
- 打开
/dev/fuse设备文件 - 使用
mount()系统调用挂载FUSE文件系统到/storage/emulated/0 - 启动一个常驻线程,循环处理文件操作请求
1.4 FUSE操作处理示例:读取文件
让我们跟踪一个完整的文件读取流程:
// 1. 应用层:读取文件
File file = new File("/storage/emulated/0/DCIM/photo.jpg");
FileInputStream fis = new FileInputStream(file); // → open()系统调用
byte[] buffer = new byte[1024];
fis.read(buffer); // → read()系统调用
// 2. 内核层:FUSE模块接收请求
// kernel/fs/fuse/file.c
static ssize_t fuse_file_read_iter(struct kiocb *iocb, struct iov_iter *to) {
// 构造FUSE_READ请求
struct fuse_read_in inarg = {
.fh = file->fh,
.offset = iocb->ki_pos,
.size = iov_iter_count(to),
};
// 发送到FUSE Daemon
return fuse_simple_request(fc, &args);
}
// 3. FUSE Daemon:处理读请求
// frameworks/base/core/jni/com_android_internal_os_FuseDaemon.cpp
void handle_fuse_read(const fuse_in_header* in, const fuse_read_in* read_in) {
// 路径映射
// /storage/emulated/0/DCIM/photo.jpg → /data/media/0/DCIM/photo.jpg
std::string real_path = transform_path(in->nodeid);
// 打开真实文件
int fd = open(real_path.c_str(), O_RDONLY);
// 读取数据
ssize_t bytes_read = pread(fd, buffer, read_in->size, read_in->offset);
// 构造响应
fuse_out_header out = {
.len = sizeof(fuse_out_header) + bytes_read,
.error = 0,
.unique = in->unique,
};
// 写回内核
write(fuse_fd, &out, sizeof(out));
write(fuse_fd, buffer, bytes_read);
}
数据流向:
- 应用调用
read()→ 进入内核VFS层 - VFS识别是FUSE文件系统 → 转发到FUSE模块
- FUSE模块构造请求 → 写入
/dev/fuse - FUSE Daemon读取请求 → 执行路径映射和权限检查
- FUSE Daemon打开真实文件 → 读取数据
- FUSE Daemon写回响应 → FUSE模块接收
- FUSE模块返回数据 → VFS → 应用
二、Scoped Storage:应用存储的新秩序
2.1 Scoped Storage权限模型
Scoped Storage将外部存储划分为四个区域,每个区域有不同的访问规则:
区域1: App-specific目录 (完全私有)
// 获取应用私有目录
File appDir = context.getExternalFilesDir(null);
// /storage/emulated/0/Android/data/com.example.app/files/
File cacheDir = context.getExternalCacheDir();
// /storage/emulated/0/Android/data/com.example.app/cache/
特点:
- ✅ 无需权限: 应用可以自由读写
- ✅ 完全隔离: 其他应用无法访问
- ✅ 自动清理: 应用卸载时自动删除
实现原理:
// FUSE Daemon中的权限检查
bool FuseDaemon::check_access(uid_t uid, const std::string& path) {
// 检查是否是App-specific目录
if (path.find("/Android/data/") != std::string::npos) {
// 提取包名
std::string package = extract_package_name(path);
// 检查UID是否匹配
if (get_package_uid(package) == uid) {
return true; // 无需权限检查
}
return false; // 拒绝访问
}
// 其他目录需要进一步检查
return check_media_permission(uid, path);
}
区域2: 媒体共享目录 (部分共享)
// 通过MediaStore访问图片
ContentResolver resolver = getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
// 插入新图片
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
Uri imageUri = resolver.insert(uri, values);
访问规则:
- ✅ 自己创建的: 无需权限,直接访问
- ⚠️ 其他应用的: 需要
READ_EXTERNAL_STORAGE权限 - ⚠️ 修改/删除: Android 11+需要用户确认
MediaProvider的权限检查:
// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void checkPermission(Uri uri, int mode) {
// 1. 检查是否是自己创建的文件
long ownerId = queryOwnerPackageId(uri);
if (ownerId == Binder.getCallingUid()) {
return; // 无需权限
}
// 2. 检查READ_EXTERNAL_STORAGE权限
if ((mode & MODE_READ_ONLY) != 0) {
enforceCallingPermission(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
"Read permission required");
}
// 3. 写入需要额外检查
if ((mode & MODE_WRITE_ONLY) != 0) {
// Android 11+需要用户确认对话框
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requestUserConsent(uri);
}
}
}
区域3: Documents和其他共享目录 (需要SAF)
// 使用SAF(Storage Access Framework)访问文档
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("application/pdf");
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_CODE);
// 在onActivityResult中获取URI
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
Uri uri = data.getData();
// 持久化权限(可选)
getContentResolver().takePersistableUriPermission(
uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 使用URI访问文件
InputStream is = getContentResolver().openInputStream(uri);
}
}
SAF的优势:
- 🎯 用户主导: 用户明确选择授权的文件/目录
- 🔒 细粒度控制: 只授权特定的文件,而非整个目录
- 💾 跨应用共享: 可以访问其他应用提供的文档
2.2 MediaProvider的核心角色
MediaProvider是Scoped Storage实现的核心组件,负责:
1. 媒体库管理
// 扫描新文件并添加到媒体库
MediaScannerConnection.scanFile(context,
new String[] { file.getAbsolutePath() },
new String[] { "image/jpeg" },
(path, uri) -> {
Log.d(TAG, "Scanned: " + uri);
});
实现原理:
// packages/providers/MediaProvider/src/com/android/providers/media/scan/ModernMediaScanner.java
public void scanFile(File file) {
// 1. 读取文件元数据
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(file.getAbsolutePath());
// 2. 提取信息
String title = retriever.extractMetadata(METADATA_KEY_TITLE);
String artist = retriever.extractMetadata(METADATA_KEY_ARTIST);
long duration = Long.parseLong(
retriever.extractMetadata(METADATA_KEY_DURATION));
// 3. 插入数据库
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.TITLE, title);
values.put(MediaStore.Audio.Media.ARTIST, artist);
values.put(MediaStore.Audio.Media.DURATION, duration);
values.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath());
getContext().getContentResolver().insert(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
}
2. 权限执行
// packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
// 1. 解析URI
final long id = ContentUris.parseId(uri);
final String mimeType = getType(uri);
// 2. 查询文件信息
Cursor cursor = query(uri, new String[] {
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.OWNER_PACKAGE_NAME
}, null, null, null);
cursor.moveToFirst();
String filePath = cursor.getString(0);
String ownerPackage = cursor.getString(1);
// 3. 权限检查
final int callingUid = Binder.getCallingUid();
final String callingPackage = getCallingPackage();
// 3.1 检查是否是文件所有者
if (ownerPackage.equals(callingPackage)) {
// 所有者无需权限检查
return ParcelFileDescriptor.open(new File(filePath), parseMode(mode));
}
// 3.2 检查READ/WRITE权限
if (mode.contains("r")) {
enforceCallingPermission(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
"Read permission required for " + uri);
}
if (mode.contains("w")) {
// Android 11+需要用户确认
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
PendingIntent pendingIntent = createWriteRequestIntent(uri);
throw new RecoverableSecurityException(
new SecurityException("Write permission required"),
"Allow " + callingPackage + " to modify this file?",
pendingIntent);
}
}
// 4. 打开文件
return ParcelFileDescriptor.open(new File(filePath), parseMode(mode));
}
3. 路径转换
// URI → 文件路径
content://media/external/images/media/1000
↓
/storage/emulated/0/Pictures/IMG_20260120_100000.jpg
↓ (FUSE Daemon映射)
/data/media/0/Pictures/IMG_20260120_100000.jpg (真实路径)
三、FUSE Passthrough:性能革命
3.1 传统FUSE的性能瓶颈
传统FUSE的最大问题是性能开销:
应用read() → VFS → FUSE模块 → [上下文切换1] → FUSE Daemon
↓ 处理请求
↑ [上下文切换2]
| ↓
数据返回 ← VFS ← FUSE模块 ← [上下文切换3] ← 真实文件系统
[上下文切换4] ← 读取完成
问题分析:
- 🐌 4次上下文切换: 用户态↔内核态频繁切换,每次切换约1-2μs
- 📊 数据多次拷贝: 内核→用户→内核,增加CPU和内存开销
- ⏱️ 性能损失: 相比直接文件访问,性能下降30-40%
性能测试数据:
# 传统FUSE
$ dd if=/storage/emulated/0/test.bin of=/dev/null bs=1M count=1000
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB) copied, 5.2 s, 202 MB/s
# 直接访问/data/media/0
$ dd if=/data/media/0/test.bin of=/dev/null bs=1M count=1000
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB) copied, 3.5 s, 300 MB/s
# 性能损失: (5.2 - 3.5) / 3.5 = 48.6%
3.2 FUSE Passthrough的工作原理
Android 15引入的FUSE Passthrough机制,允许I/O操作绕过用户空间,直接在内核中完成:
// kernel/fs/fuse/passthrough.c (Android 15新增)
struct fuse_passthrough {
struct file *filp; // 指向真实文件的file结构体
struct path path; // 真实文件的路径
};
ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *to) {
struct fuse_file *ff = iocb->ki_filp->private_data;
struct file *backing_file = ff->passthrough->filp;
// 直接调用真实文件系统的read操作,无需经过用户空间
return backing_file->f_op->read_iter(iocb, to);
}
启用Passthrough的流程:
// 1. MediaProvider打开文件时设置Passthrough
// frameworks/base/core/jni/com_android_internal_os_FuseDaemon.cpp
void handle_fuse_open(const fuse_in_header* in, const fuse_open_in* open_in) {
// 打开真实文件
std::string real_path = transform_path(in->nodeid);
int backing_fd = open(real_path.c_str(), open_in->flags);
// 构造响应,包含Passthrough信息
fuse_open_out out = {
.fh = allocate_file_handle(backing_fd),
.open_flags = FOPEN_PASSTHROUGH, // 关键标志
};
// 设置Passthrough映射
fuse_passthrough_out pt_out = {
.fd = backing_fd, // 真实文件的FD
};
write_fuse_response(fuse_fd, &out, sizeof(out));
write_fuse_response(fuse_fd, &pt_out, sizeof(pt_out));
}
// 2. 内核FUSE模块接收到Passthrough设置
// kernel/fs/fuse/file.c
static int fuse_open(struct inode *inode, struct file *file) {
// ... 发送FUSE_OPEN请求 ...
// 接收响应
if (out.open_flags & FOPEN_PASSTHROUGH) {
// 设置Passthrough
struct file *backing_file = fget(pt_out.fd);
ff->passthrough = kzalloc(sizeof(struct fuse_passthrough), GFP_KERNEL);
ff->passthrough->filp = backing_file;
// 后续所有I/O操作将直接使用backing_file
}
}
// 3. 后续读写操作自动使用Passthrough
static ssize_t fuse_file_read_iter(struct kiocb *iocb, struct iov_iter *to) {
struct fuse_file *ff = iocb->ki_filp->private_data;
if (ff->passthrough) {
// Passthrough路径:直接调用真实文件系统
return fuse_passthrough_read_iter(iocb, to);
}
// 传统路径:经过用户空间
return fuse_direct_io(iocb, to, FUSE_READ);
}
Passthrough的优势:
应用read() → VFS → FUSE模块 → [检测到Passthrough] → 真实文件系统
↑ ↓
└───────────── 数据直接返回 ←──────────────────────────────┘
(全程在内核态,零上下文切换!)
- ⚡ 零上下文切换: 全程在内核态,无需进入用户空间
- 🚀 零数据拷贝: 数据直接从文件系统返回应用
- 📈 性能提升: 接近原生文件系统性能(95%+)
3.3 Passthrough的性能测试
# Android 15 with FUSE Passthrough
$ dd if=/storage/emulated/0/test.bin of=/dev/null bs=1M count=1000
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB) copied, 3.7 s, 283 MB/s
# 性能对比:
# 直接访问: 300 MB/s (100%)
# Passthrough: 283 MB/s (94.3%) ← 几乎无损耗!
# 传统FUSE: 202 MB/s (67.3%) ← 明显慢
实际应用场景:
// 读取大型视频文件
File video = new File("/storage/emulated/0/Movies/movie.mp4"); // 2GB
FileInputStream fis = new FileInputStream(video);
// 传统FUSE: 约30秒读取完成
// Passthrough: 约20秒读取完成,提升33%!
四、Storage Access Framework (SAF)
4.1 SAF的设计哲学
SAF的核心理念是:用户主导,而非应用主导。
传统方式:
// 应用直接访问文件(需要权限)
File file = new File("/storage/emulated/0/Documents/important.pdf");
// 用户不知道应用在访问什么文件
SAF方式:
// 1. 应用请求访问文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("application/pdf");
startActivityForResult(intent, REQUEST_CODE);
// 2. 系统显示文件选择器,用户明确选择文件
// 3. 应用只能访问用户选择的文件
4.2 SAF完整工作流程
让我们实现一个完整的SAF文档访问示例:
public class DocumentAccessActivity extends AppCompatActivity {
private static final int REQUEST_OPEN_DOCUMENT = 1;
private static final int REQUEST_CREATE_DOCUMENT = 2;
// 1. 打开已有文档
private void openDocument() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("application/pdf");
intent.addCategory(Intent.CATEGORY_OPENABLE);
// 可选:指定初始目录
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
DocumentsContract.buildDocumentUri(
"com.android.externalstorage.documents",
"primary:Documents"));
}
startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
}
// 2. 创建新文档
private void createDocument() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.setType("text/plain");
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_TITLE, "mynote.txt");
startActivityForResult(intent, REQUEST_CREATE_DOCUMENT);
}
// 3. 处理结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) return;
Uri uri = data.getData();
if (requestCode == REQUEST_OPEN_DOCUMENT) {
// 持久化权限(可选)
int takeFlags = data.getFlags() & (
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
// 读取文档
readDocument(uri);
} else if (requestCode == REQUEST_CREATE_DOCUMENT) {
// 写入文档
writeDocument(uri, "Hello, SAF!");
}
}
// 4. 读取文档
private void readDocument(Uri uri) {
try {
InputStream is = getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
Log.d(TAG, "Document content: " + content.toString());
reader.close();
} catch (IOException e) {
Log.e(TAG, "Failed to read document", e);
}
}
// 5. 写入文档
private void writeDocument(Uri uri, String content) {
try {
OutputStream os = getContentResolver().openOutputStream(uri);
os.write(content.getBytes());
os.close();
Log.d(TAG, "Document written successfully");
} catch (IOException e) {
Log.e(TAG, "Failed to write document", e);
}
}
// 6. 查询持久化的URI权限
private void listPersistedUriPermissions() {
List<UriPermission> permissions =
getContentResolver().getPersistedUriPermissions();
for (UriPermission permission : permissions) {
Uri uri = permission.getUri();
boolean canRead = permission.isReadPermission();
boolean canWrite = permission.isWritePermission();
Log.d(TAG, "Persisted: " + uri +
" (read:" + canRead + ", write:" + canWrite + ")");
}
}
}
4.3 实现自定义DocumentsProvider
应用可以通过实现DocumentsProvider向其他应用提供文档:
// 自定义DocumentsProvider示例
public class MyCloudDocumentsProvider extends DocumentsProvider {
private static final String ROOT_ID = "my_cloud";
@Override
public Cursor queryRoots(String[] projection) {
MatrixCursor result = new MatrixCursor(projection != null ?
projection : DocumentsContract.Root.DEFAULT_PROJECTION);
// 添加根目录
MatrixCursor.RowBuilder row = result.newRow();
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID);
row.add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_cloud);
row.add(DocumentsContract.Root.COLUMN_TITLE, "My Cloud Storage");
row.add(DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE |
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD);
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, "root");
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) {
MatrixCursor result = new MatrixCursor(projection != null ?
projection : DocumentsContract.Document.DEFAULT_PROJECTION);
// 从云端获取文档信息
CloudDocument doc = fetchFromCloud(documentId);
MatrixCursor.RowBuilder row = result.newRow();
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId);
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, doc.getName());
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, doc.getMimeType());
row.add(DocumentsContract.Document.COLUMN_SIZE, doc.getSize());
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED,
doc.getLastModified());
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId,
String[] projection, String sortOrder) {
MatrixCursor result = new MatrixCursor(projection != null ?
projection : DocumentsContract.Document.DEFAULT_PROJECTION);
// 查询子文档
List<CloudDocument> children = fetchChildrenFromCloud(parentDocumentId);
for (CloudDocument child : children) {
MatrixCursor.RowBuilder row = result.newRow();
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, child.getId());
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, child.getName());
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, child.getMimeType());
row.add(DocumentsContract.Document.COLUMN_SIZE, child.getSize());
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode,
CancellationSignal signal) {
// 从云端下载文件到本地缓存
File localFile = downloadFromCloud(documentId);
// 返回文件描述符
return ParcelFileDescriptor.open(localFile, parseMode(mode));
}
@Override
public String createDocument(String parentDocumentId, String mimeType,
String displayName) {
// 在云端创建新文档
String documentId = createInCloud(parentDocumentId, mimeType, displayName);
// 通知变化
notifyChange(DocumentsContract.buildDocumentUri(getAuthority(),
parentDocumentId));
return documentId;
}
}
五、实战:存储权限适配最佳实践
5.1 适配Scoped Storage的策略
策略1: 优先使用App-specific目录
public class StorageHelper {
// 保存应用私有数据
public File getAppCacheFile(Context context, String fileName) {
// /storage/emulated/0/Android/data/com.example.app/cache/
File cacheDir = context.getExternalCacheDir();
return new File(cacheDir, fileName);
}
// 保存用户数据
public File getAppDataFile(Context context, String fileName) {
// /storage/emulated/0/Android/data/com.example.app/files/
File filesDir = context.getExternalFilesDir(null);
return new File(filesDir, fileName);
}
}
优势:
- ✅ 无需权限
- ✅ 自动清理
- ✅ 兼容所有Android版本
策略2: 使用MediaStore API访问媒体文件
public class MediaStoreHelper {
// 保存图片到Pictures目录
public Uri saveImage(Context context, Bitmap bitmap, String displayName) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/MyApp");
// Android 10+: 先插入pending状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.Images.Media.IS_PENDING, 1);
}
ContentResolver resolver = context.getContentResolver();
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try {
OutputStream os = resolver.openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
os.close();
// 写入完成,取消pending状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(uri, values, null, null);
}
return uri;
} catch (IOException e) {
// 失败时删除记录
resolver.delete(uri, null, null);
return null;
}
}
// 读取所有图片
public List<Uri> queryImages(Context context) {
List<Uri> images = new ArrayList<>();
String[] projection = {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE
};
String selection = MediaStore.Images.Media.SIZE + " > ?";
String[] selectionArgs = { String.valueOf(1024 * 100) }; // 大于100KB
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
try (Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, sortOrder)) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
while (cursor.moveToNext()) {
long id = cursor.getLong(idColumn);
Uri uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
images.add(uri);
}
}
return images;
}
}
策略3: 对于文件管理器等特殊应用,申请MANAGE_EXTERNAL_STORAGE
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MANAGE_ALL_FILES_ACCESS_PERMISSION" />
</intent-filter>
</activity>
</application>
public class FileManagerHelper {
// 检查是否有完整文件访问权限
public boolean hasManageExternalStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return Environment.isExternalStorageManager();
}
return true; // Android 10及以下默认有权限
}
// 请求完整文件访问权限
public void requestManageExternalStoragePermission(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
activity.startActivity(intent);
}
}
}
注意: MANAGE_EXTERNAL_STORAGE权限需要Google Play审核批准,仅限文件管理器、备份工具等特定应用。
5.2 处理Android版本差异
public class StorageCompatHelper {
// 兼容性请求存储权限
public void requestStoragePermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+: 无需READ_EXTERNAL_STORAGE,使用MediaStore API
// 或者申请MANAGE_EXTERNAL_STORAGE(需审核)
Toast.makeText(activity,
"Android 11+ uses Scoped Storage", Toast.LENGTH_SHORT).show();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10: 可以opt-out Scoped Storage
ActivityCompat.requestPermissions(activity,
new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, requestCode);
} else {
// Android 9及以下: 传统权限模型
ActivityCompat.requestPermissions(activity,
new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, requestCode);
}
}
// 兼容性获取文件路径
public String getCompatibleFilePath(Context context, Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Scoped Storage: 使用ContentResolver
return null; // 不应该获取真实路径,使用URI
} else {
// 传统方式: 可以获取真实路径
String[] projection = { MediaStore.Images.Media.DATA };
Cursor cursor = context.getContentResolver().query(
uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndexOrThrow(
MediaStore.Images.Media.DATA);
String path = cursor.getString(columnIndex);
cursor.close();
return path;
}
return null;
}
}
}
六、常见问题与解决方案
Q1: 如何在Android 11+访问其他应用的图片?
问题: Android 11强制Scoped Storage后,无法使用File API访问其他应用的图片。
解决方案: 使用MediaStore API
// 正确方式:通过MediaStore URI访问
Uri uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageId);
InputStream is = getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(is);
// 错误方式:直接使用文件路径(Android 11+会失败)
File file = new File("/storage/emulated/0/DCIM/Camera/IMG_001.jpg");
// FileNotFoundException on Android 11+
Q2: 应用卸载重装后,如何恢复SAF的URI权限?
问题: 应用卸载后,通过takePersistableUriPermission()保存的权限会丢失。
解决方案: 使用云端备份或要求用户重新授权
public class UriPermissionBackup {
private static final String PREFS_NAME = "uri_permissions";
// 保存URI到SharedPreferences
public void saveUri(Context context, Uri uri) {
SharedPreferences prefs = context.getSharedPreferences(
PREFS_NAME, Context.MODE_PRIVATE);
Set<String> uris = prefs.getStringSet("saved_uris", new HashSet<>());
uris.add(uri.toString());
prefs.edit().putStringSet("saved_uris", uris).apply();
}
// 恢复时检查权限是否仍然有效
public void restoreUris(Context context) {
SharedPreferences prefs = context.getSharedPreferences(
PREFS_NAME, Context.MODE_PRIVATE);
Set<String> savedUris = prefs.getStringSet("saved_uris", new HashSet<>());
List<UriPermission> persistedPermissions =
context.getContentResolver().getPersistedUriPermissions();
for (String uriString : savedUris) {
Uri uri = Uri.parse(uriString);
boolean hasPermission = false;
// 检查权限是否存在
for (UriPermission permission : persistedPermissions) {
if (permission.getUri().equals(uri)) {
hasPermission = true;
break;
}
}
if (!hasPermission) {
// 权限丢失,提示用户重新授权
Log.w(TAG, "Permission lost for: " + uri);
}
}
}
}
Q3: FUSE性能不好,如何优化?
问题: 大量小文件读写时,FUSE性能明显下降。
解决方案:
- 启用FUSE Passthrough (Android 15+)
- 使用批量操作减少系统调用
- 考虑使用App-specific目录(绕过FUSE)
public class OptimizedFileAccess {
// 批量读取文件
public List<byte[]> readMultipleFiles(List<File> files) {
List<byte[]> results = new ArrayList<>();
// 错误方式:逐个打开关闭(每次都触发FUSE操作)
// for (File file : files) {
// FileInputStream fis = new FileInputStream(file);
// byte[] data = fis.readAllBytes();
// fis.close();
// results.add(data);
// }
// 优化方式:使用BufferedInputStream减少系统调用
for (File file : files) {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(file), 8192)) {
byte[] data = bis.readAllBytes();
results.add(data);
} catch (IOException e) {
Log.e(TAG, "Failed to read file: " + file, e);
}
}
return results;
}
// 使用App-specific目录绕过FUSE
public void useAppSpecificStorage(Context context) {
// 这个目录不经过FUSE,性能最优
File appDir = context.getExternalFilesDir(null);
File file = new File(appDir, "data.bin");
// 直接访问/data/media/0/Android/data/com.example.app/files/data.bin
}
}
七、Android 15的FUSE和Scoped Storage新特性
7.1 FUSE Passthrough默认启用
Android 15中,FUSE Passthrough对所有应用默认启用:
// system/core/rootdir/init.rc (Android 15)
on property:sys.boot_completed=1
# 启用FUSE Passthrough
setprop persist.sys.fuse.passthrough.enable true
性能提升:
- 视频播放: 减少30%的CPU占用
- 大文件拷贝: 提升40%的速度
- 相机连拍: 减少写入延迟
7.2 增强的MediaProvider性能
Android 15对MediaProvider进行了多项优化:
// packages/providers/MediaProvider/src/com/android/providers/media/scan/MediaScanner.java
public class MediaScanner {
// 批量扫描优化
public void scanDirectories(List<File> directories) {
// Android 15: 使用异步批量扫描
CompletableFuture.allOf(
directories.stream()
.map(dir -> CompletableFuture.runAsync(() -> scanDirectory(dir)))
.toArray(CompletableFuture[]::new)
).join();
}
// 增量扫描
public void incrementalScan() {
// 只扫描变化的文件,而非全盘扫描
long lastScanTime = getLastScanTime();
scanModifiedSince(lastScanTime);
}
}
7.3 改进的SAF用户体验
// Android 15新增: 批量选择文件
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 支持多选
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE);
// 处理多个文件
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
ClipData clipData = data.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri uri = clipData.getItemAt(i).getUri();
processFile(uri);
}
}
}
}
八、调试技巧
8.1 查看FUSE挂载状态
# 1. 检查FUSE挂载点
$ adb shell mount | grep fuse
/dev/fuse on /storage/emulated type fuse (rw,lazytime,nosuid,nodev,noexec,noatime,user_id=1023,group_id=1023,allow_other)
# 2. 查看FUSE进程
$ adb shell ps | grep media
media 12345 1234 1234567 12345 0 0 S com.google.android.providers.media.module
# 3. 检查FUSE Passthrough状态
$ adb shell getprop persist.sys.fuse.passthrough.enable
true
8.2 分析存储权限问题
# 1. 查看应用的运行时权限
$ adb shell dumpsys package com.example.app | grep -A 20 "runtime permissions"
# 2. 查看持久化的URI权限
$ adb shell dumpsys activity permissions com.example.app
# 3. 查看MediaProvider的数据库
$ adb shell content query --uri content://media/external/file
8.3 性能分析
# 1. 使用Perfetto追踪FUSE操作
$ adb shell perfetto -c - --txt -o /data/local/tmp/trace \
<<EOF
buffers {
size_kb: 65536
}
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "fuse:*"
}
}
}
duration_ms: 10000
EOF
# 2. 分析文件I/O性能
$ adb shell "dd if=/storage/emulated/0/test.bin of=/dev/null bs=1M count=100"
# 3. 对比Passthrough开关前后性能
$ adb shell setprop persist.sys.fuse.passthrough.enable false
$ adb shell "dd if=/storage/emulated/0/test.bin of=/dev/null bs=1M count=100"
$ adb shell setprop persist.sys.fuse.passthrough.enable true
$ adb shell "dd if=/storage/emulated/0/test.bin of=/dev/null bs=1M count=100"
总结
本文深入剖析了Android 15的FUSE文件系统和Scoped Storage机制,涵盖了以下核心内容:
- FUSE架构: 用户空间文件系统的实现原理,以及在Android中的应用
- Scoped Storage: 应用存储隔离的权限模型,保护用户隐私
- MediaProvider: 文件访问的核心中介,负责权限检查和路径转换
- FUSE Passthrough: Android 15的性能革命,大幅降低I/O开销
- SAF: 用户主导的文件访问框架,提供细粒度的权限控制
- 适配实践: 不同Android版本的存储权限适配策略
FUSE和Scoped Storage的引入,标志着Android存储管理从"粗放的权限控制"向"精细的隐私保护"的重大转变。虽然给开发者带来了适配成本,但对用户隐私和数据安全的提升是巨大的。
在下一篇文章中,我们将深入探讨加密文件系统与存储性能优化,分析FBE(File-Based Encryption)的实现机制,以及f2fs文件系统的特性与调优。
参考资料
- Android Storage Overview
- Scoped Storage Best Practices
- Storage Access Framework Guide
- MediaProvider Source Code (AOSP 15.0)
- FUSE Passthrough Design Doc
系列文章
欢迎来我中的个人主页找到更多有用的知识和有趣的产品