Android 存储用例和最佳实践

为了让用户能够更好地控制自己的文件并减少文件杂乱,Android 10 为应用引入了一种新的存储范式,称为作用域存储。作用域存储改变了应用在设备的外部存储上存储和访问文件的方式。为了帮助您迁移应用以支持作用域存储,请遵循本指南中概述的常见存储用例的最佳实践。用例分为两类:处理媒体文件处理非媒体文件

在许多情况下,您的应用会创建其他应用不需要访问或不应该访问的文件。系统提供了应用特定存储位置来管理此类文件。

要详细了解如何在 Android 上存储和访问文件,请参阅存储培训指南

处理媒体文件

本部分介绍了处理媒体文件(视频、图像和音频文件)的一些常见用例,并解释了应用可以使用的总体方法。下表总结了这些用例,并链接到包含更多详细信息的各个部分。

用例 概要
显示所有图像或视频文件 对所有版本的 Android 使用相同的方法。
显示特定文件夹中的图像或视频 对所有版本的 Android 使用相同的方法。
从照片中访问位置信息 如果您的应用使用作用域存储,请使用一种方法。如果您的应用选择不使用作用域存储,请使用另一种方法。
为新下载定义存储位置 如果您的应用使用作用域存储,请使用一种方法。如果您的应用选择不使用作用域存储,请使用另一种方法。
将用户媒体文件导出到设备 对所有版本的 Android 使用相同的方法。
修改或删除多个媒体文件(单次操作) 对 Android 11 使用一种方法。对于 Android 10,选择不使用作用域存储,并改用 Android 9 及更低版本的方法。
导入已存在的单个图像 对所有版本的 Android 使用相同的方法。
捕获单个图像 对所有版本的 Android 使用相同的方法。
与其他应用共享媒体文件 对所有版本的 Android 使用相同的方法。
与特定应用共享媒体文件 对所有版本的 Android 使用相同的方法。
访问使用直接文件路径的代码或库中的文件 对 Android 11 使用一种方法。对于 Android 10,选择不使用作用域存储,并改用 Android 9 及更低版本的方法。

显示来自多个文件夹的图像或视频文件

查询媒体集合 使用 query() API。要过滤或排序媒体文件,请调整 projectionselectionselectionArgssortOrder 参数。

显示特定文件夹中的图像或视频

使用此方法

  1. 遵循 请求应用权限 中概述的最佳实践,请求 READ_EXTERNAL_STORAGE 权限。
  2. 根据 MediaColumns.DATA 的值检索媒体文件,该值包含磁盘上媒体项的绝对文件系统路径。

注意: 当您访问现有媒体文件时,可以在您的逻辑中使用 DATA 列的值。这是因为此值具有有效的文件路径。但是,不要假设文件始终可用。做好处理可能发生的任何基于文件的 I/O 错误的准备。

另一方面,要创建或更新媒体文件,请不要使用 DATA 列。而是使用 DISPLAY_NAMERELATIVE_PATH 列。

从照片中访问位置信息

如果您的应用使用作用域存储,请按照媒体存储指南的 照片中的位置信息 部分中的步骤操作。

为新下载定义存储位置

如果您的应用使用作用域存储,请注意您选择用于存储下载媒体文件的位置。

如果其他应用需要访问文件,请考虑使用 定义明确的媒体集合 用于下载或文档集合。

在 Android 11 及更高版本上,外部应用特定目录中的文件对于其他应用不可访问,即使您使用 DownloadManager 获取这些文件。

将用户媒体文件导出到设备

定义存储用户媒体文件的适当默认位置

修改或删除多个媒体文件(单次操作)

合并基于您的应用运行的 Android 版本的逻辑。

在 Android 11 上运行

使用此方法

  1. 使用 MediaStore.createWriteRequest()MediaStore.createTrashRequest() 为您的应用的写入或删除请求创建挂起意图,然后通过调用该意图提示用户授予编辑一组文件的权限。
  2. 评估用户的响应

    • 如果授予了权限,请继续执行修改或删除操作。
    • 如果未授予权限,请向用户解释您的应用中的功能为何需要该权限。

