一 分区存储概述

Android 10之前,Android的文件存储现象就像个垃圾桶,但凡app取得了存储空间的读写权限WRITE_EXTERNAL_STORAGE,就可以肆意创建文件,难以管理。用户体验也特别差,打开文件管理器,会发现,想找个具体的文件根本无从下手。

1.1 分区存储原则

为了更好地管理自己的文件并减少混乱,加强隐私保护,Android Q开始引入了分区存储机制。外部存储空间被重新设计,按应用私有和公用共享划分。应用只能访问到自己私有空间,或者通过MediaStore APIStorage Access Framework去访问共享的资源目录。

分区存储主要遵循了三大原则对文件存储进行重新设计:

  1. 记录文件来源:系统会记录文件由哪个应用创建,应用不需要权限就可以对自己创建的文件进行读写;

    MediaStore数据库增加owner_package_name字段记录文件属于哪个应用, 应用卸载后owner_package_name字段会置空,也就是说,卸载重装后,之前创建的文件,已不属于应用创建的了,需要相关存储权限才能再次读写

  2. 应用数据的保护:对外部存储空间进行了访问限制,应用只能访问自身的私有空间或共享空间,即使获得了读写权限,也是无法访问其他应用的私有空间的;

  3. 用户数据保护:当用户下载了一些文件,比如带有敏感信息的邮件附件,这些文件应该对其他应用不可见。添加了pdf、office、doc等文件的访问限制,用户即使申请了存储权限也不能通过MediaStore访问其他应用创建的pdf、office、doc等文件,需要通过Storage Access Framework 框架,由用户参与选择,才能获得访问权限

Android 系统的版本越新,就越依赖于文件的用途而不是位置来确定应用对文件的访问能力

1.2 关于存储方式的兼容和判断

  1. 当targetSdk <= 28时,应用使用传统存储方式;
  2. 当targetSdk <= 29时,可以通过在应用清单的application标签中添加android:requestLegacyExternalStorage="true" ,从而关闭分区存储功能,继续使用传统访问方式。
  3. 当targetSdk>>=30时,Android会强制执行分区存储,无法关闭。
  4. 可以通过Environment.isExternalStorageLegacy()判断应用存储的运行方式,true表示以传统的兼容方式运行,false表示以分区存储运行

注意:当修改了requestLegacyExternalStorage属性的值,必须要卸载掉旧APK,重新安装才会生效

二 存储空间的影响

Android 提供了两类物理存储位置内部存储空间和外部存储空间。在大多数设备上,内部存储空间小于外部存储空间。不过,所有设备上的内部存储空间都是始终可用的,因此在存储应用所依赖的数据时更为可靠。

可移除卷(例如 SD 卡)在文件系统中属于外部存储空间。空间较大,现在的智能机基本都配有,但为了兼容性,也可在使用相关api时检查该空间是否处于可用状态。Environment.getExternalStorageState()

