3. 分区存储核心原理:MediaStore 机制、沙箱目录结构、SAF 作用、直接文件路径限制
自 Android 10(API 29)起,分区存储成为强制访问模型。应用不再拥有对整个外部存储的完全文件路径访问权限,而是被限制在三个互不重叠的访问通道内。本章将从原理层剖析这四个核心概念,并给出对应的实现基线与边界条件。
3.1 MediaStore 机制:统一媒体文件入口
MediaStore 是 Android 系统提供的内容提供者(ContentProvider),对外暴露 content:// URI。应用通过 ContentResolver 操作 MediaStore 元数据表来读写共享媒体文件(图片、视频、音频)。
| MediaStore 类型 | 对应 URI | 说明 |
|---|---|---|
| Images | content://media/external/images | JPEG、PNG、WebP 等 |
| Video | content://media/external/video | MP4、3GP、MKV 等 |
| Audio | content://media/external/audio | MP3、FLAC、WAV 等 |
| Downloads | content://media/external/downloads | 非媒体通用文件(Android 10+) |
关键 API 示例:通过 MediaStore 插入图片并获取 URI。
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo_2024.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/AppAlbum");
Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
// 通过 uri 打开输出流写入文件数据
OutputStream os = contentResolver.openOutputStream(uri);
3.2 沙箱目录结构:应用专属隔离空间
每个应用在外部存储中拥有独立的沙箱目录,无需任何权限即可读写。该目录位于 Android/data/<package_name>/ 下,系统会在应用卸载时自动清除。
- 通过
context.getExternalFilesDir(type)获取专有目录,例如getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)。 - 通过
context.getExternalCacheDir()获取缓存目录。 - 沙箱目录 不可 通过 MediaStore 查询,只能使用
FileAPI 直接读写。 - Android 11 起,沙箱目录外的文件即使应用有
READ_EXTERNAL_STORAGE也默认不可访问。
常见错误:在沙箱目录中使用
Environment.getExternalStorageDirectory() 拼接路径会触发 FileNotFoundException。应始终使用 context.getExternalFilesDir() 获取根路径。
3.3 SAF(Storage Access Framework)作用:跨沙箱文件选择
SAF 提供了一套标准的系统 UI 让用户选择文档或目录,应用获得用户授予的 URI 读写权限。它不依赖文件路径,完全基于 URI 和 DocumentFile 操作。
| SAF 关键动作 | Intent Action | 返回 URI 类型 |
|---|---|---|
| 选择单个文件 | Intent.ACTION_OPEN_DOCUMENT | 单文件 URI |
| 创建新文件 | Intent.ACTION_CREATE_DOCUMENT | 新文件 URI |
| 选择目录(树) | Intent.ACTION_OPEN_DOCUMENT_TREE | 树 URI |
持久化权限(Take Persistable Permissions):获取返回的 URI 后调用
contentResolver.takePersistableUriPermission(uri, FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION) 可跨应用重启保留权限。
3.4 直接文件路径限制
自 Android 10 起,禁止使用 Environment.getExternalStorageDirectory() 和 /sdcard/ 硬编码路径访问共享存储中的任意文件。该限制使应用必须通过 MediaStore、SAF 或沙箱目录这三种合法通道操作文件。
- 读取:只能通过 MediaStore URI 或 SAF URI 读取图片、视频、音频等。
- 写入:只能通过 MediaStore 插入新记录或通过 SAF 创建文档。
- 例外:沙箱目录(
getExternalFilesDir)不受路径限制,但仅限应用自身数据。
兼容适配警告:强制使用旧路径的代码在 Android 10+ 上会触发
FileNotFoundException 或 SecurityException。建议使用 Environment.isExternalStorageLegacy() 判断并区分适配路径。
3.5 三通道对比矩阵
| 特性 | MediaStore | 沙箱目录 | SAF |
|---|---|---|---|
| 所需权限 | READ_EXTERNAL_STORAGE (Android 13 + 细粒度) | 无需权限 | 无(用户交互授权) |
| 访问范围 | 共享媒体文件(图片、视频、音频) | 仅本应用私有数据 | 用户选择的任意文件/目录 |
| 文件操作方式 | ContentResolver openInputStream/openOutputStream | 标准 Java File API | DocumentFile + ParcelFileDescriptor |
| 持久化访问 | URI 长期可用(除非记录被删除) | 应用卸载即清除 | 需调用 takePersistableUriPermission |
| 是否支持路径操作 | 否(仅 URI) | 是(真实文件路径) | 否(仅 URI) |
3.6 性能与稳定性注意点
- MediaStore 批量操作:使用
ContentProviderOperation批量插入/更新可提升效率,避免逐行事务。 - SAF 树 URI 递归:遍历大量文件时应使用
DocumentFile.listFiles()并在后台线程执行,防止 UI 卡顿。 - 沙箱目录空间管理:应用应主动清理无用文件,避免占用过多外存触发系统清理弹窗。
- 直接路径检测:在旧代码中搜索
/sdcard、Environment.getExternalStorageDirectory()等模式,尽早替换。
调试建议:在 Android 10+ 模拟器或真机上用
adb shell dumpsys media 查看 MediaStore 数据库状态;使用 adb shell ls -la /sdcard/Android/data/<package>/ 验证沙箱目录。