Android 分区存储 Scoped Storage
Android 分区存储(Scoped Storage)完全指南
什么是分区存储
分区存储是 Android 10(API 级别 29)引入的一种存储访问机制,旨在从根本上改变应用访问外部存储文件的方式。它的核心思想是为每个应用提供一个独立的、隔离的存储沙盒,应用默认只能访问自己的专属目录,对外部存储的公共区域(如 DCIM、Pictures、Downloads 等)的访问受到严格限制。这一变化替代了传统的“放任式”文件系统访问模型,显著提升了用户数据的隐私与安全性。
在 Android 10 中,分区存储是可选的,应用可以通过在清单中添加 requestLegacyExternalStorage 标记暂时沿用旧版存储模型。但从 Android 11 开始,所有应用都必须强制使用分区存储,即使声明了旧版兼容标记也无效(除非应用专门且必要地请求了 MANAGE_EXTERNAL_STORAGE 权限)。
为什么需要分区存储
在传统的存储模型中,应用只要获得了 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 权限,就可以随意读写整个外部存储空间。这带来了几大问题:
- 隐私泄露:恶意应用可以轻易扫描并收集用户的所有照片、文档、聊天记录等文件。
- 存储混乱:每个应用都在外部存储的根目录下创建自己的文件夹,卸载应用后残留大量垃圾文件,用户很难清理。
- 安全风险:应用可能修改或删除其他应用的关键数据,导致功能异常或安全漏洞。
分区存储通过隔离和受控的访问,让用户对自己的文件拥有更细粒度的控制权,应用只能拿到它真正需要的文件,而不能“一揽子”获取所有内容。
分区存储下的目录结构
分区存储将外部存储空间划分成几个具有不同访问规则的区域:
-
应用专属目录(App-specific directories)
路径为/Android/data/<包名>/files和/Android/media/<包名>/。
应用无需任何权限即可读写该目录下的文件。当应用被卸载时,该目录下的数据会被自动清理。适合存放内部缓存、配置文件等不希望用户直接访问或与管理的数据。 -
公共共享集合(Shared Collections)
由 MediaStore API 管理的媒体文件集合,例如图片(MediaStore.Images)、视频(MediaStore.Video)、音频(MediaStore.Audio)以及下载文件(MediaStore.Downloads)。
访问这些集合中的文件不需要申请READ_EXTERNAL_STORAGE或WRITE_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_NAME、RELATIVE_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。
灰度迁移建议
- 分析现有代码中对
File路径或外部存储根目录的直接操作。 - 将核心存储逻辑替换为
MediaStore或SAF实现。 - 在新版本中设置
targetSdkVersion 30+,测试在 Android 11 设备上的行为。 - 对于旧数据,提供一次性的“导入”功能,将用户选择的旧文件复制到应用专属目录或 MediaStore 中。
总结
分区存储是 Android 存储机制的一次深刻变革,将“任意访问”转变为“最小权限访问”。开发者需要摒弃直接文件路径的习惯,转而使用 MediaStore 管理媒体文件、SAF 处理文档类文件、应用专属目录存储内部数据。尽早适配不仅能保证应用在较新系统上的正常运行,也是保护用户隐私和提升应用质量的必由之路。