详细了解如何使用这些可在 Android 11 及更高版本上使用的 管理一组媒体文件 方法。

在 Android 10 上运行

如果您的应用针对 Android 10(API 级别 29),选择退出作用域存储 并继续使用 Android 9 及更低版本的方法执行此操作。

在 Android 9 或更低版本上运行

使用此方法

  1. 遵循 请求应用权限 中概述的最佳实践,请求 WRITE_EXTERNAL_STORAGE 权限。
  2. 使用 MediaStore API 修改或删除媒体文件。

导入已存在的单个图像

当您想要导入已存在的单个图像时(例如,用作用户的个人资料照片),您的应用可以使用自己的 UI 执行操作,也可以使用系统选择器。

呈现您自己的用户界面

使用此方法

  1. 遵循 请求应用权限 中概述的最佳实践,请求 READ_EXTERNAL_STORAGE 权限。
  2. 使用 query() API 查询媒体集合
  3. 在您的应用的自定义 UI 中显示结果。

使用系统选择器

使用 ACTION_GET_CONTENT 意图,该意图要求用户选择要导入的图像。

如果您想要过滤系统选择器向用户呈现以供选择的图像类型,可以使用 setType()EXTRA_MIME_TYPES

捕获单个图像

当您想要捕获要用于应用的单个图像时(例如,用作用户的个人资料照片),请使用 ACTION_IMAGE_CAPTURE 意图要求用户使用设备的摄像头拍摄照片。系统将捕获的照片存储在 MediaStore.Images 表中。

与其他应用共享媒体文件

使用 insert() 方法直接将记录添加到 MediaStore 中。有关更多信息,请参阅媒体存储指南的 添加项目 部分。

与特定应用共享媒体文件

使用 Android FileProvider 组件,如 设置文件共享 指南中所述。

访问使用直接文件路径的代码或库中的文件

合并基于您的应用运行的 Android 版本的逻辑。

在 Android 11 上运行

使用此方法

  1. 遵循 请求应用权限 中概述的最佳实践,请求 READ_EXTERNAL_STORAGE 权限。
  2. 使用直接文件路径访问文件。

有关更多信息,请参阅关于如何使用 直接文件路径 打开媒体文件的章节。

在 Android 10 上运行

如果您的应用针对 Android 10(API 级别 29),选择退出作用域存储 并继续使用 Android 9 及更低版本的方法执行此操作。

在 Android 9 或更低版本上运行

使用此方法

  1. 遵循 请求应用权限 中概述的最佳实践,请求 WRITE_EXTERNAL_STORAGE 权限。
  2. 使用直接文件路径访问文件。

处理非媒体文件

本节介绍处理非媒体文件的一些常见用例,并解释您的应用可以使用的高级方法。下表汇总了这些用例中的每一个,并链接到包含更多详细信息的每个部分。

用例 概要
打开文档文件 对所有版本的 Android 使用相同的方法。
写入辅助存储卷上的文件 对 Android 11 使用一种方法。对更早版本的 Android 使用不同的方法。
从旧版存储位置迁移现有文件 尽可能将文件迁移到作用域存储。在必要时,为 Android 10 选择退出作用域存储。
与其他应用共享内容 对所有版本的 Android 使用相同的方法。
缓存非媒体文件 对所有版本的 Android 使用相同的方法。
将非媒体文件导出到设备 如果您的应用使用作用域存储,请使用一种方法。如果您的应用选择不使用作用域存储,请使用另一种方法。

打开文档文件

使用 ACTION_OPEN_DOCUMENT 意图要求用户使用系统选择器选择要打开的文件。如果您想要过滤系统选择器将向用户呈现以供选择的类型,可以使用 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 上运行

使用此方法

  1. 使用 作用域存储 模型。
  2. 针对 Android 10(API 级别 29)或更低版本。
  3. 声明 WRITE_EXTERNAL_STORAGE 权限。
  4. 执行以下类型的访问之一
    • 使用 MediaStore API 访问文件。
    • 使用 Filefopen() 等 API 直接访问文件路径。

