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 系统版本的存储策略选择逻辑:
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 调试与部署验证
| 系统版本 | targetSdkVersion | requestLegacyExternalStorage | 期望行为 |
|---|---|---|---|
| Android 10 | 29 | true | 旧版存储可用,全局路径正常 |
| Android 10 | 29 | false 或未配置 | 分区存储生效,旧 API 抛出 FileNotFoundException |
| Android 10 | 30 | 任意 | 分区存储生效(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属于一次性系统标志,应用启动后不再变化,不需要运行时反复检查,将其值保存为全局静态变量即可。