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() 为您的应用写入或删除请求创建待处理 intent,然后通过调用该 intent 提示用户授予编辑一组文件的权限。
  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 intent,它会要求用户选择要导入的图片。

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

捕获单个图片

当您想捕获单个图片以在应用中使用(例如,用作用户个人资料照片)时,请使用 ACTION_IMAGE_CAPTURE intent 要求用户使用设备相机拍照。系统会将捕获的照片存储在 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 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 上

使用此方法

  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 文件中

<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 存储的更多信息,请查看以下资料

博客文章