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 类型扩展详解 ------ 新手使用指南
相关推荐
编程之路从0到13 分钟前
React Native新架构之Android端初始化源码分析
android·react native·源码阅读
行稳方能走远5 分钟前
Android java 学习笔记2
android·java
Linux内核拾遗13 分钟前
人人都在聊 MCP,它到底解决了什么?
aigc·ai编程·mcp
A5IDC19 分钟前
如何有效处理不平衡数据集对AI模型的影响?通过重采样与损失函数调整解决数据偏差
ai编程
编程之路从0到127 分钟前
React Native 之Android端 Bolts库
android·前端·react native
爬山算法29 分钟前
Hibernate(38)如何在Hibernate中配置乐观锁?
android·java·hibernate
恋猫de小郭37 分钟前
Google DeepMind :RAG 已死,无限上下文是伪命题?RLM 如何用“代码思维”终结 AI 的记忆焦虑
前端·flutter·ai编程
行稳方能走远1 小时前
Android java 学习笔记 1
android·java
zhimingwen1 小时前
【開發筆記】修復 macOS 上 JADX 啟動崩潰並實現快速啟動
android·macos·反編譯