Android 15存储子系统深度解析(二):FUSE文件系统与Scoped Storage

304 阅读12分钟

写在前面

如果你开发过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架构全景

05-01-fuse-architecture.png

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;
}

启动时的关键操作:

  1. 打开/dev/fuse设备文件
  2. 使用mount()系统调用挂载FUSE文件系统到/storage/emulated/0
  3. 启动一个常驻线程,循环处理文件操作请求

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);
}

数据流向:

  1. 应用调用read() → 进入内核VFS层
  2. VFS识别是FUSE文件系统 → 转发到FUSE模块
  3. FUSE模块构造请求 → 写入/dev/fuse
  4. FUSE Daemon读取请求 → 执行路径映射和权限检查
  5. FUSE Daemon打开真实文件 → 读取数据
  6. FUSE Daemon写回响应 → FUSE模块接收
  7. FUSE模块返回数据 → VFS → 应用

二、Scoped Storage:应用存储的新秩序

2.1 Scoped Storage权限模型

05-02-scoped-storage-model.png

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. 权限执行

05-03-mediaprovider-file-access-sequence.png

// 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的性能瓶颈

05-04-fuse-passthrough-comparison.png

传统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完整工作流程

05-05-saf-workflow-sequence.png

让我们实现一个完整的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性能明显下降。

解决方案:

  1. 启用FUSE Passthrough (Android 15+)
  2. 使用批量操作减少系统调用
  3. 考虑使用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机制,涵盖了以下核心内容:

  1. FUSE架构: 用户空间文件系统的实现原理,以及在Android中的应用
  2. Scoped Storage: 应用存储隔离的权限模型,保护用户隐私
  3. MediaProvider: 文件访问的核心中介,负责权限检查和路径转换
  4. FUSE Passthrough: Android 15的性能革命,大幅降低I/O开销
  5. SAF: 用户主导的文件访问框架,提供细粒度的权限控制
  6. 适配实践: 不同Android版本的存储权限适配策略

FUSE和Scoped Storage的引入,标志着Android存储管理从"粗放的权限控制"向"精细的隐私保护"的重大转变。虽然给开发者带来了适配成本,但对用户隐私和数据安全的提升是巨大的。

在下一篇文章中,我们将深入探讨加密文件系统与存储性能优化,分析FBE(File-Based Encryption)的实现机制,以及f2fs文件系统的特性与调优。


参考资料


系列文章


欢迎来我中的个人主页找到更多有用的知识和有趣的产品