Google 为了更好的保护用户数据并限制设备冗余文件增加,在 Android 10 版本变更了设备外部存储访问方式,外部存储新特性称为分区存储(Scoped Storage)。最近项目需要升级到Android10 (SDK 29),但是由于一些概念和升级后的方法还不太清楚,所以感觉有必要整体梳理一遍。
一、Android 存储目录
要理解 Android 10 的分区存储(Scoped storage),我们必须要将 Android 存储梳理清楚。Android 存储分为内部存储(Internal storage)和外部存储(External storage)。那么这个内外是怎么区分的呢?
Android 长久以来都是支持外置存储空间这个功能的,也就是我们常说的SD存储卡。现在部分双卡手机也是支持扩展micro SD存储卡,也有一部分手机已经取消支持扩展功能了。现在的手机一般都会自带一个机身存储,这个机身存储和SD存储卡的功能是完全一样的,都是通过 Linux 文件挂载的方式将这些存储空间挂载系统上,机身自带的存储会比SD存储卡更快更安全。所以我们谈论的 Android 的内部和外部存储都是是否通过挂载的方式将额外的存储空间挂载到系统中。一般机身自带的存储挂载点都在 /mnt/user/0/primary
,一般SD卡挂载点 /storage/sdcard1,根据设备的不同,则可能不同。
如果我们使用 Android studio 使用 Device File Explorer 打开正在链接的手机(Xiaomi Redmi K30)文件系统,可能会让你迷惑,我们在根目录里可以看到 /sdcard、/mnt/sdcard、/storage/self/primary 里面的内容都是一样的:
仔细看图标右上角有个三角符合,鼠标放到上面可以显示一个文件路径,带这些符号的都是一个软连接,最后这个软连接都指向 /mnt/user/0/primary
,也就是机身自带存储的挂载点。不同的手机部分目录有可能不同。
所以,我们所说的 Android 内外存储本质就是数据放置在 /data/data/
和 /mnt/user/0/primary
的区别:
因此,下面的内部存储和外部存储就可以很好的理解了。
1.1 内部存储
内部存储就是指的是 App 私有的目录,如: /data/data/packagename/
有些手机的目录是 /data/user/0/com.application.id/
实际上是同一个目录,/data/user/0 目录是一个软连接,其实际指向的目录即 /data/data
内部存储的目录会随着 APP 卸载而被删除
1.2 外部存储
外部存储包括外部私有存储和外部公共存储,这些数据都是存储在挂载上的空间上的。
1.2.1 外部私有存储
外部私有存储是指 /storage/emulated/0/Android/data/packagename
在外部私有存储中,APP 可以读取自己目录下的文件,如果 Api 大于 19 ,不需要申请写权限。如果需要读取其他 APP 的外部私有存储目录,则需要声明读写权限,若高于23,还需要动态进行权限申请。
外部私有存储的目录会随着 APP 卸载而被删除
1.2.2 外部公共存储
外部存储是指 sdcard 中根目录中的公共目录,即 /storage/emulated/0
,例如,图片文件夹:/storage/emulated/0/DCIM 和下载文件夹:/storage/emulated/0/Download
这部分的目录是共享的,所以如果 APP 在这个目录下读写文件,需要申请读写权限,并且在 App 卸载后不会被删除。
1.3 使用Api 获取存储目录
只有使用外部公共存储的时候需要读写权限
写入权限 android.Manifest.permission#WRITE_EXTERNAL_STORAGE
读取权限 android.Manifest.permission#READ_EXTERNAL_STORAGE
二、Android 10 分区存储机制
2.1 原则
分区存储遵循以下三个原则对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件。
- 文件更好的归属:系统记录文件由哪个应用创建,应用不需要存储权限即可以访问应用自己创建文件
- 应用数据保护:添加外部存储应用私有目录文件访问限制,应用即使申请了存储权限也不能访问其他应用外部存储私有目录文件
- 用户数据保护:添加pdf、office、doc等非媒体、图片和音频文件的访问限制,用户即使申请了存储权限也不能访问其他应用创建的pdf、office、doc等文件
2.2 分区存储概览
分区存储就是对外部存储进行重新设计,简单来说,对外部共享文件的访问需要通过 MediaStrore API 和 Storage Access Framework 来访问;对外部私有文件来说在无法读写自己应用以外创建的其他文件。
2.2.1 外部共享文件的访问
在第一小节中,我们已经知道外部共享目录就是sdcard 中根目录中的公共目录,即 /storage/emulated/0
目录下的文件,例如 DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等。
在访问共享目录下文件的时候,在 Android 10 以前,在通过存储权限申请后,可以直接通过 file path 获取资源。在Android 10 版本以及以后的版本中,共享目录文件需要通过MediaStore API或者 Storage Access Framework 方式访问:
- MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限
- MediaStore API 访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常
- MediaStore API 不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过 Storage Access Framework 方式访
2.2.2 外部私有文件的访问
外部私有文件的目录对应 :/storage/emulated/0/Android/data/packagename ,
- 在 Android 10 以前,在申请存储权限后,可以对整个sdcard 进行读取文件,这也当然包括其他应用创建的外部私有文件。
- 在 Android 10 版本以及以后,在分区存储中应用只能访问应用自己创建文件,不需要申请存储权限。
2.2.3 内部存储文件的访问
内部私有存储文件的目录对应: /data/data/packagename/
文件访问方式与之前Android版本一致,可以通过file path获取资源
2.2.4 其他受影响的变更
- 图片位置信息 一些图片会包含位置信息,因为位置对于用户属于敏感信息, Android 10 应用在分区存储模式下图片位置信息默认获取不到,应用通过以下两项设置可以获取图片位置信息,在 manifest 中申请 ACCESS_MEDIA_LOCATION 调用 MediaStore setRequireOriginal(Uri uri) 接口更新图片 Uri
- MediaStore.Files 应用分区存储模式下,MediaStore.Files 集合只能够获取媒体文件信息(图片、音频、视频),获取不到非媒体(pdf、office、doc、txt等)文件
- File Path 路径访问受影响接口开启分区存储新特性, Andrioid 10不能够通过File Path路径直接访问共享目录下资源,以下接口通过File路径操作文件资源,功能会受到影响,应用需要使用MediaStore或者SAF方式访问
类名称 | 受影响接口名称 |
File | crateNewFile() |
delete() | |
renameTo() | |
mkdir() | |
mkdirs() | |
FileInputStream | FileInputStream(File file) |
FileInputStream(String name) | |
FileIOutputStream | FileOutputStream(File file) |
FileOutputStream(File file, boolean append) | |
FileOutputStream(String name) | |
FileOutputStream(String name, boolean append) | |
BitmapFactory | decodeFile(File file) |
decodeFile(String name, Options opts) |
2.2.5 存储特性Android版本差异
三、分区存储适配方案
3.1 兼容模式
在应用没有完成分区适配工作时,可以临时使用兼容方案,兼容模式下应用申请存储权限,即可以拥有外部存储完整目录的访问权限,通过 Android 10 之前文件访问方式运行,可以使用以下两种方法设置兼容模式:
- Target 小于等于 Android 9(API 28)
- Target 大于等于 Android 10 (API 29),在 manifest 中设置 requestLegacyExternalStorage 属性为 true
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
使用 Environment.isExternalStorageLegacy() 判断兼容模式接口,true 表示应用以兼容模式运行,false 表示应用以分区存储特性运行。
注意:
应用已完成分区存储适配工作且已经打开分区存储开关,如果当前应用以兼容模式运行,覆盖安装后应用仍然会以兼容模式运行,卸载重新安装才会以分区存储模式运行。
3.2 分区存储适配方案
3.2.1 文件迁移
文件迁移是将应用共享目录文件迁移到应用私有目录或者 Android 10 要求的媒体集合目录。
- 针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过 File path 方式访问文件资源,降低适配成本;
- 允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到 Android 10 要求的媒体集合目录。
3.2.2 文件访问兼容行适配
共享目录文件不能通过 File path 方式读取,需要使用 MediaStore API 或者 Storage Access Framework 框架进行访问。
3.2.2.1 MediaStore API 解析
MediaStore 是 Android 系统提供的一个多媒体数据库,专门用于存放多媒体信息的,通过 ContentResolver 即可对数据库进行操作。
Android 10版本 MediaStore API只允许在共享目录指定目录创建文件, 非指定目录创建文件会抛出IllegalArgumentException, 创建文件目录汇总如下:
媒体类型 | Uri | 默认创建目录 | 允许创建目录 |
---|---|---|---|
Image | content://meida/external/images/meida | Pictures | DCIM、Pictures |
Audio | content://meida/external/audio/meida | Music | Alarms、Music、Notifications、Podcasts、Ringtones |
Video | content://meida/external/video/media | Movies | DCIM 、Movies |
Download | content://media/external/downloads | Download | Download |
在不同存储权限 MediaStore API 可访问文件区域:
- 无存储权限,能过在共享目录指定目录创建文件,可以读取应用自己创建的文件
- 申请读权限,能够读取共享目录其他应用创建的媒体类型文件
- 申请写权限,能够修改或删除共享目录其他应用创建的媒体类型文件
3.2.1.2 Storage Access Framework 解析
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。借助 SAF,用户可轻松浏览和打开各种文档、图片及其他文件,而不用管这些文件来自其首选文档存储提供程序中的哪一个。用户可通过易用的标准界面,跨所有应用和提供程序以统一的方式浏览文件并访问最近用过的文件。
SAF 包含以下元素:
- 文档提供程序 : 一种内容提供程序,可让存储服务(如 Google 云端硬盘)提供其管理的文件。文档提供程序以 DocumentsProvider 类的子类形式实现。文档提供程序的架构基于传统的文件层次结构,但其实际的数据存储方式由您决定。Android 平台包含若干内置的文档提供程序,如 Downloads、Images 和 Videos。
- 客户端应用 :一种定制化的应用,它会调用 ACTION_CREATE_DOCUMENT、ACTION_OPEN_DOCUMENT 和 ACTION_OPEN_DOCUMENT_TREE intent 操作并接收文档提供程序返回的文件。
- 选择器 :一种系统界面,可让用户访问所有文档提供程序内满足客户端应用搜索条件的文档。
以下为 SAF 提供的部分功能:
- 让用户浏览所有文档提供程序的内容,而不仅仅是单个应用的内容。
- 让您的应用获得对文档提供程序所拥有文档的长期、持续访问权限。用户可通过此访问权限添加、修改、保存和删除提供程序中的文件。
- 支持多个用户帐号和临时根目录,如只有在插入 U 盘后才会出现的“USB 存储提供程序”。