Android 分区存储 Scoped Storage

FreeGuideOnline 最新 2026-07-12

Android 分区存储(Scoped Storage)完全指南

什么是分区存储

分区存储是 Android 10(API 级别 29)引入的一种存储访问机制,旨在从根本上改变应用访问外部存储文件的方式。它的核心思想是为每个应用提供一个独立的、隔离的存储沙盒,应用默认只能访问自己的专属目录,对外部存储的公共区域(如 DCIM、Pictures、Downloads 等)的访问受到严格限制。这一变化替代了传统的“放任式”文件系统访问模型,显著提升了用户数据的隐私与安全性。

在 Android 10 中,分区存储是可选的,应用可以通过在清单中添加 requestLegacyExternalStorage 标记暂时沿用旧版存储模型。但从 Android 11 开始,所有应用都必须强制使用分区存储,即使声明了旧版兼容标记也无效(除非应用专门且必要地请求了 MANAGE_EXTERNAL_STORAGE 权限)。

为什么需要分区存储

在传统的存储模型中,应用只要获得了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限,就可以随意读写整个外部存储空间。这带来了几大问题:

  • 隐私泄露:恶意应用可以轻易扫描并收集用户的所有照片、文档、聊天记录等文件。
  • 存储混乱:每个应用都在外部存储的根目录下创建自己的文件夹,卸载应用后残留大量垃圾文件,用户很难清理。
  • 安全风险:应用可能修改或删除其他应用的关键数据,导致功能异常或安全漏洞。

分区存储通过隔离和受控的访问,让用户对自己的文件拥有更细粒度的控制权,应用只能拿到它真正需要的文件,而不能“一揽子”获取所有内容。

分区存储下的目录结构

分区存储将外部存储空间划分成几个具有不同访问规则的区域:

  • 应用专属目录(App-specific directories)
    路径为 /Android/data/<包名>/files/Android/media/<包名>/
    应用无需任何权限即可读写该目录下的文件。当应用被卸载时,该目录下的数据会被自动清理。适合存放内部缓存、配置文件等不希望用户直接访问或与管理的数据。

  • 公共共享集合(Shared Collections)
    由 MediaStore API 管理的媒体文件集合,例如图片(MediaStore.Images)、视频(MediaStore.Video)、音频(MediaStore.Audio)以及下载文件(MediaStore.Downloads)。
    访问这些集合中的文件不需要申请 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限,而是通过 MediaStore 进行受控读写。

  • 其他文件访问(通过存储访问框架 SAF)
    通过 Intent.ACTION_OPEN_DOCUMENT / ACTION_CREATE_DOCUMENT 让用户主动选择文件或目录,应用获得该特定文件的长期访问权限。这种方式完全不需要任何存储权限,完全由用户掌控。

Android 10 与 Android 11 的行为区别

功能点 Android 10 (API 29) Android 11 (API 30)
分区存储强制 可选,支持 requestLegacyExternalStorage 强制开启,旧版标记失效
MediaStore 访问 应用可以读取自身未贡献的媒体文件,但需要 READ_EXTERNAL_STORAGE 权限 应用可以读取自身未贡献的媒体文件,但需要 READ_EXTERNAL_STORAGE 权限;新增查询过滤限制
访问其他应用创建的媒体文件 如果没有对应权限,只能读取自己的文件 与 10 一致,但新增了 QUERY_ALL_PACKAGES 限制的影响
访问 Downloads 集合 通过 SAF 或拥有 READ/WRITE 权限使用 MediaStore.Downloads 与 10 相同,但 MANAGE_EXTERNAL_STORAGE 应用才可绕过大部分限制
应用专属目录访问 无变化 无变化
文件路径访问 MediaStore 文件的 DATA 列可能被隐藏(返回 null) 彻底弃用 DATA 列,必须使用 ContentResolver 和 Uri

如何适配分区存储

1. 将文件迁移到应用专属目录

如果应用之前直接在外部存储根目录下创建文件夹存储数据,建议迁移到应用的专属目录。使用 getExternalFilesDir()getExternalCacheDir() 获取目录,并且不需要任何权限。

File appSpecificDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File myFile = new File(appSpecificDir, "user_data.txt");
// 正常读写文件,无需存储权限