// 是否可读写
fun isExternalStorageWritable(): Boolean {return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}// 是否可读
fun isExternalStorageReadable(): Boolean {return Environment.getExternalStorageState() insetOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

在写入存储之前,需要知道设备有多少空间可用,不够的话会抛出异常。不过现在的智能设备内存也是比较大的,这部分可以参考Google 查询可用空间

2.1 内部存储空间

打开Android studio的 Device File Explorer时,可以看到应用的内部空间目录:/data/data/包名/

内部存储空间本身便是保护应用隐私设计的。这部分是没有变化的。应用不需要任何系统权限即可读取和写入这些目录中的文件。其他应用无法访问存储在内部存储空间中的文件。

内部存储空间为应用提供目录。一个目录专为应用的持久性文件而设计,而另一个目录包含应用的缓存文件。内部存储空间是应用专属的,是可以正常使用File相关api的,所以只要取得路径便可自由发挥:

  1. 持久性文件根目录File:context.filesDir(),/data/data/包名/files/

  2. 缓存性文件根目录File::context.cacheDir(),/data/data/包名/cache/

android也提供了一些简便的api创建删除文件:context.openFileOutput(filename, Context.MODE_PRIVATE)context.openFileInput(filename)context.fileList()context.getDir(dirName, Context.MODE_PRIVATE)context.delefteFile(fileName)

注意:卸载app后,系统会自动移除这些目录释放空间!!

2.2 外部存储空间

/storage/emulated/0/Android/data/包名

Android 10的分区存储特性,对Android系统的外部存储空间重新设计,外部存储被分为应用私有目录以及共享目录两个部分:

  1. 应用私有目录:存储应用私有数据,外部存储应用私有目录对应Android/data/包名
  2. 共享目录:存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录。

2.2.1 应用私有空间

与以往相同的是,访问自身的外部存储下的应用私有空间是不需要任何权限的。与内部一样,也有一个目录专为应用的持久性文件而设计,和另一个目录包含应用的缓存文件。也是可以正常使用File相关api的,所以只要取得路径便可自由发挥。

需要注意的不同点是:开启了分区存储特性后,应用只能访问自身的私有空间,即使获得了存储权限,也无法访问其他应用的私有空间

另外与内部空间的区别是,外部存储空间可能被移除也可能有多个,所以返回的是一个数组,对于返回数组中的第一个元素被视为主外部存储卷。除非该卷已满或不可用,否则请使用该卷。

  1. 持久性文件:getExternalFilesDirs(@NonNull Context context, @Nullable String type),type根据文件类型可传系统预定义的子目录常量,如图片Environment.DIRECTORY_PICTURES,此时返回/storage/emulated/0/Android/data/包名/files/Pictures。或者传null直接返回/storage/emulated/0/Android/data/包名/files
  2. 缓存性文件:ContextCompat.getExternalCacheDirs(context)/storage/emulated/0/Android/data/包名/cache

注意:卸载app后,系统会自动移除这些目录释放空间!!

三 共享存储空间的影响

如果用户数据可供或应可供其他应用访问,并且即使在用户卸载应用后也可对其进行保存,请使用共享存储空间。

共享文件类型, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录。Android 分别提供用于获得该类型可共享数据文件Uri的 API:

  • 媒体内容:可以使用 MediaStore API 访问此内容
  • 文档和其他文件:系统有一个特殊目录,用于包含其他文件类型,例如 PDF 文档和采用 EPUB 格式的图书。应用可以使用Storage Access Framework访问这些文件。

对于共享文件,。以往可以通过data column获得路径,再使用File API来操作,现在都会返回失败。开启了分区存储特性之后,应用只能通过系统提供的api来向系统请求得到对应文件的Uri,并通过Uri生成FileDescriptorInputStream等方式进行文件读写:(简而言之,对于共享文件的增删查改,主要问题在于Uri的获取)

注:android 11 又允许通过路径来访问,系统会自动重定向为Uri。

val resolver = applicationContext.contentResolver//读resolver.openFileDescriptor(content-uri, "r")?.use { pfd ->val inputStream =  FileInputStream(pfd.fileDescriptor)}resolver.openInputStream(content-uri).use { stream ->}//写resolver.openFileDescriptor(content-uri, "w")?.use { pfd ->val outputStream =  FileOutputStream(pfd.fileDescriptor)}resolver.openOutputStream(content-uri).use { stream ->}//图片bitmapBitmapFactory.decodeFileDescriptor(pfd.fileDescriptor)

3.1 MediaStore API

MediaStrore API的增删查改,可参看Google官方指南,主要是通过contentResolver获得对应的uri,这里就不引入了。图片来源

3.1.1 MediaStore 概述

Android系统会自动扫描外部存储空间,将媒体文件按类型添加到系统预定义的Images、Videos、Audio files、Downloaded files集合中。Android Q通过MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问相对应共享目录文件资源。预定义集合所对应的目录如下表所示:

媒体类型 Uri 默认创建目录 允许创建目录
Image content://media/external/images/media Pictures DCIM,Pictures
Audio content://media/external/audio/media Music Alarms,Music,Notifications,Podcasts,Ringtones
Video content://media/external/video/media Movies DCIM,Movies
Download content://media/external/downloads Download Download

注意:MediaStore.Downloads.EXTERNAL_CONTENT_URI是Android10版本新增API,用于创建、访问非媒体文件

3.1.1 MediaStore 的变化

  1. MediaStore API在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限;
  2. MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常;
  3. MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), Android 10 里唯一一种访问其他应用创建的非媒体文件的途径是使用存储访问框架 (Storage Access Framework) 提供的文档选择器。

3.1.2 通过api创建的文件存放到哪里?如何自定义位置?

