Android Q Labs| Android Q 分区存储

3,921 阅读18分钟

Shared Storage/外部存储区

首先我们先来明确一下什么是外部存储区?

外部存储区的英文名字叫 shared storage,也能更直观的反映出外部存储区的概念,它是一块共享的区域,应用写到这个区域的内容,取得 shared storage 权限的其他应用都可以直接访问。

我们看到的这些方法,他们所获取内容的位置通常都是在这块外部存储区。除了前三个分别用于获取文件、缓存文件、媒体文件目录的三个方法以外,通常游戏开发者还需去访问**obb **文件目录,可以通过 getObbDirs 的方法来获取,这个目录也是在外部存储区的。

Android ap上应用存储区

我们来回顾一下,在 Android ap 上应用存储区,它大概是一个什么样的情况。

应用程序存储区

一方面是应用程序,它都有一块属于自己的专用的内部存储区域,也叫应用私有存储区,所有存储在这个区域里面的内容,其他的应用无论有什么样的权限,都无法去直接访问。 因此这个区域就特别适合被用来保存那些属于应用、用户不需要直接访问的私有数据。

共享外部存储区

另一方面我们还有一块共享的外部存储区,也就是 shared storage。存储在这个区域里的内容,其他的应用可以通过申请权限去访问,用户也可以去直接访问。访问的方法就是将手机通过 USB 数据线连接到电脑,用户就可以在电脑端直接去看到这一块存储的内容。

要查看外部存储区,也就是 /sdcard 的目录下的内容应用程序,需要先跟用户申请权限。一般的情况下写到外部存储区的文件,不会被算入应用所占空间大小。但是有一个例外,就是我们可以看到,在外部存储区里经常会有一些以应用的 package 命名的目录,它是不需要任何的访问权限的。这些目录就包括刚才列举的那些方法所返回的目录。由于这些以应用的 package 命名的目录,它们是具有非常明确的所有权的结构,因此它所归属的应用不需要申请任何权限就可以直接去访问。这个所有权的设置,也就确保了当用户在卸载这款应用的时候,目录里面的文件会被系统清除。

而我们都知道在 Android 系统上,存储是应用程序经常使用的权限之一。 在Android AP 及 P 以前的版本,我们看到应用在对于外部存储区的使用上经常会有这样一些问题:它导致的直接结果就是用户经常会发现自己的手机的内存莫名其妙被占满了。

Android Q的存储变化

在 Android Q 上我们希望通过在明确归属、保护应用程序、保护用户程序这三个方面作出努力,对外部存储区的使用做出优化,从而能提升用户的整体使用体验。

明确归属

首先是更明确的空间归属,也就是对目录有一个更明确的所有权结构。我们通过避免在应用程序被卸载之后,还残留在用户磁盘上挥之不去的文件,来解决内存空间使用混乱的问题。 因此在 Android Q 上首先要确保系统能够清楚的知道哪些文件是属于哪些应用程序的,这也使得用户可以更容易地管理他们的文件。当然这也意味着当应用程序被用户卸载的时候,它写到磁盘里边的内容就会被清除。有些应用被用户卸载之后,卸载前写入的一部分数据,用户是希望在应用卸载之后还能继续去访问的,这一块的内容我们会在后面有讨论。

保护应用程序

第二个方面是加强应用的私有数据的保护。当应用将文件写到外部存储区的时候,有一些情况不希望其他的应用可以去随意访问。作为应用开发者,通常是不希望其他应用能随意地去查看这个应用在做什么。在 Android Q 上,我们通过禁止其他的应用可以随意地查看这部分数据,来保护应用的特有的私有数据。

保护用户程序

第三个方面是加强对用户数据的保护。当用户去下载一些私人聊天中的图片,或者是像银行类 APP 的对账对账单 pdf 文件的时候,用户当然不希望手机上装的其他应用都能去查看这部分内容,从而去收集他的个人隐私信息。在 Android Q 上,我们确保用户能对应用访问数据访问数据和文件的情况有所掌控,能知道应用程序在什么时间访问了哪些文件内容。

坚持这三点原则,也就意味着我们正在对 Android 平台的存储方式进行一个非常深刻的改变。我们知道应用可能需要一些稍长的时间来适应这些变化,但我们也认为从长远来看,它对 Android 平台会更有益处,能够让 Android 成为一个更好的用户平台,也能为用户提供更好的隐私保护。

