1. 分区存储背景与适配目标

1.1 分区存储的引入背景

Android 10(API 29)正式引入分区存储(Scoped Storage)机制。在此之前,应用通过 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限即可访问设备外部存储中的任意文件。这种宽松的模型导致用户隐私泄露、垃圾文件泛滥。分区存储将存储空间划分为多个隔离区域,应用只能直接访问自己专属的私有目录,访问其他应用文件或公共目录必须通过 MediaStore、SAF(Storage Access Framework)或特定的媒体集合 API。

1.2 适配范围与影响

  • 强制版本:targetSdkVersion ≥ 29 的应用默认启用分区存储;targetSdkVersion 28 及以下仍可暂时使用旧模式(但新应用提交必须 ≥ 29)。
  • 受影响区域:外部存储的公共目录(DCIM、Pictures、Download、Documents、Movies 等)。内部存储私有目录(getFilesDir()、getCacheDir())不受影响。
  • 特殊例外:系统级应用、文件管理器、备份恢复应用可申请 MANAGE_EXTERNAL_STORAGE 全权限。

1.3 主要文件类型与访问方式

文件类型访问方式权限需求
媒体文件(图片、视频、音频)MediaStore API(ContentResolver)READ_MEDIA_IMAGES / VIDEO / AUDIO (Android 13+) 或 READ_EXTERNAL_STORAGE
文档 & 下载文件SAF(ACTION_OPEN_DOCUMENT / ACTION_CREATE_DOCUMENT)或 MediaStore.Downloads无需额外权限(用户主动选择)
应用私有文件Context.getFilesDir(), getCacheDir(), getExternalFilesDir()不需要存储权限
应用专属外部文件Context.getExternalFilesDir(type)不需要存储权限
关键提示: 从 Android 11 起,分区存储不可逆退出。即使申请 MANAGE_EXTERNAL_STORAGE,普通应用也无法绕过 MediaStore 直接访问公共目录。

1.4 适配目标总览

适配的核心目标:将对外部存储公共目录的直接文件路径操作迁移为基于 ContentResolver 的 URI 操作,并充分利用 Android 提供的 MediaStore、SAF 和 FileProvider 等组件。

典型的适配流程包括:

  1. 更新 AndroidManifest 权限声明(区分 Android 13 新细粒度媒体权限)。
  2. 重构文件读取/写入逻辑,使用 MediaStore URI 代替 File 路径。
  3. 集成 SAF 实现用户自定义目录的文件选择与保存。
  4. 测试不同 Android 版本行为差异。

1.5 分区存储架构图

分区存储架构概览 内部存储私有目录 /data/data/包名/ getFilesDir(), getCacheDir() 无需权限,应用独占 外部存储公有目录 DCIM, Pictures, Download等 必须通过 MediaStore / SAF 读写需细粒度权限 隔离 应用专属外部目录 getExternalFilesDir() / 无需权限 MediaStore 内容提供者 ContentResolver URI 操作 SAF (Storage Access Framework) 用户手动选择文件/目录
建议: 在开始适配前,先确认应用当前的 targetSdkVersion 和 minSdkVersion。对于 targetSdkVersion < 29 的应用,可以逐步提升版本并逐步测试。
注意: 分区存储是强制行为,不能通过清单属性 requestLegacyExternalStorage 在 Android 11+ 上绕过。建议尽早适配,以免应用在高版本设备上崩溃或功能受限。

1.6 典型适配 API 示例

以下示例展示如何通过 MediaStore 向公共 Pictures 目录写入一张图片:

ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "my_photo.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.put(MediaStore.Images.Media.IS_PENDING, 1);
}
Uri uri = context.getContentResolver().insert(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try (OutputStream out = context.getContentResolver().openOutputStream(uri)) {
    // 将图片数据写入 out
} catch (IOException e) {
    e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.clear();
    values.put(MediaStore.Images.Media.IS_PENDING, 0);
    context.getContentResolver().update(uri, values, null, null);
}

1.7 适配清单检查表

  • ☐ 升级 targetSdkVersion 至 29 或更高。
  • ☐ 移除 requestLegacyExternalStorage(Android 11+ 无效)。
  • ☐ 检查所有直接文件路径访问,替换为 MediaStore / SAF / FileProvider。
  • ☐ 申请细粒度媒体权限(Android 13+ 使用 READ_MEDIA_IMAGES 等)。
  • ☐ 处理多版本兼容(minSdk < 29 的应用需保留旧路径逻辑)。

以上适配目标为后续实现奠定基础。