4. Android 10 兼容适配策略:requestLegacyExternalStorage 标志与迁移路线

Android 10(API 29)首次引入分区存储(Scoped Storage),但为降低生态冲击,Google 提供了临时逃生口——requestLegacyExternalStorage 清单标志。本章从原理、配置、验证到迁移路线,完整拆解这一兼容策略的落地细节。

4.1 原理:为什么需要“临时标志”?

分区存储要求所有文件访问必须通过 MediaStore 或 Storage Access Framework,但大量存量 App 依赖直接路径操作。为此,Android 10 允许在 AndroidManifest.xml 中添加 android:requestLegacyExternalStorage="true",使应用临时恢复传统外部存储访问模式。

⚠️ 标志作用范围
  • 仅对 targetSdkVersion=29 的应用生效。
  • Android 11(API 30)起,该标志完全失效,即使设置也无法规避分区存储。
  • Google Play 从 2021 年起逐步强制要求使用分区存储。

4.2 清单配置:添加 requestLegacyExternalStorage

在 AndroidManifest.xml 的 <application> 标签内添加属性:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <application
        android:requestLegacyExternalStorage="true"
        ... />

</manifest>
属性值 行为 适用场景
true 应用保持传统文件路径访问(如 /sdcard/) 存量 App 优先采用,争取迁移时间
false(默认) 立即启用分区存储,仅能通过 MediaStore/SAF 访问 全新应用或已完成适配的应用
💡 最佳实践
如果 targetSdkVersion=29 且短期内无法完成全量迁移,务必添加此标志。否则应用访问 /sdcard/ 中的文件时会抛出 FileNotFoundException。

4.3 测试验证方法

4.3.1 使用 adb 检查标志生效状态

安装应用后,通过 dumpsys 查看应用的存储模式:

adb shell dumpsys package com.example.app | grep requestLegacyExternalStorage

输出示例:

requestLegacyExternalStorage=true

4.3.2 功能验证清单

  • 路径写文件:context.getExternalFilesDir() 或 Environment.getExternalStorageDirectory() 下创建/读取文件是否成功。
  • MediaStore 关联:原有通过 ContentResolver 插入图片的逻辑是否仍能正确获取 Uri。
  • FileProvider 权限:共享文件时 Intent 是否携带正确 Uri 与 FLAG_GRANT_READ_URI_PERMISSION。
  • 日志检查:运行应用后过滤 ScopedStorage 或 FileUtils 相关 logcat 输出。

4.3.3 在 Android 10 模拟器/真机上重点测试

  • 模拟器:创建 API 29 系统镜像,关闭“设置 → 开发者选项 → 强制启用分区存储”。
  • 真机:确保系统版本为 Android 10,且 targetSdkVersion=29。
📌 临时禁用分区存储(仅调试用)
在开发者选项中开启“强制启用分区存储”会绕开 requestLegacyExternalStorage,可用于验证应用在分区存储下的表现。

4.4 迁移路线:从传统存储到分区存储

下图展示一条经过验证的迁移路径,帮助团队平滑过渡:

迁移路线流程图 ① 添加临时标志 requestLegacyExternalStorage ② 识别路径调用点 Environment / File API ③ 替换为 MediaStore ContentResolver ④ 适配 SAF 访问 ACTION_OPEN_DOCUMENT ⑤ 移除临时标志 同时维护 ContentProvider 和 FileProvider 的 Uri 兼容性

4.4.1 详细步骤说明

  1. 添加临时标志:在 AndroidManifest.xml 中设置 android:requestLegacyExternalStorage="true",确保当前功能可用。
  2. 识别路径调用点:全局搜索 Environment.getExternalStorageDirectory()、File(…) 构造、getAbsolutePath() 等直接路径操作。
  3. 替换为 MediaStore:将文件读写改为 ContentResolver.insert()、openOutputStream(),图片/视频/音频等媒体文件使用标准 MediaStore.Images 等 API。
  4. 适配 SAF(Storage Access Framework):非媒体文件(如文档、自定义格式)通过 ACTION_OPEN_DOCUMENT/ACTION_CREATE_DOCUMENT 获取 Uri。
  5. 移除临时标志:在所有路径都替换完成后,将 targetSdkVersion 升级至 30+ 并删除 requestLegacyExternalStorage 属性。

4.5 常见错误与排查

错误现象 根因 解决
访问 /sdcard/ 抛 FileNotFoundException 未设置 requestLegacyExternalStorage 或 targetSdkVersion>=30 添加标志;若已到 30+,必须迁移至 MediaStore
通过 Uri 读取图片失败 未授予 READ_EXTERNAL_STORAGE 或缺少 Uri 权限 动态请求权限;或使用 takePersistableUriPermission()
调用 Environment.getExternalStoragePublicDirectory() 返回 null 分区存储下该方法已废弃,返回 null 改用 Context.getExternalFilesDir() 或 SAF

4.6 性能与稳定性优化

  • 批量操作:使用 ContentProviderOperation 批量插入/更新媒体文件,减少 Binder 调用。
  • Uri 缓存:对频繁访问的媒体 Uri 做内存缓存,避免重复查询。
  • 异步写入:MediaStore 写入操作放在后台线程,避免主线程 ANR。
  • 谨慎移除临时标志:在移除 requestLegacyExternalStorage 前,先进行完整的回归测试(包括文件共享、备份恢复等场景)。
🚀 迁移检查清单
  • ✓ 所有 File 路径操作已替换为 Uri 操作
  • ✓ AndroidManifest.xml 中的 requestLegacyExternalStorage 已移除
  • ✓ targetSdkVersion 已升至 30 或以上
  • ✓ 在 Android 10、11、12 真机上完成兼容性测试