当通过MediaStore API创建文件时,文件会默认保存到对应的类型目录,比如图片存到Pictures/目录下,可以往上查看表格的默认目录及允许目录;

可以使用MediaStore.xxx.Media.RELATIVE_PATH自己指定要存放的目录或者子目录,如:contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/自定义子目录"),文件就会放在Pictures/自定义子目录/ 中;或者使用contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment. DIRECTORY_DCIM),将文件放到DCIM/

注意:每一种类型都有对应的可允许创建的目录,否则会返回失败。具体可创建目录可以往上查看表格

3.2 Storage Access Framework

SAF框架支持用户与系统选择器互动,从而选择文档提供器以及供您的应用创建、打开或修改的特定文档和其他文件。由于用户参与了文件的选择,因此该机制无需任何系统权限。

应用通过调用 ACTION_CREATE_DOCUMENT , ACTION_OPEN_DOCUMENT , 和ACTION_OPEN_DOCUMENT_TREE Intent获取Document provider提供的文件,并在onActivityResult接口接收返回的被选择文件的Uri。另外,在配置 intent 时,应指定文件的名称和 MIME 类型,并且还可以根据需要使用 EXTRA_INITIAL_URI intent extra 指定文件选择器在首次加载时应显示的文件或目录的 URI。

这部分也是没变化的,可参考官方指南:从共享存储空间访问文档和其他文件

3.2.1 获取持久权限

对于通过SAF框架获得的uri权限,可以通过申请持久权限,不用每次重启手机都要重新请求。

contentResolver.takePersistableUriPermission(documentUri,Intent.FLAG_GRANT_READ_URI_PERMISSION
)

四 存储特性Android版本差异概览

4.1 其他变化:图片位置信息

一些照片在元数据中包含位置信息,以便用户查看照片的拍摄地点。由于此位置信息属于敏感信息,如果应用使用了分区存储,默认情况下 Android 10 会对应用隐藏此信息。

如果应用需要访问照片的位置信息:

  1. 在应用清单中请求ACCESS_MEDIA_LOCATION权限
  2. 通过调用 setRequireOriginal(),从 MediaStore 对象获取照片的确切字节,并传入照片的 URI

五 更新

5.1 Android 10 的分区存储"bug"

Android 10 通过媒体MediaStore API 删除(delete)一个媒体文件,只是简单移除了MediaStore数据库的索引,并不会真正删除物理存储上的实体文件,而且只要手机重启,则索引又被加上去了。issue

这需求也比较少见,只是刚好测试发现了。网上查了下,的确存在这个问题,Android 11 就可以正常删除了。 要是有什么解决方案,热烈欢迎指出!!

5.2 Android 11 的存储变更

5.2.1 允许继续使用原始文件路径

可以再次使用文件路径,系统自动重定向为Uri

5.2.2 增加批量操作

在 Android 10 中,应用在对MediaStore的每一个文件请求编辑或删除时都必须一个个地得到用户的确认。而在 Android 11 中,应用可以一次请求修改或者删除多个媒体文件。

主要通过以下新增的批量操作api

