分区存储
为了让用户更好地控制自己的文件并减少混乱,Android 10 针对应用推出了一种新的存储范例,称为分区存储。分区存储改变了应用在设备的外部存储设备中存储和访问文件的方式。以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件。
分区存储的目标是保护应用和用户数据的隐私。这包括保护用户信息(例如照片元数据)、防止应用在未经明确许可的情况下修改或删除用户文件,以及保护下载到“下载”或其他文件夹的敏感用户文档。
使用分区存储的应用可具有以下访问权限级别(实际访问权限因实现而异)。
对自己的文件拥有读取和写入访问权限(没有权限限制)
对其他应用的媒体文件拥有读取访问权限(需要具备 READ_EXTERNAL_STORAGE 权限)
只有在用户直接同意的情况下,才允许对其他应用的媒体文件拥有写入访问权限(系统图库以及符合“所有文件访问权限”获取条件的应用除外)
对其他应用的外部应用数据目录没有读取或写入访问权限
换一种说法:
每个应用向自己的私有目录读写文件,不需要读写权限。私有文件目录具体路径: storage/emulated/0/android/data/packageName/ ,获取方法: Context#getExternalFilesDir()
应用即使获取了读写权限,也无法访问其他应用的私有目录。
当应用需要获取媒体文件时,通过 MediaStore API 向公共存储目录DCIM、Music或者Movie获取。同样写媒体文件也是如此。并且读写自己的文件时不需要申请权限。 只有读其他应用的媒体文件时才会需要申请READ_EXTERNAL_STORAGE权限。
(更新:Android11为目标平台时,可以使用文件直接路径去访问媒体,这是在Android10上没有的,应用的性能会略有下降,还是推荐使用MediaStore )当应用需要获取其他非媒体文件时,比如doc、pdf文件,需要使用 系统的文件选择器SAF 来进行访问。
所以WRITE_EXTERNAL_STORAGE权限,在未来的Android11版本里,会被废弃。 (写文件不需要权限,只能在私有目录和公共目录写文件)
下面是关于分区存储权限和其他相关项目的表格。
类型 | 位置 | 访问应用自己生成的文件 | 访问其他应用生成的的文件 | 访问方法 | 卸载应用是否删除文件 |
---|---|---|---|---|---|
外部存储 | 应用特定的目录 | 无需权限 | 无法直接访问 | getExternalFilesDir()获取到属于应用自己的文件路径 | 是 |
外部存储 | Downloads | 无需权限 | 无需权限 | 通过存储访问框架SAF,加载系统文件选择器 | 否 |
外部存储 | Photo/ Video/ Audio/ | 无需权限 | 需要权限READ_EXTERNAL_STORAGE | MediaStore Api | 否 |
将分区存储与 FUSE 搭配使用
Android 11 或更高版本支持用户空间中的文件系统 (FUSE),这使 MediaProvider 模块可以检查用户空间中的文件操作并根据允许、拒绝或隐去访问权限的政策限制对文件的访问。分区存储中使用 FUSE 的应用可获得分区存储的隐私功能以及通过直接文件路径访问文件的功能(让 File API 继续在应用中运行)。
Android 10 对 MediaProvider 执行的文件访问强制实施了分区存储规则,但对直接文件路径访问(例如,使用 File API 和 NDK API)则不实施此类规则,因为拦截内核调用所需的工作量较大。因此,分区存储中的应用无法使用直接文件路径访问文件。此限制影响了应用开发者的适应能力,因为需要大量的代码更改才能将 File API 访问重写为 MediaProvider API 访问。
安卓11以前的外部存储权限控制做的比较粗糙。应用申请了WREAD_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE就可以对外部存储进行读写。
在 Android 10 及更低版本中,SDCardFS 是文件系统,而 MediaProvider 为文件(如图片、视频和音乐文件等)集合提供了一个接口。应用使用 File API 创建一个文件时,它可以要求 MediaProvider 扫描该文件并将其记录在数据库中。
在 Android 11 或更高版本中,SDCardFS 已废弃,MediaProvider 成为了外部存储空间的文件系统处理程序(适用于 FUSE),可使外部存储空间上的文件系统和 MediaProvider 数据库保持一致。作为 FUSE 文件系统的用户空间处理程序,MediaProvider 可以拦截内核调用并确保文件操作的隐私安全。
在 Android 11 及更高版本中,MediaProvider 也是一个可在 Android 版本之外更新的模块化系统组件(一个 Mainline 模块)。这意味着,MediaProvider 中存在的性能、隐私或安全问题可以通过 Google Play 商店或合作伙伴提供的其他机制以无线下载方式提交和修复。FUSE 处理程序牵涉的各类资源均可更新,这使得更新可以修复 FUSE 性能降低问题和 bug。
1、为了实现动态的外置存储权限,Android会挂载三个目录到/dev/fuse来对应同一个外置存储,三个目录分别是:
/mnt/runtime/default/${label},
/mnt/runtime/read/${label},
/mnt/runtime/write/${label},这三个目录代表不同的权限,当一个应用进程取得了读外置存储的权限,那么它将使用 /mnt/runtime/read/${label} 目录来操作外置存储。
当一个应用程序取得了写外置存储的权限,那么它将使用/mnt/runtime/write/${label}目录来操作外置存储。
当一个应用程序没有获取操作外置存储的权限,将使用/mnt/runtime/default/${label}目录来操作主存储。
三个fuse目录最终都会作用于外置存储的media目录,只不过对目录下的可进行的操作权限是不同的。
Android 的FUSE file-system daemon会根据应用程序进程使用的fuse目录来决定是否可以读写外置存储的media目录下的数据。不过在Android 11里面这个路径方面有变化,变成了/mnt/user/0/emulated, /mnt/installer/0/emulated, /mnt/androidwritable/0/emulated, /storage/emulated。
2、${userid}/Android/obb, ${userid}/Android/data, ${userid}/Android/media 下的${package} 权限管理则根据/data/system/packages.list文件中的内容来完成。 如果一个应用程序操作fuse目录,FUSE file-system daemon处理文件请求的时候可以获取操作文件的进程的uid,并根据/data/system/packages.list下的内容找到uid对应的包名,如果进程操作的报名和uid相对应,则允许操作,否则拒绝操作。
3、不同userid对应的相同应用的uid不同,根据 2中规则,即可实现 相同应用程序(相同包名)不同用户( ${userid} )的目录的权限隔离。
文件访问的三种方式
1. MediaStore API,2. 存储访问框架(SAF),3. File API(android 11 共享文件夹,私有缓存文件夹)。
Android系统针对文件类型进行了分类,图片、音频、视频这三类文件将可以通过MediaStore API来进行访问,而其他类型的文件则需要使用系统的文件选择器来进行访问。
存储访问框架(SAF)
既可以选取多媒体文件,也可以选取其他类型文件。当你需要访问非多媒体文件时,或者访问根目录下的非共享文件夹时(可能是连接电脑后,在电脑上创建的),那么只能使用SAF。
Android外部存储空间的实现比较复杂,主要是借助了Linux文件系统的机制,运用到的关机技术有sdscard_fs、mnt namespace、bind mount。sdcard_fs接管了底层文件系统的权限控制,通过bind mount避免了为每个进程mount带来的内核文件系统节点开销,mnt namespace保证每个应用进程有自己的挂载点,并在运行时通过在vold中为进程切换新的mount namespace来实现了动态授予权限的母的。
参考文献
关键文件路径
packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java. 918
packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java 800
base/core/java/android/os/Environment.java