Android Q 存储空间的访问

接下来,我们看一下在 Android Q 上存储空间的访问是什么样的?

在 Android Q 上,我们首先是在 Mediastore 这个方面着重发力。第一个变化就是应用程序现在不需要申请任何权限,就可以向 Mediastore 去提供内容。但是如果应用想要去查看其他应用为 Mediastore 提供的内容则必须获取 Storage 权限。应用程序还是可以继续读取和写入特有的目录,也就是以它的 package 命名的目录。但是其他应用程序,即便它已经获取了权限,也无法去访问以其他应用的 package 命名的目录。应用程序可以使用 system picker 去选择用户磁盘上的任何位置的文件,这个是为应用和用户提供了一种事务型的方法,可以去获取一个或者是多个文件。

我们认为这种方式来获取文件会更加安全,因为用户会清楚地知道目前是哪个应用正在访问哪个或哪些文件。此外在 Android Q 上,私有存储区的工作方式是没有变化的。

MediaStore

Mediastore 是一个属于用户的公用媒体文件集合,它从 Android 发布的第一个版本就已经存在。

之前我们就提到 Android Q 上的 MediaStore 会有重大改变,下面我们来看一下 Android Q 上的 Mediastore 是如何工作的。

Android Q 的 Mediastore 是如何工作

Mediastore 被设计用来保存那些属于用户,而不是属于特定某一款应用的,且其它应用程序可以访问的数据内容。那么哪些内容属于 Mediastore?就这与用户的期待和期望有关。我们有一个总体的把控原则,就是希望作为应用开发者,需要站在用户的角度去考虑,只把那些你觉得用户认为它应该属于 Mediastore 的内容放到 Mediastore 里面,比如说在相机里面拍摄到的照片等这样的数据等等。Mediastore 所可以接受的主要的类型有音频、视频和照片。 应用卸载后,写到 Mediastore 里内容是不会被删除的。但是有一个需要注意的地方:就是应用一旦被卸载之后,如果用户在后续还把这个应用重新安装回来,他去访问之前写到 Mediastore 里的内容的时候,是需要去申请权限的。只有在获取了存储权限之后,才能查看在上一个应用安装的生命周期里写入 Mediastore 的数据。那么哪些内容不属于 Mediastore 还是与用户的期望有关系。作为开发者,在大多数的情况下,你也是可以很明确地感知到哪些内容不应该被放入到 Mediastore 里的。

我们举一些例子来说:比如说你的应用里面会有一些缩略图文件,或者是如果你的应用是一个音乐播放类软件,他很可能会有一些专辑的封面图片这样的文件,或者是你的应用是一个美图类的应用,可能会有一些贴纸这样的资源文件。那么这样这些类型的文件就不适合被放到 Mediastore 里面。因为通常来说,用户是不会需要在其他的应用里去访问这些资源文件的,也同样是不希望应用被卸载之后,这些资源文件还残留在用户的内存区域中。

在一些情况下,开发者可能无法确定某些内容是否应该被放入用户的磁盘空间里,以及放到哪个具体的位置。

比如说聊天应用当中,用户可能会发送大量的图片文件,如果你直接就把这些文件存到 Mediastore ,就会让 Mediastore 变得很混乱。针对这种情况,我们建议开发者让用户自己去做出一些主动行为,比如说当用户明确做出保存行为时,才把这个文件保存到用户的磁盘上。

Android Q 上新增 Download 集合

Android Q上新增了一个 Download 集合,用于存储所有不适合被放到的 Mediastore(视频、音频、图片)集合里面的文件。

一个很具体的例子:比如说是银行对账单这样的 pdf 文件,是会被放到 Mediastore 集合里面的文件里,如果你觉得你很希望把这个文件也放到 Downloads 集合里面,当然也是可以的。 你可以把它放到集合当中来。如果是这种情况的话,这个文件它是会被同步到适当的媒体集合里面的。

权限变更

Mediastore 在提供内容和访问内容的时候,有哪些使用细节?

