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 类型扩展详解 ------ 新手使用指南
相关推荐
Lary_Rock18 分钟前
Android 编译问题 prebuilts/clang/host/linux-x86
android·linux·运维
王江奎1 小时前
Android FFmpeg 交叉编译全指南:NDK编译 + CMake 集成
android·ffmpeg
limingade1 小时前
手机打电话通话时如何向对方播放录制的IVR引导词声音
android·智能手机·蓝牙电话·手机提取通话声音
小厂永远得不到的男人2 小时前
基于 Trae 的 WebSocket 聊天室保姆级教程(超详细版)
websocket·全栈·trae
hepherd2 小时前
Flutter 环境搭建 (Android)
android·flutter·visual studio code
_一条咸鱼_3 小时前
揭秘 Android ListView:从源码深度剖析其使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
深入剖析 Android NestedScrollView 使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android ScrollView:深入剖析其使用原理与源码奥秘
android·面试·android jetpack
_一条咸鱼_3 小时前
深入剖析 Android View:从源码探寻使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android View 绘制原理:从源码剖析到极致理解
android·面试·android jetpack