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 类型扩展详解 ------ 新手使用指南
相关推荐
程序员鱼皮11 小时前
离大谱,我竟然在 VS Code 里做了个视频!
github·aigc·ai编程
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker11 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
Kayshen12 小时前
我用纯前端逆向了 Figma 的二进制文件格式,实现了 .fig 文件的完整解析和导入
前端·agent·ai编程
wangruofeng12 小时前
OpenClaw 飞书机器人不回复消息?3 小时踩坑总结
ai编程
xq952712 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
恋猫de小郭14 小时前
AI 正在造就你的「认知卸载」,但是时代如此
前端·人工智能·ai编程
草梅友仁15 小时前
墨梅博客 1.7.0 发布与 AI 开发实践 | 2026 年第 9 周草梅周报
开源·github·ai编程