来自 https://developer.android.google.cn/preview/privacy/storage
Android 11 中的存储机制更新
Android 11 进一步增强了平台功能,为外部存储设备上的应用和用户数据提供了更好的保护。预览版引入了多项去年在 Android 开发者峰会上宣布的增强功能,例如可主动选择启用的媒体原始文件路径访问机制、面向媒体的批量修改操作,以及存储访问框架的界面更新。
为了帮助开发者轻松过渡到使用分区存储,该平台为开发者引入了进一步的改进。如需详细了解如何根据应用的用例迁移应用以使用分区存储,请参阅本页的分区存储部分、Android 存储用例和最佳做法指南,以及标题为 Android 存储常见问题解答的媒体文章。
我们一如既往地诚邀您提供反馈,帮助我们完善下一版 Android。请使用问题跟踪器向我们发送反馈意见。
强制执行分区存储
为了给开发者更多时间进行测试,以 Android 10(API 级别 29)为目标平台的应用仍可请求 requestLegacyExternalStorage
属性。应用可以利用此标记暂时停用与分区存储相关的变更,例如授予对不同目录和不同类型的媒体文件的访问权限。当您将应用更新为以 Android 11 为目标平台后,系统会忽略 requestLegacyExternalStorage
标记。
保持与 Android 10 的兼容性
如果应用在 Android 10 设备上运行时选择退出分区存储,建议您继续在应用的清单文件中将 requestLegacyExternalStorage
设为 true
。这样,应用就可以在运行 Android 10 的设备上继续按预期运行。
将数据迁移到使用分区存储时可见的目录
如果您的应用使用旧版存储模型且之前以 Android 10 或更低版本为目标平台,您可能会将数据存储到启用分区存储模型后您的应用无法访问的目录中。在以 Android 11 为目标平台之前,请将数据迁移到与分区存储兼容的目录。在大多数情况下,您可以将数据迁移到您的应用专用目录。
如果您有需要迁移的数据,当用户升级到以 Android 11 为目标平台的新版应用时,可以保留旧版存储模型。这样,用户就可以保留对您的应用之前用于保存数据的目录中存储的应用数据的访问权限。如需启用旧版存储模型以进行升级,请在应用的清单中将 preserveLegacyExternalStorage
属性设为 true
。
注意:大多数应用都不需要使用 preserveLegacyExternalStorage
。此标记仅适用于这样一种情况:您将应用数据迁移到了与分区存储兼容的位置,并且希望用户在更新您的应用时保留对数据的访问权限。使用此标记会导致更难以测试分区存储对您应用的用户有何影响,因为当用户更新您的应用时,它会继续使用旧版存储模型。
如果您使用 preserveLegacyExternalStorage
,旧版存储模型只在用户卸载您的应用之前保持有效。如果用户在搭载 Android 11 的设备上安装或重新安装您的应用,那么无论 preserveLegacyExternalStorage
的值是什么,您的应用都无法停用分区存储模型。
测试分区存储
如需在您的应用中启用分区存储,而不考虑应用的目标 SDK 版本和清单标记值,请启用以下应用兼容性标记:
-
DEFAULT_SCOPED_STORAGE
(默认情况下,对所有应用处于启用状态) -
FORCE_ENABLE_SCOPED_STORAGE
(默认情况下,对所有应用处于停用状态)
如需停用分区存储而改用旧版存储模型,请取消设置这两个标记。
管理设备存储空间
在 Android 11 上,使用分区存储模型的应用只能访问自身的应用专用缓存文件。如果您的应用需要管理设备存储空间,请执行以下操作:
通过调用
ACTION_MANAGE_STORAGE
intent 操作检查可用空间。-
如果设备上的可用空间不足,请提示用户同意让您的应用清除所有缓存。为此,请调用
ACTION_CLEAR_APP_CACHE
intent 操作。注意:
ACTION_CLEAR_APP_CACHE
intent 操作会严重影响设备的电池续航时间,并且可能会从设备上移除大量的文件。
外部存储设备上的应用专用目录
在 Android 11 上,应用无法在外部存储设备上创建自己的应用专用目录。如需访问系统为您的应用提供的目录,请调用 getExternalFilesDirs()
。
媒体文件访问权限
为了在保证用户隐私的同时可以更轻松地访问媒体,Android 11 增加了以下功能。
执行批量操作
为实现各种设备之间的一致性并增加用户便利性,Android 11 向 MediaStore
API 中添加了多种方法。对于希望简化特定媒体文件更改流程(例如在原位置编辑照片)的应用而言,这些方法尤为有用。
添加的方法如下:
<dl style="box-sizing: inherit; margin: 0px; padding: 0px;">
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createWriteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">用户向应用授予对指定媒体文件组的写入访问权限的请求。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createFavoriteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">用户将设备上指定的媒体文件标记为“收藏”的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为“收藏”。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createTrashRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">
用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容会在系统定义的时间段后被永久删除。
注意:如果您的应用是设备 OEM 的预安装图库应用,您可以将文件放入垃圾箱而不显示对话框。如需执行该操作,请直接将 IS_TRASHED
设置为 1
。</dd>
<dt style="box-sizing: inherit; font: 700 16px/24px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;">createDeleteRequest()
</dt>
<dd style="box-sizing: inherit; margin: 16px 0px; padding: 0px 0px 0px 40px;">
用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求。
</dd>
</dl>
系统在调用以上任何一个方法后,会构建一个 PendingIntent
对象。应用调用此 intent 后,用户会看到一个对话框,请求用户同意应用更新或删除指定的媒体文件。
例如,以下是构建 createWriteRequest()
调用的方法:
<devsite-selector scope="auto" active="kotlin" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-kotlin" tab="kotlin" id="kotlin" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">KOTLIN</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-java" tab="java" id="java" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">JAVA</tab></devsite-tabs> <devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 0px -23px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="lang-kotlin" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px 24px 24px 23px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">val urisToModify = /* A collection of content URIs to modify. */ val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify) // Launch a system prompt requesting user permission for the operation. startIntentSenderForResult(editPendingIntent.intentSender, <var style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">EDIT_REQUEST_CODE</var>, null, 0, 0, 0) </pre></devsite-code></devsite-selector>
评估用户的响应,然后继续操作,或者在用户不同意时向用户说明您的应用为何需要获取权限:
<devsite-selector scope="auto" active="kotlin" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-kotlin" tab="kotlin" id="kotlin" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">KOTLIN</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-java" tab="java" id="java" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">JAVA</tab></devsite-tabs> <devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 0px -23px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="lang-kotlin" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px 24px 24px 23px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { ... when (requestCode) { <var style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">EDIT_REQUEST_CODE</var> -> if (resultCode == Activity.RESULT_OK) { /* Edit request granted; proceed. / } else { / Edit request not granted; explain to the user. */ } } } </pre></devsite-code></devsite-selector>
您可以对 createFavoriteRequest()
、createTrashRequest()
和 createDeleteRequest()
使用相同的通用模式。
使用直接文件路径和原生库访问文件
为了帮助您的应用更顺畅地使用第三方媒体库,Android 11 允许您使用除 MediaStore
API 之外的 API 访问共享存储空间中的媒体文件。不过,您也可以转而选择使用以下任一 API 直接访问媒体文件:
-
File
API。 - 原生库,例如
fopen()
。
如果您的应用没有任何存储权限,您可以使用直接文件路径访问归因于您的应用的媒体文件。如果您的应用具有 READ_EXTERNAL_STORAGE
权限,则可以使用直接文件路径访问所有媒体文件,无论这些文件是否归因于您的应用。
如果您直接访问媒体文件,建议您在应用的清单文件中将 requestLegacyExternalStorage
设置为 true
以停用分区存储。这样,您的应用就可以在搭载 Android 10 的设备上正常工作。
性能
当您使用直接文件路径依序读取媒体文件时,其性能与 MediaStore
API 相当。
但是,当您使用直接文件路径随机读取和写入媒体文件时,进程的速度可能最多会慢一倍。在此类情况下,我们建议您改为使用 MediaStore
API。
媒体库中的可用值
当您访问现有媒体文件时,您可以使用您的逻辑中 DATA
列的值。这是因为,此值包含有效的文件路径。但是,不要假设文件始终可用。请准备好处理可能发生的任何基于文件的 I/O 错误。
另一方面,如需创建或更新媒体文件,请勿使用 DATA
列的值。请改用 DISPLAY_NAME
和 RELATIVE_PATH
列的值。
访问其他应用中的数据
为了保护用户隐私,Android 11 进一步限制您的应用访问其他应用的私有目录。
访问内部存储设备上的数据目录
<devsite-selector scope="auto" active="变更详情" ready="" style="box-sizing: inherit; pointer-events: auto; visibility: visible; background: rgb(255, 255, 255); border: 1px solid rgb(232, 234, 237); display: block; font: 14px/20px Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; margin: 16px 0px;"><devsite-tabs role="tablist" connected="" style="box-sizing: inherit; display: flex; -webkit-box-flex: 1; flex-grow: 1; height: 48px; max-width: none; position: relative; border-bottom: 1px solid rgb(232, 234, 237);"><tab role="tab" aria-selected="true" aria-controls="tabpanel-变更详情" tab="变更详情" id="change-details" active="" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">变更详情</tab><tab role="tab" aria-selected="false" aria-controls="tabpanel-如何切换" tab="如何切换" id="how-to-toggle" style="box-sizing: inherit; flex-shrink: 0; position: relative; display: flex;">如何切换</tab></devsite-tabs>
变更名称:APP_DATA_DIRECTORY_ISOLATION
变更 ID:143937733
</devsite-selector>
Android 9(API 级别 28)开始限制哪些应用可使其内部存储设备上数据目录中的文件可由其他应用进行全局访问。以 Android 9 或更高版本为目标平台的应用不能使其数据目录中的文件全局可访问。
Android 11 在此限制的基础上进行了扩展。如果您的应用以 Android 11 为目标平台,则不能访问其他任何应用的数据目录中的文件,即使其他应用以 Android 8.1(API 级别 27)或更低版本为目标平台且已使其数据目录中的文件全局可读也是如此。
访问外部存储设备上的应用专用目录
在 Android 11 上,应用无法再访问外部存储设备中的任何其他应用的专用于特定应用的目录中的文件。
文档访问限制
为让开发者有时间进行测试,以下与存储访问框架 (SAF) 相关的变更只有在应用以 Android 11 为目标平台时才会生效。
访问目录
您无法再使用 ACTION_OPEN_DOCUMENT_TREE
intent 操作请求访问以下目录:
- 内部存储卷的根目录。
- 设备制造商认为可靠的各个 SD 卡卷的根目录,无论该卡是模拟卡还是可移除的卡。可靠的卷是指应用在大多数情况下可以成功访问的卷。
-
Download
目录。
访问文件
您无法再使用 ACTION_OPEN_DOCUMENT_TREE
或 ACTION_OPEN_DOCUMENT
intent 操作请求用户从以下目录中选择单独的文件:
-
Android/data/
目录及其所有子目录。 -
Android/obb/
目录及其所有子目录。
测试变更
如需测试此行为更改,请执行以下操作:
- 通过
ACTION_OPEN_DOCUMENT
操作调用 intent。检查Android/data/
和Android/obb/
目录是否均不显示。 - 执行以下某项操作:
- 启用
RESTRICT_STORAGE_ACCESS_FRAMEWORK
应用兼容性标记。 - 以 Android 11 为目标平台。
- 启用
- 通过
ACTION_OPEN_DOCUMENT_TREE
操作调用 intent。检查Download
目录是否已显示,以及与目录关联的操作按钮是否呈灰显状态。
权限
Android 11 引入了与存储权限相关的以下变更。
以任何版本为目标平台
[图片上传失败...(image-f14c5a-1598409913523)]
<figcaption style="box-sizing: inherit; font-size: 14px; margin-top: -4px;">图 1. 应用使用分区存储并请求 READ_EXTERNAL_STORAGE
权限时显示的对话框。</figcaption>
不管应用的目标 SDK 版本是什么,以下变更均会在 Android 11 中生效:
存储运行时权限已重命名为文件和媒体。
-
如果您的应用未停用分区存储并且请求
READ_EXTERNAL_STORAGE
权限,用户会看到不同于 Android 10 的对话框。该对话框表明您的应用正在请求访问照片和媒体,如图 1 所示。用户可以在系统设置中查看哪些应用具有
READ_EXTERNAL_STORAGE
权限。在设置 > 隐私 > 权限管理器 > 文件和媒体页面上,具有该权限的每个应用都列在允许存储所有文件下。注意:如果您的应用以 Android 11 为目标平台,请记住,对“所有文件”的这种访问权限是只读访问权限。如需使用此应用读取和写入共享的存储空间中的所有文件,需要具有所有文件访问权限。
以 Android 11 为目标平台
如果应用以 Android 11 为目标平台,那么 WRITE_EXTERNAL_STORAGE
权限和 WRITE_MEDIA_STORAGE
特许权限将不再提供任何其他访问权限。
请注意,在搭载 Android 10(API 级别 29)或更高版本的设备上,您的应用可以提供明确定义的媒体集合,例如 MediaStore.Downloads
,而无需请求任何存储相关权限。详细了解如何在处理应用中的媒体文件时仅请求必要的权限。
所有文件访问权限
绝大多数需要共享存储空间访问权限的应用都可以遵循分区存储最佳做法,例如存储访问框架或 MediaStore API。但是,某些应用的核心用例需要广泛访问设备上的文件,但无法采用注重隐私保护的存储最佳做法高效地完成这些操作。
例如,防病毒应用的主要用例可能需要定期扫描不同目录中的许多文件。如果此扫描需要反复的用户交互,让其使用系统文件选择器选择目录,可能就会带来糟糕的用户体验。其他用例(如文件管理器应用、备份和恢复应用以及文档管理应用)可能也需要考虑类似情况。
应用可通过执行以下操作,向用户请求名为“所有文件访问权限”的特殊应用访问权限:
- 在清单中声明
MANAGE_EXTERNAL_STORAGE
权限。 - 使用
ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。
如需确定您的应用是否已获得 MANAGE_EXTERNAL_STORAGE
权限,请调用 Environment.isExternalStorageManager()
。
MANAGE_EXTERNAL_STORAGE
权限会授予以下权限:
-
对共享存储空间中的所有文件的读写访问权限。
注意:
/sdcard/Android/media
目录是共享存储空间的一部分。 对
MediaStore.Files
表的内容的访问权限。对 USB On-The-Go (OTG) 驱动器和 SD 卡的根目录的访问权限。
-
除
/Android/data/
、/sdcard/Android
和/sdcard/Android
的大多数子目录外,对所有内部存储目录的写入权限。此写入权限包括文件路径访问权限。获得此权限的应用仍然无法访问属于其他应用的应用专用目录,因为这些目录在存储卷上显示为
Android/data/
的子目录。
当应用具有 MANAGE_EXTERNAL_STORAGE
权限时,它可以使用 MediaStore
API 或文件路径访问这些额外的文件和目录。但是,当您使用存储访问框架时,只有在您不具备 MANAGE_EXTERNAL_STORAGE
权限也能访问文件或目录的情况下才能访问文件或目录。
为测试启用
如需了解“所有文件访问权限”这项权限对您的应用有何影响,您可以为了测试目的启用该权限。为此,请在连接到测试设备的计算机上运行以下命令:
<devsite-code style="box-sizing: inherit; clear: both; display: block; margin: 16px 0px; overflow: hidden; position: relative; direction: ltr !important;"><pre class="none devsite-terminal" translate="no" dir="ltr" is-upgraded="" style="box-sizing: inherit; background: rgb(241, 243, 244); color: rgb(55, 71, 79); font: 14px/20px "Roboto Mono", monospace; padding: 24px; direction: ltr !important; text-align: left !important; margin: 0px; overflow-x: auto; position: relative;">adb shell appops set --uid <var translate="no" style="box-sizing: inherit; color: rgb(236, 64, 122); -webkit-font-smoothing: auto; font-weight: 700;">PACKAGE_NAME</var> MANAGE_EXTERNAL_STORAGE allow
</pre></devsite-code>
Google Play 通知 [图片上传失败...(image-48e432-1598409913522)]
此部分为在 Google Play 上发布应用的开发者提供通知。
为了限制对共享存储的广泛访问,Google Play 商店已更新其政策,用来评估以 Android 11 为目标平台且通过 MANAGE_EXTERNAL_STORAGE
权限请求“所有文件访问权限”的应用。
仅当您的应用无法有效利用更有利于保护隐私的 API(如存储访问框架或 Media Store API)时,您才能请求 MANAGE_EXTERNAL_STORAGE
权限。此外,应用对此权限的使用必须在允许的使用情形范围内,并且必须与应用的核心功能直接相关。如果您的应用包含与以下示例类似的用例,很可能允许您的应用请求 MANAGE_EXTERNAL_STORAGE
权限:
- 文件管理器
- 备份和恢复
- 防病毒应用
- 文档管理应用
出于新型冠状病毒肺炎 (COVID-19) 方面的考虑,在 2021 年初之前,以 Android 11 为目标平台且需要 MANAGE_EXTERNAL_STORAGE
权限的应用无法上传到 Google Play。这包括新应用以及现有应用的更新。如需了解详情,请阅读政策帮助中心内更新后的政策。
注意:只有在您的应用以 Android 11 为目标平台且请求 MANAGE_EXTERNAL_STORAGE
权限时,此上传限制才会对其产生影响。
目前,如果您认为自己的应用需要管理外部存储权限,建议您暂时不要将目标 SDK 更新为 Android 11。如果您的应用以 Android 10 为目标平台,不妨考虑使用 requestLegacyExternalStorage
标记。
<devsite-page-rating position="footer" selected-rating="0" hover-rating-star="0" style="box-sizing: inherit; display: block; border-top: 1px solid rgb(218, 220, 224); margin: 16px -40px -40px; padding: 31px 40px 40px; text-align: center; color: rgb(32, 33, 36); font-family: Roboto, "Noto Sans", "Noto Sans JP", "Noto Sans KR", "Noto Naskh Arabic", "Noto Sans Thai", "Noto Sans Hebrew", "Noto Sans Bengali", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">此页内容对您有帮助吗?</devsite-page-rating>