Android NDK开发详解应用数据和文件之Android 存储用例和最佳做法
为了让用户更好地控制自己的文件并减少混乱,Android 10 针对应用推出了一种新的存储范例,称为分区存储。分区存储改变了应用在设备的外部存储设备中存储和访问文件的方式。为了帮助您迁移应用以支持分区存储,请遵循本指南中有关常见存储用例的最佳做法。这些用例分为两类:处理媒体文件和处理非媒体文件。
如需详细了解如何在 Android 平台中存储和访问文件,请参阅存储培训指南。
处理媒体文件
本部分介绍了处理媒体文件(视频、图片和音频文件)的一些常见用例,并概要说明了应用可以使用的方法。下表对其中每个用例进行了总结,并列出了包含更多详细介绍的各个部分的链接。
显示多个文件夹中的图片或视频文件
使用 query() API 查询媒体集合。如需对媒体文件进行过滤或排序,请调整 projection、selection、selectionArgs 和 sortOrder 参数。
显示特定文件夹中的图片或视频
请使用以下方法:
按照请求应用权限中所述的最佳做法,请求 READ_EXTERNAL_STORAGE 权限。
根据 MediaColumns.DATA 的值检索媒体文件,该值包含磁盘上的媒体项的绝对文件系统路径。
注意:当您访问现有媒体文件时,您可以使用您的逻辑中 DATA 列的值。这是因为,此值包含有效的文件路径。 但是,不要假设文件始终可用。请准备好处理可能发生的任何基于文件的 I/O 错误。
另一方面,如需创建或更新媒体文件,请勿使用 DATA 列,而是改用 DISPLAY_NAME 和 RELATIVE_PATH 列。
访问照片中的位置信息
如果应用使用分区存储,请按照媒体存储指南的照片中的位置信息部分的步骤操作。
注意:如访问共享存储空间中的媒体文件页面中所述,以 Android 10 或更高版本为目标平台的应用需要 ACCESS_MEDIA_LOCATION 权限,才能读取使用 MediaStore API 访问的图像中的未编辑位置信息。
为新下载内容定义存储位置
如果您的应用使用分区存储,请注意您选择存储下载媒体文件的位置。
如果其他应用需要访问文件,不妨针对下载内容或文档集合考虑使用明确定义的媒体集合。
注意:在 Android 11 或更高版本(无论目标 SDK 级别是什么)中,其他应用无法访问外部存储设备上的应用专用目录中存储的文件。
在 Android 11 及更高版本中,即使您使用 DownloadManager 提取文件,其他应用也无法访问外部应用专用目录中的这些文件。
将用户媒体文件导出到设备
定义适当的默认位置来存储用户媒体文件:
允许用户使用应用专用存储空间或共享存储空间,选择是否允许其他应用读取其媒体文件。
允许用户将文件从应用专用目录导出到一个更常用的位置。使用 MediaStore 的图片、视频和音频集合将媒体文件导出到设备的媒体库中。
注意:为避免混乱,请使用 externalStoragePublicDirectory() 或 externalMediaDirs() 等通常可访问的位置。
在一次操作中修改或删除多个媒体文件
根据应用在哪个 Android 版本上运行来纳入逻辑。
在 Android 11 上运行
请使用以下方法:
使用 MediaStore.createWriteRequest() 或 MediaStore.createTrashRequest() 为应用的写入或删除请求创建待定 intent,然后通过调用该 intent 提示用户授予修改一组文件的权限。
评估用户的响应:
如果授予了权限,请继续修改或删除操作。
如果未授予权限,请向用户说明您的应用中的功能为何需要该权限。
详细了解如何使用 Android 11 及更高版本提供的这些方法管理媒体文件组。
在 Android 10 上运行
如果您的应用以 Android 10(API 级别 29)为目标平台,请停用分区存储,继续使用适用于 Android 9 及更低版本的方法来执行此操作。
在 Android 9 或更低版本上运行
请使用以下方法:
按照请求应用权限中所述的最佳做法,请求 WRITE_EXTERNAL_STORAGE 权限。
使用 MediaStore API 修改或删除媒体文件。
导入已经存在的单张图片
当您要导入已经存在的单张图片(例如,用作用户个人资料的照片)时,应用可以将自己的界面用于此操作,也可以使用系统选择器。
提供您自己的界面
请使用以下方法:
按照请求应用权限中所述的最佳做法,请求 READ_EXTERNAL_STORAGE 权限。
使用 query() API 查询媒体集合。
在应用的自定义界面中显示结果。
使用系统选择器
使用 ACTION_GET_CONTENT intent,它会要求用户选择要导入的图片。
如果您想过滤系统选择器提供给用户选择的图片类型,您可以使用 setType() 或 EXTRA_MIME_TYPES。
拍摄单张图片
当您想拍摄单张图片在应用中使用(例如,用作用户个人资料的照片)时,请使用 ACTION_IMAGE_CAPTURE intent 要求用户使用设备的摄像头拍照。系统会将拍摄的照片存储在 MediaStore.Images 表中。
与其他应用共享媒体文件
使用 insert() 方法将记录直接添加到 MediaStore 中。如需了解详情,请参阅媒体存储指南的添加项目部分。
与特定应用共享媒体文件
按照设置文件共享指南中所述,使用 Android FileProvider 组件。
从代码或依赖库中使用直接文件路径访问文件
根据应用在哪个 Android 版本上运行来纳入逻辑。
在 Android 11 上运行
请使用以下方法:
按照请求应用权限中所述的最佳做法,请求 READ_EXTERNAL_STORAGE 权限。
使用直接文件路径访问文件。
如需了解详情,请参阅有关如何使用直接文件路径打开媒体文件的部分。
在 Android 10 上运行
如果您的应用以 Android 10(API 级别 29)为目标平台,请停用分区存储,继续使用适用于 Android 9 及更低版本的方法来执行此操作。
在 Android 9 或更低版本上运行
请使用以下方法:
按照请求应用权限中所述的最佳做法,请求 WRITE_EXTERNAL_STORAGE 权限。
使用直接文件路径访问文件。
处理非媒体文件
本部分介绍了处理非媒体文件的一些常见用例,并概要说明了应用可以使用的方法。下表对其中每个用例进行了总结,并列出了包含更多详细介绍的各个部分的链接。
打开文档文件
使用 ACTION_OPEN_DOCUMENT intent 要求用户使用系统选择器选择要打开的文件。如果您想过滤系统选择器提供给用户选择的文件类型,您可以使用 setType() 或 EXTRA_MIME_TYPES。
例如,您可以使用以下代码查找所有 PDF、ODT 和 TXT 文件:
Kotlin
startActivityForResult(
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(
"application/pdf", // .pdf
"application/vnd.oasis.opendocument.text", // .odt
"text/plain" // .txt
))
},
REQUEST_CODE
)
Java
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {
"application/pdf", // .pdf
"application/vnd.oasis.opendocument.text", // .odt
"text/plain" // .txt
});
startActivityForResult(intent, REQUEST_CODE);
写入辅助存储卷中的文件
辅助存储卷包括 SD 卡。您可以使用 StorageVolume 类访问有关给定存储卷的信息。
根据应用在哪个 Android 版本上运行来纳入逻辑。
在 Android 11 上运行
注意:以下方法只允许访问可靠卷,这是应用在大多数情况下可以成功访问的卷。
请使用以下方法:
使用分区存储模型。
以 Android 10(API 级别 29)或更低版本为目标平台。
声明 WRITE_EXTERNAL_STORAGE 权限。
执行以下一种访问权限:
使用 MediaStore API 访问文件。
使用 File 或 fopen() 等 API 直接访问文件路径。
在旧版本上运行
使用存储访问框架,该框架允许用户在辅助存储卷上选择应用可写入文件的位置。
从旧版存储位置迁移现有文件
如果目录不是应用专属目录或公开共享目录,则被视为旧版存储位置。如果您的应用要在旧版存储位置中创建文件或使用其中的文件,我们建议您将应用的文件迁移到可通过分区存储进行访问的位置,并对应用进行必要的更改以使用分区存储中的文件。
保留对旧版存储位置的访问权限以进行数据迁移
您的应用需要保留对旧版存储位置的访问权限,才能将任何应用文件迁移到可通过分区存储进行访问的位置。您应该使用的方法取决于应用的目标 API 级别。
如果应用以 Android 11 为目标平台
将 preserveLegacyExternalStorage 标记设为 true,以保留旧版存储模型,以便在用户升级到以 Android 11 为目标平台的新版应用时,应用可以迁移用户的数据。
注意:如果将 preserveLegacyExternalStorage 设置为 true,则旧版存储模型只在用户卸载您的应用之前保持有效。如果用户在搭载 Android 11 的设备上安装或重新安装您的应用,那么无论 preserveLegacyExternalStorage 的值是什么,您的应用都无法停用分区存储模型。
继续停用分区存储,以便您的应用可以继续在搭载 Android 10 的设备上访问旧版存储位置中的文件。
如果应用以 Android 10 为目标平台
停用分区存储,更轻松地在不同 Android 版本之间保持应用行为不变。
迁移应用数据
当应用准备就绪,可以迁移时,请使用以下方法:
以 Android 10 或更低版本为目标平台
停用分区存储,以便您的应用可以访问您需要迁移的文件。
部署使用 File API 的代码,将文件从 /sdcard/ 下的当前位置移动到可通过分区存储访问的位置:
将任何专用应用文件移至 getExternalFilesDir() 方法返回的目录。
将任何共享的非媒体文件移至 Downloads/ 目录的应用专用子目录中。
从 /sdcard/ 目录中移除应用的旧存储目录。
用户在安装应用的新版本后,会在其设备上完成数据迁移过程。您可以通过创建分析事件来监控整个用户群的迁移过程。
用户迁移其数据后,以 Android 11 为目标平台为您的应用再发布一项更新。
与其他应用共享内容
如需与一个其他应用共享应用的文件,请使用 FileProvider。对于全部需要在彼此之间共享文件的应用,我们建议您对每个应用使用内容提供程序,然后在将应用添加到集合中时同步数据。
缓存非媒体文件
您应该使用的方法取决于您需要缓存的文件类型。
小文件或包含敏感信息的文件:请使用 Context#getCacheDir()。
大型文件或不含敏感信息的文件:请使用 Context#getExternalCacheDir()。
将非媒体文件导出到设备
定义一个适当的默认位置来存储非媒体文件。允许用户将文件从应用专用目录导出到一个更常用的位置。使用 MediaStore 的下载内容或文档集合,可将非媒体文件导出到设备。
注意:为避免混乱,请使用 externalStoragePublicDirectory() 或 externalMediaDirs() 等通常可供访问的位置。
暂时停用分区存储
在您的应用与分区存储完全兼容之前,您可以在测试和正式版应用中暂时选择停用分区存储。
停用测试
在 Android 10(API 级别 29)及更高版本中,应用的测试默认在存储沙盒中运行。此沙盒可防止您的应用访问应用专属目录和公开共享的目录之外的文件。
如果测试输出主机文件(例如屏幕截图、调试数据、覆盖率数据或性能指标),您可以将这些文件写入全局目录。为此,请将以下标记添加到调用 am instrument 的相关自动化测试框架中:
-e no-isolated-storage 1
此标记会影响插桩测试用例的所有行为,并会影响所有调用的测试代码。因此,使用此标记时,您无法验证应用与分区存储的兼容性。对于测试输出,最好改为写入可以通过 shell 读取的应用分区存储空间。然后,您可以拉取该应用分区目录。若想确定从哪个目录拉取,请调用 getExternalMediaDirs()。
注意:应用卸载后,应用分区存储空间内的文件将不会保留。
在正式版应用中选择停用
如果您的应用以 Android 10(API 级别 29)或更低版本为目标平台,您可以暂时在正式版应用中停用分区存储。不过,如果您以 Android 10 为目标平台,则需要在应用的清单文件中将 requestLegacyExternalStorage 的值设置为 true:
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
注意:当您将应用更新为以 Android 11(API 级别 30)为目标平台后,如果应用在搭载 Android 11 的设备上运行,系统会忽略 requestLegacyExternalStorage 属性,因此您的应用必须做好支持分区存储并为这些设备上的用户迁移应用数据的准备。
如需测试以 Android 10 或更低版本为目标平台的应用在使用分区存储时的行为,您可以通过将 requestLegacyExternalStorage 的值设置为 false,选择启用该行为。如果在搭载 Android 11 的设备上进行测试,您还可以使用应用兼容性标记来测试应用在使用和不使用分区存储时的行为。
其他资源
如需详细了解 Android 存储空间,请查看以下资料:
博文
为 Viber 用户带来现代存储机制
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2023-03-30。