Android 的权限管理经历了重大变革:
-
安装时授权(Android 5.0 之前) :
- 应用安装时一次性请求所有权限
- 用户只能选择"全部接受"或"放弃安装"
- 导致过度授权和隐私泄露风险
-
运行时权限(Runtime Permissions,Android 6.0+) :
- 引入危险权限(Dangerous Permissions) 概念
- 应用在运行时请求关键权限
- 用户可以按需授予或拒绝单个权限
- 存储权限属于典型的危险权限
// 运行时权限请求示例
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 解释为何需要权限(可选)
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_EXTERNAL_STORAGE)) {
// 显示解释UI
}
// 实际请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_STORAGE);
}
二、存储权限全景图
1. 核心权限详解
| 权限 | 引入版本 | 作用范围 | 分区存储下的变化 | 使用场景 |
|---|---|---|---|---|
| READ_EXTERNAL_STORAGE | API 16 | 读取公共存储区域 | API 29+ 仅能访问媒体文件 | 读取公共图片/视频/音频文件 |
| WRITE_EXTERNAL_STORAGE | API 4 | 写入公共存储区域 | API 29+ 仅能修改应用自己创建的文件 | 保存文件到公共目录 |
| READ_MEDIA_IMAGES | API 33 | 读取图片和照片 | 替代 READ_EXTERNAL_STORAGE 的图片部分 | 访问用户相册 |
| READ_MEDIA_VIDEO | API 33 | 读取视频文件 | 替代 READ_EXTERNAL_STORAGE 的视频部分 | 访问用户视频 |
| READ_MEDIA_AUDIO | API 33 | 读取音频文件 | 替代 READ_EXTERNAL_STORAGE 的音频部分 | 访问用户音乐库 |
| MANAGE_EXTERNAL_STORAGE | API 30 | 访问所有文件(特殊权限) | 绕过分区存储限制 | 文件管理器、备份应用等 |
2. 权限与Android版本的兼容矩阵
图表
3. MANAGE_EXTERNAL_STORAGE:最后的核选项
特性:
- 允许应用访问设备上所有文件(包括其他应用的文件)
- 用户必须在系统设置中手动启用
- Google Play 有严格的审核政策
- 仅适用于文件管理器、备份应用等特定类型
声明方式:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
检查是否已授权:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
// 引导用户前往设置页面
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
使用忠告: 除非绝对必要,否则不要使用此权限。Google Play 会严格审查使用此权限的应用,普通应用很可能被拒绝上架。
三、运行时权限最佳实践
1. 完整的权限请求流程
2. 权限请求代码模板
// 检查权限状态
when {
// 情况1:权限已授予
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED -> {
performStorageOperation()
}
// 情况2:需要显示解释
ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permission
) -> {
showRationaleDialog(permission)
}
// 情况3:直接请求权限
else -> {
ActivityCompat.requestPermissions(
activity,
arrayOf(permission),
requestCode
)
}
}
// 处理权限结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_CODE -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
} else {
// 处理拒绝
if (!shouldShowRequestPermissionRationale(permission)) {
// 用户勾选"不再询问"
showSettingsDialog()
} else {
// 普通拒绝
showDenialMessage()
}
}
}
}
}
3. 处理用户拒绝的优雅方案
- 首次拒绝:轻量提示,解释权限必要性
- 再次拒绝:提供更详细的解释和引导
- 勾选"不再询问" :引导用户前往应用设置手动开启权限
fun showSettingsDialog() {
AlertDialog.Builder(this)
.setTitle("需要存储权限")
.setMessage("您已永久拒绝存储权限,请在设置中手动启用")
.setPositiveButton("去设置") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
.setNegativeButton("取消", null)
.show()
}
四、权限与分区存储的协同规则
1. 权限作用域的变化
Android 10(API 29)之前:
READ_EXTERNAL_STORAGE:可访问所有公共文件WRITE_EXTERNAL_STORAGE:可写入所有公共区域
Android 10+(分区存储):
- 即使拥有权限,也无法通过文件路径直接访问公共文件
- 访问媒体文件必须通过MediaStore API
- 写入位置受限:只能写入特定媒体集合
2. 各区域访问权限总结
| 存储区域 | 访问方式 | 所需权限 | 备注 |
|---|---|---|---|
| 内部存储 | 直接访问 | 无需权限 | 应用私有 |
| 应用专属外部存储 | 直接访问 | 无需权限 (API≥19) | 卸载时自动清理 |
| 公共媒体文件 | MediaStore API | API 33+: READ_MEDIA_* API 29-32: READ_EXTERNAL_STORAGE | 分区存储强制要求 |
| 公共非媒体文件 | SAF | 无 | 需用户主动选择 |
| 所有文件 | 直接访问 | MANAGE_EXTERNAL_STORAGE | 需用户手动开启 |
3. 真实场景权限需求
-
保存图片到相册:
// 无需 WRITE_EXTERNAL_STORAGE! val values = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "my_image.jpg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) } } val resolver = context.contentResolver val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) -
读取用户选择的文件:
// 使用SAF不需要声明任何权限 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, REQUEST_CODE_OPEN);
五、常见陷阱与解决方案
1. 权限请求的时机错误
错误做法: 应用启动时立即请求所有权限
正确方案: 在用户执行需要权限的操作时才请求(按需请求)
2. 忽略"不再询问"状态
解决方案:
if (!shouldShowRequestPermissionRationale(permission)) {
// 引导用户前往设置
showSettingsDialog();
}
3. 适配不同Android版本时权限处理混乱
健壮方案:
fun requestNeededPermissions() {
val permissionsToRequest = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13+ 使用细化权限
if (!hasPermission(Manifest.permission.READ_MEDIA_IMAGES)) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_IMAGES)
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10-12 使用READ_EXTERNAL_STORAGE
if (!hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) {
permissionsToRequest.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
// Android 9及以下使用老模式
if (permissionsToRequest.isNotEmpty()) {
requestPermissions(permissionsToRequest.toTypedArray(), REQUEST_CODE)
}
}
4. 过度依赖 MANAGE_EXTERNAL_STORAGE
替代方案:
- 使用应用专属存储存放私有文件
- 使用MediaStore API访问媒体文件
- 使用SAF让用户选择文件
六、权限测试清单
-
基础流程测试:
- 首次请求权限 → 允许 → 功能正常
- 首次请求权限 → 拒绝 → 优雅降级
- 二次请求权限 → 允许 → 功能正常
- 二次请求权限 → 拒绝 → 不再提示
-
特殊场景测试:
- 勾选"不再询问"后的处理
- 权限在设置中手动启用/禁用
- 低版本Android(<6.0)的权限处理
- Android 10-12 与 Android 13+ 的权限差异
-
边界情况测试:
- 权限允许后撤销
- 同时请求多个权限
- 权限请求过程中配置变更(旋转屏幕)
总结
理解并正确使用Android存储权限,是你构建健壮、合规应用的关键一步。记住这些核心原则:
- 最小权限原则:只请求真正需要的权限
- 按需请求:在用户需要时请求权限
- 优雅降级:妥善处理权限拒绝情况
- 尊重分区存储:优先使用MediaStore和SAF
- 远离MANAGE_EXTERNAL_STORAGE:除非绝对必要