1. 存储权限的版本兼容问题概述
在Android应用开发中,存储权限管理一直是开发者需要重点关注的兼容性问题。从Android 6.0(API 23)引入运行时权限开始,到Android 10(API 29)引入分区存储(Scoped Storage),再到Android 11(API 30)对分区存储的进一步调整,存储权限的处理逻辑经历了多次重大变更。
我最近在维护一个需要长期支持Android 5.0到Android 13的应用时,深刻体会到正确处理存储权限版本兼容的重要性。不同Android版本对存储权限的处理差异,可能导致应用在部分设备上无法正常工作,特别是涉及文件读写、图片选择、备份恢复等核心功能时。
2. Android存储权限的版本演进
2.1 Android 5.1及之前版本
在Android 5.1(API 22)及更早版本中,应用只需要在AndroidManifest.xml中声明存储权限即可:
xml复制<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
这些权限会在安装时一次性授予,用户无法在运行时撤销。这种设计虽然简单,但也带来了隐私和安全问题。
2.2 Android 6.0到Android 9
从Android 6.0(API 23)开始,Google引入了运行时权限模型。存储权限被归类为"危险权限",需要在运行时动态申请:
java复制if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_CODE_STORAGE);
}
这个阶段,虽然需要运行时申请,但一旦授予权限,应用就可以访问整个外部存储(包括其他应用的文件)。
2.3 Android 10的分区存储
Android 10(API 29)引入了分区存储(Scoped Storage)概念,这是存储权限管理的一次重大变革:
- 默认情况下,应用只能访问自己的私有目录和特定类型的媒体文件
- 访问其他文件需要使用系统文件选择器
- 即使拥有READ/WRITE_EXTERNAL_STORAGE权限,也无法直接访问非媒体文件
可以通过在AndroidManifest.xml中添加以下属性来暂时禁用分区存储(但不推荐长期使用):
xml复制<application
android:requestLegacyExternalStorage="true"
...>
2.4 Android 11及更高版本
Android 11(API 30)进一步强化了分区存储:
- requestLegacyExternalStorage属性对目标API为30的应用无效
- 引入了MANAGE_EXTERNAL_STORAGE权限,用于需要广泛文件访问的特殊应用
- 访问媒体文件也需要使用MediaStore API
3. 实现版本兼容的存储权限管理
3.1 权限检查与申请的最佳实践
在实际开发中,我们需要根据不同Android版本采取不同的权限处理策略:
java复制private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager() ||
(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
} else {
true
}
}
3.2 分区存储下的文件访问
在启用分区存储的环境下,访问文件需要使用正确的API:
- 访问应用私有文件:使用getExternalFilesDir()或getExternalCacheDir()
- 访问媒体文件:使用MediaStore API
- 访问其他文件:使用Storage Access Framework (SAF) 的文件选择器
java复制// 使用MediaStore查询图片
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
val query = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)
3.3 处理权限拒绝场景
用户可能会拒绝存储权限,我们需要优雅地处理这种情况:
java复制override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_CODE_STORAGE -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
proceedWithFileOperation()
} else {
// 权限被拒绝
if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 用户选择了"拒绝",可以再次解释为什么需要权限
showPermissionExplanationDialog()
} else {
// 用户选择了"拒绝并不再询问",引导用户去设置中开启
showGoToSettingsDialog()
}
}
}
}
}
4. 常见问题与解决方案
4.1 兼容性问题的典型表现
在实际开发中,我们经常会遇到以下兼容性问题:
- 在Android 10+设备上,即使有存储权限也无法访问某些文件
- 应用升级到目标API 30后,原有的文件访问方式突然失效
- 不同厂商的设备对存储权限的实现有差异
- 从相册选择图片或保存文件时出现权限问题
4.2 文件路径处理的兼容方案
避免使用硬编码的文件路径,而是使用兼容的API:
java复制// 不推荐
File file = new File("/sdcard/MyApp/files/document.pdf");
// 推荐
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "document.pdf");
对于需要共享的文件,使用FileProvider:
xml复制<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
4.3 测试策略
为确保存储权限在所有目标版本上正常工作,应该:
- 在Android 5.x、6.x、9.x、10.x、11+设备上分别测试
- 测试权限授予、拒绝、永久拒绝等各种场景
- 测试应用升级后的权限保持情况
- 测试不同厂商的设备(特别是华为、小米等有深度定制的ROM)
5. 实际项目中的经验分享
5.1 渐进式权限请求
不要一开始就请求所有权限,而是根据功能需要逐步请求:
java复制fun requestStoragePermissionIfNeeded(operation: () -> Unit) {
when {
hasStoragePermission() -> operation()
shouldShowRequestPermissionRationale() -> showExplanationDialog {
requestStoragePermission()
}
else -> requestStoragePermission()
}
}
5.2 权限封装库的使用
考虑使用第三方权限库简化处理,如:
- PermissionsDispatcher
- EasyPermissions
- RxPermissions
这些库可以简化权限检查和请求的代码,但要注意它们可能不会自动处理所有版本差异。
5.3 备份与恢复的兼容处理
处理应用数据备份时,特别注意:
- 不要依赖外部存储保存重要数据
- 使用Android的自动备份功能(Auto Backup)
- 对于大文件,考虑使用云存储服务
xml复制<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
...>
5.4 处理厂商定制ROM的问题
某些厂商的Android版本对存储权限有特殊处理:
- 华为EMUI:可能需要额外检查"存储权限"和"文件管理权限"
- 小米MIUI:可能需要用户手动开启"显示悬浮窗"等看似无关的权限
- OPPO ColorOS:后台时可能限制文件访问
针对这些情况,可以在应用启动时检查必要的权限,并引导用户正确设置。