在往 Mediastore 提供内容,或者是编辑和删除自己放到 Mediastore 到里面的媒体内容时,不用去申请运行时的权限。但是如果你想查看其它的应用为 Mediastore 提供的内容,就必须申请存储权限。你申请权限之后会看到除了 download 集合以外的其他集合里的内容。Mediastore 里面的文件,在应用被用户卸载之后,还是会存储在用户的手机上。 当一个应用在向 Mediastore 提供内容的时候,直白一点说,就相当于应用在告诉系统:这个文件应该是属于用户的,用户很可能之后会希望在其他应用程序里也需要去使用这些数据。

如何向 Mediastore 添加内容

现在来看这个代码示例:如何向 Mediastore 添加内容。

首先你是创建一个 contentvalues 的映射。至少需要设置其中的 DISPLAY-NAME 和 MIME-TYPE。这里是 Mediastore 自己去帮你选择存储的默认位置。若有需要也可使用 relativepath 来设置使用特定的目录。

我们看到高亮的 IS_PENDING 这一行,这里是把 IS_PENDING 设为 True 的,表示对应的 item 还没准备好,希望系统能够暂时隐藏,直到准备好。这个是在执行一些运行时间比较长的处理,或者是等待加载的时候会比较有用。只有 item 的所有者程序,可以在项目被标记为 IS_pending 的时候去访问里面的内容。一旦你设置好了这些映射之后,并且确定了 item 的IS_PENDING 状态,接下来就只需要选择一个集合,然后讲然后将 item 存到里面就可以了。

当你的 item 的状态已经准备好之后,你就可以将 IS_PENDING 标记设置为false,来告诉系统说他们现在已经准备好了,可以供其他的应用来使用了。

变更存储目录

Mediastore 会根据文件的 RELATIVE_PATH 去自动进行分类存储,但是你也可以去设置 MediaColumns 里面的 RELATIVE_PATH,去变更他们在磁盘上的储存位置。系统会尽量确保你使用的 RELATIVE_PATH 是一个合理的路径。比如说对于 image.jpeg 类型的文件,它就会被放置在DCIM或者 photos 目录。

如果你还想去指定保存到某个特定的存储设备上,你可以通过使用 Mediastore 的get ExternalVolumeNames 或者是 get AllVolumeNames,然后去选择你想要的系统设备上所有的 VolumeNames,把它传递给 getContentUri 的方法。另外还有一个接口是 StorageManager 的 Storage Volume 这个方法。它可以告诉你目前系统连上的所有存储设备的底层信息。

这一页的代码演示的就是如何通过设置 RELATIVE_PATH,将一个音频文件存储到指定的设备及其指定的目录下。

关于 Mediastore 的查询也有一些需要特别注意的地方,由于现在是由系统去决定保存的位置,所以 DATA columns 已经被废弃。 希望大家使用 ContentResolver.query() 和 ContentResolver.openFileDescriptor() 这两个方法去取代它。我们在 Q 上有一些兼容性的逻辑来帮助应用适应这种变化。但是这种方案并不是特别的完美,所以我们还是强烈的建议开发者尽量去使用我们在 Q 上提供的 ContentResolver 里面的接口去实现这个变化。

如果你正在查询 Mediastore 然后你发现里面有一些隐藏的,也就是它的 IS_PENDING 标记被设置为 true 的这些项目,然后你又确认你的应用,真的去需要访问这些项目。 我们现在还有一个接口叫做 setincludePending(),通过调用这个接口,你可以设置获取到这些被标记为 IS_PENDING 的 item,但是即使你能获取到这些 item,你还是不能去访问里面的具体的数据的,只有这些 item owner 的 app 可以把 IS_PENDING 的标记设置为 false 之后,才可以去访问项目里面的具体内容。如果应用有需要在媒体代码里面去访问这些打开的文件,首先你需要在你的应用的 Java/Kotlin 代码当中打开文件,然后把这个打开的 find script 传递给你的 code。

我们这一页是一个代码片段,举例说明了如何通过 content 和 resolver 接口的方法来对 Mediastore 进行查询。

编辑文件

Storage 运行时的权限可以让应用去发现并读取其他的应用,存入到 Mediastore 里面的媒体文件,但是如果你需要修改这些文件的话,你需要去获取 Right_exstorage 权限。如果你的应用没有获取到必要的权限,就直接调用了update/delete/openFileDescriptor 这些方法,那就会收到系统抛出的异常。

你可以通过 try catch 来捕获这些异常,并且在里面做一些处理,而且我们非常鼓励开发者这样做。

Metadata

