15、权限与存储:Scoped Storage、MANAGE_EXTERNAL_STORAGE、MediaStore权限

存储权限这块,可以说是Android权限演进中最折腾人的部分了。我从Android 6.0做到现在,每次大版本更新,存储这块几乎都要改一遍。尤其是Android 10推出的Scoped Storage,当时我们团队好几个项目都因为这个改了架构。

说白了,Google就是想管住那些乱读乱写文件的App。你想想看,一个手电筒App凭什么要读你SD卡里的所有照片?这确实不合理。但对我们开发者来说,这改动确实不小。

15.1 Scoped Storage 到底是什么

Scoped Storage,中文叫「分区存储」。它的核心思想很简单:每个App只能自由访问自己的专属目录,以及公共媒体目录中的文件

具体来说,App能直接读写的地方就两个:

  • 内部存储的私有目录Context.getFilesDir()Context.getCacheDir()
  • 外部存储的私有目录Context.getExternalFilesDir()

至于其他目录,比如Download、Documents,或者别的App的私有目录,你就别想了。除非用户通过系统文件选择器主动授权。

核心变化:Android 10及以上,READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE这两个权限,只能让你访问公共媒体目录(Music、Movies、Pictures等)。想访问其他目录?得用MANAGE_EXTERNAL_STORAGE

我记得刚适配Android 10时,有个功能需要读取Download目录下的文件。按老思路直接拿路径去读,结果一直报权限错误。查了半天才发现,这条路已经被堵死了。

15.2 MediaStore 的正确用法

既然不能直接拿路径操作文件了,那怎么办?Google给出的方案就是MediaStore。它相当于一个内容提供者,帮你管理公共媒体文件。

举个例子,你想读取所有图片:

// 查询所有图片
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.DATA
)

contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
        val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
        // 注意:DATA字段在Android 10+可能为空
        val data = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
    }
}

这里有个坑,我必须要提醒你。DATA字段(也就是文件路径)在Android 10以上可能返回null。你不能依赖它来访问文件。正确的做法是用Uri来操作:

// 通过Uri读取文件内容
val uri = ContentUris.withAppendedId(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    id
)
val inputStream = contentResolver.openInputStream(uri)
// 然后读取流数据...

写入文件也是类似的套路:

// 插入一条新记录到MediaStore
val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_photo.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}

val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri?.let {
    contentResolver.openOutputStream(it)?.use { outputStream ->
        // 写入图片数据
    }
}

个人建议:尽量用RELATIVE_PATH字段来指定存储位置,而不是拼绝对路径。这样兼容性更好,也符合Scoped Storage的设计思路。

15.3 MANAGE_EXTERNAL_STORAGE 权限

有些App确实需要访问所有文件,比如文件管理器、杀毒软件、备份工具。Google给这类App留了一个后门——MANAGE_EXTERNAL_STORAGE权限。

这个权限在Android 11引入,申请后可以获得「所有文件访问权限」。但门槛很高:

  • 你的App必须属于特定的核心功能类别
  • Google Play审核时会严格审查
  • 用户需要手动去设置里开启

申请方式如下:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

然后在代码中检查并引导用户:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    if (!Environment.isExternalStorageManager()) {
        // 引导用户去设置页面开启
        val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
            data = Uri.parse("package:${context.packageName}")
        }
        startActivity(intent)
    }
}

警告:千万别为了省事随便申请这个权限。Google Play对滥用此权限的App处罚很重。我曾经见过一个App因为非必要场景申请了MANAGE_EXTERNAL_STORAGE,直接被下架了。

15.4 不同Android版本的兼容策略

现实情况是,你的App可能要同时支持Android 9、10、11、12、13……每个版本的存储策略都不一样。怎么办?

我的做法是分层处理:

Android版本 存储策略 推荐做法
Android 9及以下 传统存储 直接使用文件路径,申请READ/WRITE_EXTERNAL_STORAGE
Android 10 Scoped Storage(可关闭) 在manifest中设置requestLegacyExternalStorage="true"可临时关闭
Android 11及以上 强制Scoped Storage 必须使用MediaStore或SAF

对于Android 10,Google留了一个过渡方案:

<application
    android:requestLegacyExternalStorage="true"
    ...>

加上这个属性后,你的App在Android 10上可以继续使用传统文件路径。但注意,这只是临时方案。Android 11开始这个属性就失效了。

我建议你尽早迁移到MediaStore方案。别拖到用户都升级到Android 11以上了才改,那时候就手忙脚乱了。

15.5 知识体系总览

下面这张图总结了本章的核心逻辑:

Android 存储权限体系 存储权限 Scoped Storage MANAGE_EXTERNAL_STORAGE MediaStore 查询/插入/更新媒体文件 所有文件访问权限 需Google Play审核 Android 10+ 强制 Scoped Storage Android 11+ 引入 MANAGE_EXTERNAL_STORAGE

15.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要用File API直接操作公共目录:Android 11以上会直接抛异常。老老实实用MediaStore。
  • 注意Uri的持久化:用户通过文件选择器选中的Uri,记得用takePersistableUriPermission()保存权限,否则App重启后就失效了。
  • 测试时多换几台设备:不同厂商对Scoped Storage的实现有差异。我遇到过某品牌手机在Android 10上仍然允许直接读写公共目录,但另一台就不行。
  • 不要假设用户会给你MANAGE_EXTERNAL_STORAGE权限:很多用户看到这个权限就直接拒绝了。你的App要做好降级方案。

我的经验:如果你只是读写图片、视频、音频这类媒体文件,用MediaStore就够了。别去碰MANAGE_EXTERNAL_STORAGE。这个权限就像一把大锤,不是所有钉子都需要它来敲。

嗯,存储权限这块内容确实不少。但只要你理解了Scoped Storage的设计初衷——保护用户隐私,很多设计就说得通了。适配的时候别急,一步步来,先从MediaStore入手,把核心功能跑通,再考虑特殊场景。

公众号:蓝海资料掘金营,微信deep3321