2. 兼容旧版存储策略:配置 requestLegacyExternalStorage、判断 targetSdkVersion 与降级处理

2.1 原理:为什么需要兼容旧版?

Android 10(API 29)引入了分区存储(Scoped Storage),应用默认只能访问自己专属的目录及公共媒体文件。但许多旧应用依赖 Environment.getExternalStorageDirectory() 等全局路径,直接改造成本过高。为此 Google 提供了 requestLegacyExternalStorage 临时旗标,允许在 targetSdkVersion = 29 时仍保留旧版存储行为,给开发者 1 年迁移窗口。Android 11(API 30)开始强制关闭该旗标,应用必须完整适配分区存储。

若应用 targetSdkVersion 低于 29,系统会自动使用旧版存储策略,无需额外配置;若 targetSdkVersion = 29,需在 Manifest 声明 requestLegacyExternalStorage=true 才能继续使用传统文件访问方式;targetSdkVersion ≥ 30 时该旗标被系统忽略。

核心矛盾:在保证新设备合规的同时,不能破坏旧版用户已有功能。因此需要动态判断 targetSdkVersion,并准备两种降级路径:

  • 应用自身 targetSdkVersion 较低时(<29)直接使用旧 API
  • 应用 targetSdkVersion=29 且未配置旗标时,降级为分区存储部分能力
  • 应用 targetSdkVersion≥30 时,严格走分区存储流程,拒绝旧 API 调用

2.2 实现流程与决策树

以下流程图展示了应用针对不同 targetSdkVersion 和 Android 系统版本的存储策略选择逻辑:

兼容存储策略决策流程 应用启动 检查 targetSdkVersion targetSdkVersion < 29 系统自动旧版 targetSdkVersion = 29 targetSdkVersion ≥ 30 强制分区存储 是否配置 requestLegacyExternalStorage 启用旧版兼容模式 传统 global 路径有效 降级为分区存储 只能访问沙箱和媒体 说明:targetSdkVersion < 29 在 Android 10+ 设备上仍使用旧存储;targetSdkVersion=29 需声明 flag 才兼容;target=30 则完全废弃 flag。

2.3 配置与代码实现

2.3.1 AndroidManifest.xml 配置

将以下属性加入 <application> 节点(仅当 targetSdkVersion = 29 时有效):

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

若 targetSdkVersion ≥ 30 或 < 29,该属性将被系统忽略。

2.3.2 代码判断当前存储策略

运行时可以通过 Environment.isExternalStorageLegacy() 或反射检查当前是否启用旧版兼容。示例如下:

fun isLegacyExternalStorage(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        Environment.isExternalStorageLegacy()
    } else {
        true // Android 9 及以下默认旧版
    }
}

2.3.3 根据 targetSdkVersion 做降级处理

在文件读写操作入口增加分支:

fun getExternalFilesPath(context: Context): String {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        // Android 11+ 必须使用分区存储 API
        context.getExternalFilesDir(null)?.absolutePath ?: ""
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Android 10: 根据 flag 决定
        if (Environment.isExternalStorageLegacy()) {
            Environment.getExternalStorageDirectory().absolutePath
        } else {
            context.getExternalFilesDir(null)?.absolutePath ?: ""
        }
    } else {
        // 旧设备
        Environment.getExternalStorageDirectory().absolutePath
    }
}
提示:即使配置了 requestLegacyExternalStorage=true,在 Android 11+ 设备上系统会完全忽略该设置,必须提前迁移到分区存储。建议利用此过渡期完成适配,并在 Manifest 中同时声明 android:maxSdkVersion="29" 来限制旧版 API 的最大系统版本。

2.4 调试与部署验证

系统版本targetSdkVersionrequestLegacyExternalStorage期望行为
Android 1029true旧版存储可用,全局路径正常
Android 1029false 或未配置分区存储生效,旧 API 抛出 FileNotFoundException
Android 1030任意分区存储生效(flag 被忽略)
Android 11+29任意分区存储生效(flag 被忽略)
Android 9 以下任意忽略旧版存储,不受影响

2.5 常见错误与稳定性优化

  • 错误 1:在 Android 10 上 targetSdkVersion=29 且未配置 flag 时调用 getExternalStorageDirectory() 报错 → 添加 flag 或改用分区 API。
  • 错误 2:在 Android 11+ 上仍然依赖旧版路径 → 升级 targetSdkVersion 至 30 并彻底适配 Scoped Storage。
  • 错误 3:降级时未处理文件访问权限 → 在 Android 10 分区模式下必须通过 MediaStore 或 SAF 授予临时权限。
稳定性建议:
1. 在 Application.onCreate() 中记录当前存储模式,避免重复判断;
2. 所有文件路径统一封装在工具类中,方便后续切换;
3. 为旧设备保留完整的旧版代码分支,待用户升级系统后自然切换到分区存储;
4. 使用 Build.VERSION.SDK_INT >= Build.VERSION_CODES.R 作为硬界线,避免在 Android 11+ 上执行任何旧版文件操作。

2.6 性能与内存考量

降级分支本身不会引入额外性能开销,但要注意:

  • 在旧版存储模式下,全局扫描整个 SD 卡可能耗时较长,建议限制扫描范围或使用缓存。
  • 分区存储模式下 MediaStore 查询性能优于遍历文件系统,但需要合理使用 URI 缓存。
  • Android 10 的 requestLegacyExternalStorage 属于一次性系统标志,应用启动后不再变化,不需要运行时反复检查,将其值保存为全局静态变量即可。