我们还注意到在照片文件中,通常还会有一个原始的地理位置的信息,这个信息是便于用户在之后查看这些照片的时候,还能回忆起这个照片当时是在什么地方拍摄的。但是地理位置信息对用户来说通常是比较敏感的个人信息,默认情况下 Android 会隐藏这个信息。如果你的应用需要去访问照片的地理位置信息,它需要在应用里面去声明 ACCESS_MEDIA_LOCATION 权限,然后还需要去调用 Mediastore 的 Save/Require/Origin 接口来指明需要获取某个 url 对应的文件的原始信息。

另外在 Mediatore 里面的 LATITUDE 和 LONGITUDE 这两个常量字段已经被废弃了,如果你需要获取经纬度信息的话,你现在需要使用 ExifInterface 接口来代替。

我们这里展示的代码片段,演示如何添加 ACCESS_MEDIA_LOCATION 权限,以及如何通过 ExifInterface 接口来获取文件的经纬度信息。

分区存储/Scoped Storage

我们现在来看一下,在 Android Q 上另一个引入的新东西叫分区存储,也叫隔离存储沙箱。

隔离存储沙箱可以让系统更好地对应用使用的存储空间有一个准确的统计,并且可以去保护应用的程序数据,以及用户的数据安全。

在应用 target Q 时,它就会自动进入分区存储模式,也就是沙箱模式了。这也就意味着应用不再可以直接访问SD卡的目录。如果你在 target Q 的情况下还去尝试直接访问SD卡,这个目录就会收到报错。虽然不可以直接去访问 SD 卡的根目录,但是还是可以直去访问以你的应用的 package 命名的目录。 如果想访问在你的 package name 目录以外的其他的地方的内容,你就必须使用 Mediastore 或者是存储访问框架 SAF。

本地、云端 一样处理

存储访问框架允许应用去调出 Android 里面的系统选择器,这个系统选择器会不断升级,不用担心说以后的版本升级 UI 会发生变更。选择器不仅可以让用户去选择本地的存储存储位置里面的文件,通过它用户也可以去选择那些存储在云端里面的文件,或者是其他的应用,通过 content provider 来的提供的那些文件。

如何更好的适配

我们刚才提到过说应用程序它在 Android Q 默认情况下,会被放到沙箱里面。然后我们认为这样的设置的变更是适用于绝大多数的用户场景,比如说用户如果想选择一些照片上传到社交网络,或者是在文件编辑器中打开和保存文档,或者是在浏览器里下载文件,这些用户场景都可以被很好地处理。然而我们在发布了 Beta 1 和 Beta 2 这两个版本之后,收到大量的开发者的反馈,普遍是说他们需要更长的一段时间来适配这种变化。所以在今年5月初的 IO 上,我们也宣布了一个在 Beta 1 和 Beta 2 里面没有的一个新东西。 现在应用可以通过加上一个新的标签来选择退出隔离模式。如果你的应用是 Android P 以及以下的版本时,那么这个标签就默认是 false。当然你也可以手动的将它设置为 true。当你将它设置为 true 的时候,无论应用的它给它SDK target version 是多少,只要应用运行在 Android Q 系统上,就会启用隔离存储沙箱模式。我们新增的标签只是一个暂时的解决方案,它的目的是给开发者能够有相对来说更长的一段时间去适配这个变化,而不是说启用了这个标签,你就可以永远的不受隔离存储沙箱的控制。

我们在明年发布的 Android 下一个大版本中会默认强制开启限制。除了下载集合以外,Mediastore 里面的其他媒体集合是不受隔离存储沙箱模式影响的。如果你的应用处于隔离存储沙箱模式,只能访问它自己向 Mediastore 的 download 集合提供的内容。

我们这一页展示了新增的 requestLegacyExternalStorage 这个标签,通过将这个标签设置为 true,你可以暂时不受沙箱的控制。当然你也可以通过下面的 isExternalStorageLegacy 这个接口去判断当前你的应用是否受到沙箱的控制。

Android Q Labs 直播专题页面

Android Q Labs 开场演讲

Android Q 有哪些更新

Android Q 现代化您的应用

后台 Activity 启动的限制

Android Q 手势导航

Jetpack 更新

Android Q 在折叠屏设备的适配

通用系统映像介绍

Google Play 商店政策

Android Q 地理位置权限变更

Android Q 深色主题

Android Q Labs 总结演讲