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

博文