Android 存储权限兼容问题详解 —— 新手指南

在 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_IMAGESREAD_MEDIA_VIDEO 等)。

二、核心权限说明

  1. 通用存储权限

    • READ_EXTERNAL_STORAGE:允许读取外部存储(Android 11+ 仅限媒体文件)。
    • WRITE_EXTERNAL_STORAGE:允许写入外部存储(Android 10+ 已废弃,仅兼容旧设备)。
  2. 作用域存储下的替代方案

    • 应用专属目录 :无需权限,直接访问 Context.getExternalFilesDir()Context.getExternalCacheDir()
    • 媒体文件访问 :通过 MediaStore API 读写图片、视频、音频(需动态权限)。
    • 文档访问 :通过 ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT 让用户选择文件。
  3. 特殊权限

    • 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"))

四、注意事项与兼容问题

  1. Android 10+ 的存储限制

    • 废弃方法 :禁止直接使用 Environment.getExternalStorageDirectory() 或绝对路径。
    • 媒体文件访问 :必须通过 MediaStore API 访问公共目录(图片、视频等)。
  2. MANAGE_EXTERNAL_STORAGE 的慎用

    • 需要用户手动在系统设置中授权(无法通过弹窗请求)。

    • Google Play 限制使用场景(仅允许文件管理、备份类应用)。

    • 适配代码示例:

      xml 复制代码
      <!-- AndroidManifest.xml -->
      <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
      kotlin 复制代码
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
          Environment.isExternalStorageManager()) {
          // 已授权
      } else {
          // 跳转到设置页面
          val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
          startActivity(intent)
      }
  3. Android 11 的文件路径访问限制

    • 无法通过 File API 访问其他应用的文件目录(如 WhatsApp 的媒体文件夹)。
    • 解决方法:使用 MediaStore 或 SAF 访问。
  4. Android 13 的细化媒体权限

    • 需要根据文件类型请求权限(如 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO)。

    • 适配代码:

      kotlin 复制代码
      val 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 和动态权限机制,开发者可以在保证用户隐私的同时,实现存储功能的跨版本兼容。

更多分享

  1. Android跨进程通信中的关键字详解:in、out、inout、oneway
  2. 一文带你吃透Kotlin协程的launch()和async()的区别
  3. Kotlin 委托与扩展函数------新手入门
  4. Kotlin 作用域函数(let、run、with、apply、also)的使用指南
  5. 一文带你吃透Kotlin中 lateinit 和 by lazy 的区别和用法
  6. Kotlin 扩展方法(Extension Functions)使用详解
  7. Kotlin 中 == 和 === 的区别
  8. Kotlin 操作符与集合/数组方法详解------新手指南
  9. Kotlin 中 reified 配合 inline 不再被类型擦除蒙蔽双眼
  10. Kotlin Result 类型扩展详解 ------ 新手使用指南
相关推荐
繁依Fanyi1 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android3 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule
田一一一7 小时前
Android framework 中间件开发(三)
android·中间件·framework·jni
无声旅者9 小时前
深度解析 IDEA 集成 Continue 插件:提升开发效率的全流程指南
java·ide·ai·intellij-idea·ai编程·continue·openapi
androidwork11 小时前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin
田一一一11 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方12 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航13 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust
千里马-horse15 小时前
android vlc播放rtsp
android·media·rtsp·mediaplayer·vlc
難釋懷15 小时前
Android开发-文本输入
android·gitee