在 Android 系统中,存储权限的适配一直是开发者面临的复杂问题,尤其是从 Android 11(API 30) 开始引入的 作用域存储(Scoped Storage) 机制,彻底改变了应用访问外部存储的规则。以下从权限变化、适配策略、代码示例和注意事项等方面进行详细解析。
一、存储权限的历史演进
Android 版本 | 特性变化 |
---|---|
< 6.0 | 安装时申请权限(READ/WRITE_EXTERNAL_STORAGE ),用户无法动态拒绝。 |
6.0+ | 运行时动态申请权限(危险权限)。 |
10.0+ | 引入作用域存储(Scoped Storage),限制应用访问其他应用文件。 |
11.0+ | 强制启用作用域存储,READ_EXTERNAL_STORAGE 仅允许访问媒体文件。 |
13.0+ | 细化媒体权限(READ_MEDIA_IMAGES 、READ_MEDIA_VIDEO 等)。 |
二、核心权限说明
-
通用存储权限
READ_EXTERNAL_STORAGE
:允许读取外部存储(Android 11+ 仅限媒体文件)。WRITE_EXTERNAL_STORAGE
:允许写入外部存储(Android 10+ 已废弃,仅兼容旧设备)。
-
作用域存储下的替代方案
- 应用专属目录 :无需权限,直接访问
Context.getExternalFilesDir()
或Context.getExternalCacheDir()
。 - 媒体文件访问 :通过
MediaStore
API 读写图片、视频、音频(需动态权限)。 - 文档访问 :通过
ACTION_OPEN_DOCUMENT
或ACTION_CREATE_DOCUMENT
让用户选择文件。
- 应用专属目录 :无需权限,直接访问
-
特殊权限
MANAGE_EXTERNAL_STORAGE
(Android 11+):允许访问所有文件(需用户手动授权,Google Play 审核严格)。
三、适配策略与代码示例
1. 适配不同 Android 版本的权限请求
kotlin
// 检查权限是否已授予
fun checkStoragePermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13+ 需要细化权限(如 READ_MEDIA_IMAGES)
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11-12 使用 READ_EXTERNAL_STORAGE(仅媒体文件)
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
} else {
// Android 10 及以下同时请求读写权限
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
}
}
// 动态请求权限
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 权限已授予
} else {
// 处理拒绝逻辑
}
}
// 根据版本请求权限
fun requestStoragePermission() {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Manifest.permission.READ_EXTERNAL_STORAGE
} else {
Manifest.permission.WRITE_EXTERNAL_STORAGE
}
requestPermissionLauncher.launch(permission)
}
2. 使用作用域存储访问文件
kotlin
// 写入图片到公共媒体目录
fun saveImageToGallery(context: Context, bitmap: Bitmap) {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "image_${System.currentTimeMillis()}.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, contentValues)
uri?.let {
resolver.openOutputStream(it)?.use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
}
}
}
// 访问应用专属目录(无需权限)
val appSpecificDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val file = File(appSpecificDir, "example.txt")
file.writeText("Hello, Scoped Storage!")
3. 使用 SAF(存储访问框架)让用户选择文件
kotlin
// 启动文档选择器
val openDocumentLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let {
// 通过 ContentResolver 读取文件内容
context.contentResolver.openInputStream(it)?.use { stream ->
val text = stream.bufferedReader().readText()
}
}
}
// 触发选择文件
openDocumentLauncher.launch(arrayOf("text/plain"))
四、注意事项与兼容问题
-
Android 10+ 的存储限制
- 废弃方法 :禁止直接使用
Environment.getExternalStorageDirectory()
或绝对路径。 - 媒体文件访问 :必须通过
MediaStore
API 访问公共目录(图片、视频等)。
- 废弃方法 :禁止直接使用
-
MANAGE_EXTERNAL_STORAGE 的慎用
-
需要用户手动在系统设置中授权(无法通过弹窗请求)。
-
Google Play 限制使用场景(仅允许文件管理、备份类应用)。
-
适配代码示例:
xml<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
kotlinif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) { // 已授权 } else { // 跳转到设置页面 val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) startActivity(intent) }
-
-
Android 11 的文件路径访问限制
- 无法通过
File
API 访问其他应用的文件目录(如 WhatsApp 的媒体文件夹)。 - 解决方法:使用
MediaStore
或 SAF 访问。
- 无法通过
-
Android 13 的细化媒体权限
-
需要根据文件类型请求权限(如
READ_MEDIA_IMAGES
、READ_MEDIA_VIDEO
)。 -
适配代码:
kotlinval permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { arrayOf(Manifest.permission.READ_MEDIA_IMAGES) } else { arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) } requestPermissions(permissions, REQUEST_CODE)
-
五、兼容方案总结
场景 | 推荐方案 |
---|---|
读写应用专属文件 | 使用 Context.getExternalFilesDir() ,无需权限。 |
访问公共媒体文件(图片、视频) | 通过 MediaStore API + 动态权限(READ_MEDIA_* 或 READ_EXTERNAL_STORAGE )。 |
用户选择文件或目录 | 使用 SAF(ACTION_OPEN_DOCUMENT /ACTION_CREATE_DOCUMENT )。 |
需要全局文件访问(特殊场景) | 申请 MANAGE_EXTERNAL_STORAGE ,但需通过 Google Play 审核。 |
通过合理使用作用域存储 API 和动态权限机制,开发者可以在保证用户隐私的同时,实现存储功能的跨版本兼容。
更多分享
- Android跨进程通信中的关键字详解:in、out、inout、oneway
- 一文带你吃透Kotlin协程的launch()和async()的区别
- Kotlin 委托与扩展函数------新手入门
- Kotlin 作用域函数(let、run、with、apply、also)的使用指南
- 一文带你吃透Kotlin中 lateinit 和 by lazy 的区别和用法
- Kotlin 扩展方法(Extension Functions)使用详解
- Kotlin 中 == 和 === 的区别
- Kotlin 操作符与集合/数组方法详解------新手指南
- Kotlin 中 reified 配合 inline 不再被类型擦除蒙蔽双眼
- Kotlin Result 类型扩展详解 ------ 新手使用指南