在旧版本上运行

使用 存储访问框架,它允许用户选择应用可以在辅助存储卷上写入文件的存储位置。

从旧版存储位置迁移现有文件

如果目录不是应用特定目录或公共共享目录,则它被视为旧版存储位置。如果您的应用在旧版存储位置中创建或使用文件,我们建议您将应用的文件迁移到可以使用作用域存储访问的位置,并对应用进行必要的更改以使用作用域存储中的文件。

维护对旧版存储位置的访问以进行数据迁移

您的应用需要维护对旧版存储位置的访问,以便将任何应用文件迁移到可以使用作用域存储访问的位置。您应该使用的方法取决于应用的目标 API 级别。

如果您的应用针对 Android 11
  1. preserveLegacyExternalStorage 标志设置为 true保留旧版存储模型,以便您的应用在用户升级到针对 Android 11 的应用的新版本时迁移用户数据。

  2. 继续 选择退出作用域存储,以便您的应用可以继续在 Android 10 设备上的旧版存储位置访问文件。

如果您的应用针对 Android 10

选择退出作用域存储,以便更轻松地维护您的应用在 Android 版本之间的行为。

迁移应用数据

当您的应用准备迁移时,请使用以下方法

  1. 针对 Android 10 或更低版本。
  2. 选择退出作用域存储,以便您的应用可以访问您需要迁移的文件。
  3. 部署使用 File API 将文件从其当前位置移到 /sdcard/ 下的可使用作用域存储访问的位置的代码

    1. 将任何私有应用文件移动到由 getExternalFilesDir() 方法返回的目录。
    2. 将任何共享的非媒体文件移动到 Downloads/ 目录的应用专用子目录。
  4. /sdcard/ 目录中删除应用的旧版存储目录。

用户安装新版应用后,会在设备上完成数据迁移过程。您可以创建分析事件来监控整个用户群的迁移过程。

用户迁移数据后,发布另一个应用更新,目标为 Android 11。

与其他应用共享内容

若要与单个其他应用共享应用文件,请使用 FileProvider。对于所有需要相互共享文件的应用,建议每个应用都使用内容提供程序,然后在将应用添加到集合中时同步数据。

缓存非媒体文件

应使用的方法取决于需要缓存的文件类型。

将非媒体文件导出到设备

定义存储非媒体文件的适当默认位置。允许用户将文件从特定于应用的目录导出到更普遍可访问的位置。使用MediaStore 的下载或文档集合将非媒体文件导出到设备。

处理特定于应用的文件

如果您的应用创建了其他应用不需要或不应该访问的文件,您可以将这些文件存储在特定于应用的存储位置

内部存储目录

系统会阻止其他应用访问这些位置,并且在 Android 10(API 级别 29)及更高版本中,这些位置会进行加密。这些位置适合存储只有您的应用才能访问的敏感数据。

外部存储目录

如果内部存储空间不足以存储特定于应用的文件,请考虑使用外部存储。尽管其他应用可能访问这些目录(前提是该应用具有相应的权限),但存储在这些目录中的文件仅供您的应用使用。

在 Android 4.4(API 级别 19)及更高版本中,您的应用无需请求任何存储相关权限即可访问外部存储中的特定于应用的目录。

当用户卸载您的应用时,存储在特定于应用的存储中的文件将被删除,因此,您不应使用此存储来保存用户希望独立于您的应用持久保存的任何内容。

暂时退出范围存储

在您的应用完全兼容范围存储之前,您可以在测试中以及生产应用中暂时退出。

在测试中退出

在 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 10 或更低版本为目标的应用在使用范围存储时的行为,您可以通过将 requestLegacyExternalStorage 的值设置为 false 来选择加入此行为。如果您在运行 Android 11 的设备上进行测试,还可以使用应用兼容性标志来测试您的应用在启用或禁用范围存储时的行为。

其他资源

有关 Android 存储的更多信息,请查看以下资料

博文