方法 说明
MediaStore.createDeleteRequest (resolver, uris) 批量删除(不放入回收站)
MediaStore.createFavoriteRequest(resolver, uris) 批量收藏
MediaStore.createTrashRequest (resolver, uris) 批量移入回收站
MediaStore.createWriteRequest(resolver, uris) 批量获得写入权限
val uris = ...
val pi = MediaStore.createWriteRequest(contentResolver,uris)
startIntentSenderForResult(pi.intentSender, REQUEST_CODE, null, 0, 0, 0)//相应
override fun onActivityResult(xxx) {when (requestCode) {REQUEST_CODE ->if (resultCode == Activity.RESULT_OK) {//获得权限,继续操作} else {// 用户拒绝了权限授予}}
}

推荐学习视频

Android R分区存储大揭秘

大家如果还想了解Android 相关的更多知识点可以点进我的【GitHub】项目中,里面记录了许多的Android 知识点。


Android 粉丝技术交流学习群

Android 存储进化:分区存储相关推荐

  1. 【Android 文件管理】分区存储 ( 修改与删除图片文件 )

    文章目录 一.分区存储模式下使用 MediaStore 修改图片 二.分区存储模式下使用 MediaStore 删除图片 三.相关文档资料 Android 分区存储系列博客 : [Android 文件 ...

  2. 【Android 文件管理】分区存储 ( 创建与查询图片文件 )

    文章目录 一.分区存储模式下使用 MediaStore 插入图片 二.分区存储模式下使用 MediaStore 查询图片 三.相关文档资料 Android 分区存储系列博客 : [Android 文件 ...

  3. 【Android 文件管理】分区存储 ( MediaStore 文件操作 )

    文章目录 一.动态权限申请 二.MediaStore 操作文件 三.完整代码示例 1.MainActivity 核心代码 2.build.gradle 构建脚本 3.清单文件 五.相关文档资料 特别注 ...

  4. Android存储之分区存储适配

    一.Android存储分区介绍 1.简介 Android 存储分为内部存储(Internal storage)和外部存储(External storage).有许多用户认为外部存储意味着SD存储卡或外 ...

  5. 【Android 文件管理】分区存储 ( 分区存储机制 和 文件索引数据 )

    文章目录 一.Android 10 以前的存储机制 二.Android 10 及以后的分区存储机制 三.Android 10 中的存储过渡方案 四.文件索引数据库 五.相关文档资料 一.Android ...

  6. Android 11 强制分区存储

    TargetSdk 30 的存储记录 前言 一.分区存储是什么? 二.应用分区存储的文件访问规定 1.应用专属目录 2.其他应用的专属目录 3.媒体资源文件 4.文件和目录访问限制 4.1.访问目录 ...

  7. Android分区存储

    1.分区存储概述 分区存储是Android 10开始引进的Android系统存储管理机制,它允许App读取和写入App自身创建的文件而不需要任何存储权限.其中根据存储位置的不同,可以分为内部内部存储和 ...

  8. Android 10 分区存储完全解析

    Android 10分区存储完全解析 一.Android 存储目录 1.1 内部存储 1.2 外部存储 1.2.1 外部私有存储 1.2.2 外部公共存储 1.3 使用Api 获取存储目录 二.And ...

  9. 结合Android去水印APP谈谈分区存储

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 前言 方便个人更新微信状态,上周花半天时间编写简单的抖音去水印AP ...

最新文章

  1. KerGNNs:结合Graph kernels的可解释GNN
  2. springcloud(七):配置中心svn示例和refresh
  3. 租不起房!你离逃离北上广还有多长时间?
  4. mysql安装ssl证书_Mysql配置ssl证书
  5. 平舌音 Z C S 的正确发音方式
  6. Scrum Master
  7. win2008服务器维护费用,win2008 服务器安全检查步骤指引(日常维护说明)
  8. 3dmax导出fbx ue4_耗子尾汁马保国—CC3快速相片建模到UE4实时面部,动作捕捉
  9. MySQL只读实例简介
  10. 这可能是今年最值得推荐的数据分析工具!
  11. WebRTC报错:depot_tools/bootstrap_python3: um.8_bin/python3/bin/python3: 没有那个文件或目录(三)
  12. Node.js meitulu图片批量下载爬虫1.02版
  13. Android 获取Device Id
  14. IE11下载文件时,文件扩展名自动由点改为下划线
  15. 常用的的身份证校验方法
  16. android应用获取手机存储空间方法
  17. LM2596的肖特基二极管选择的注意点
  18. 搭建hadoop3.x报错 Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).
  19. 【无标题】7-11 sdut-C语言实验- 数列有序! 7-12 sdut-C语言实验- 中位数 7-13 sdut-C语言实验-各位数字之和排序 7-14 sdut-C语言实验- 冒泡排序中数据交换
  20. 基于turtlebot3 仿真建图rosbrige接口web 2dmap可视化实现

热门文章

  1. P2444 [POI2000]病毒(ACAM上找环)
  2. java时间差工具_Java 工具类 计算两个日期之间相差 天 小时 分钟 秒
  3. 不玩VR游戏、不看VR视频,你的春节可能就白过了!
  4. 红旅在线语料库网站 开发笔记
  5. Spring(1) Spring的7种事务传播行为
  6. Easyconnect 状态异常及连接上校园网无法访问校内网站的问题
  7. 载荷是什么意思?底层原理是什么?
  8. PHP面向对象的mysql数据库函数封装
  9. 输入四个数字,并输出它们的和。(c语言)
  10. 安图恩频道一直连接服务器,DNF里最让大家不爽的事,安图恩频道什么时候能换服务器...