对于媒体文件,如果希望卸载后保留,则必须使用 MediaStore 或 SAF;如果允许卸载后删除,可放入 getExternalMediaDirs()

2. 使用 MediaStore 访问共享媒体

替代直接文件路径操作,通过 ContentResolver 查询、插入、更新媒体文件。下面以保存一张图片为例:

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 + "/MyApp");

ContentResolver resolver = getContentResolver();
Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

try (OutputStream out = resolver.openOutputStream(imageUri)) {
    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
}

读取图片:

Cursor cursor = resolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    null,
    null,
    null,
    null
);
if (cursor != null) {
    while (cursor.moveToNext()) {
        long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
        Uri contentUri = ContentUris.withAppendedId(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
        // 通过 contentUri 加载图片
    }
    cursor.close();
}

重要: Android 10 中 MediaStore 文件的 _data 字段可能返回路径或 null,Android 11 中一律不应使用该字段;始终通过 ContentResolver 配合 Uri 操作文件内容。

3. 使用存储访问框架 (SAF) 获取用户授权

当应用需要访问非媒体类文件(如 PDF、文档)或任意目录时,引导用户通过系统文件选择器给予权限。 启动选择文件:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE);

接收结果并持久化权限:

@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 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // 保存 uri 以便后续使用
    }
}

对于整个目录的访问,使用 ACTION_OPEN_DOCUMENT_TREE,返回的 DocumentFile 可以管理整个目录树。

4. 处理存量文件的访问兼容性

若应用在分区存储前已拥有大量外部文件,可以使用 requestLegacyExternalStorage 在 Android 10 上暂时维持旧模型。但需注意:

  • 该标记仅对 Android 10 有效,且仅当应用以 targetSdk 29 或更低运行时可生效。
  • 用户升级到 Android 11 或更新系统时,该标记失效。
  • 建议尽快完成全量迁移,而不是依赖此过渡方案。

如果应用作为文件管理器或备份工具确实需要广泛访问所有文件,可以声明 MANAGE_EXTERNAL_STORAGE 权限并引导用户到系统设置中开启“所有文件访问权限”。Google Play 对此权限有严格审核,仅允许核心功能为文件管理、防病毒等少数场景使用。

常见问题与注意事项

权限不再通用

  • 访问自己生成的媒体文件(通过 MediaStore 插入)不需要任何存储权限。
  • 访问其他应用生成的媒体文件,Android 10 需要 READ_EXTERNAL_STORAGE;Android 11 也需该权限,但查询结果可能因包可见性限制而减少。
  • 从 Android 11 开始,即使拥有 READ_EXTERNAL_STORAGE,也无法读取其他应用专属目录下的媒体文件。

MediaStore 查询性能优化

避免一次性查询所有列,应明确 projection 数组。使用分页加载(LIMIT / OFFSET)或基于 DATE_ADDED 的条件查询来减少内存占用。

处理文件重命名与移动

MediaStore 支持通过 ContentResolver.update()ContentValues 更新 DISPLAY_NAMERELATIVE_PATH 等字段实现重命名或移动文件。例如将图片移动到回收站:

ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/.Trash");
resolver.update(imageUri, values, null, null);

使用 MediaStore.Downloads

Android 10 引入了 MediaStore.Downloads,用于管理下载文件夹下的文件。应用如果要读写下载文件,建议通过 MediaStore 进行。但注意某些设备可能限制对该集合的访问,对非所有者文件可能仍需 SAF。

灰度迁移建议

  1. 分析现有代码中对 File 路径或外部存储根目录的直接操作。
  2. 将核心存储逻辑替换为 MediaStoreSAF 实现。
  3. 在新版本中设置 targetSdkVersion 30+,测试在 Android 11 设备上的行为。
  4. 对于旧数据,提供一次性的“导入”功能,将用户选择的旧文件复制到应用专属目录或 MediaStore 中。

总结

分区存储是 Android 存储机制的一次深刻变革,将“任意访问”转变为“最小权限访问”。开发者需要摒弃直接文件路径的习惯,转而使用 MediaStore 管理媒体文件、SAF 处理文档类文件、应用专属目录存储内部数据。尽早适配不仅能保证应用在较新系统上的正常运行,也是保护用户隐私和提升应用质量的必由之路。