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_NAME 列和 RELATIVE_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 修改或删除媒体文件。

导入已经存在的单张图片

当您要导入已经存在的单张图片(例如,用作用户个人资料的照片)时,应用可以将自己的界面用于此操作,也可以使用系统选择器。

提供您自己的界面

请使用以下方法:

  1. 按照请求应用权限中所述的最佳做法,请求 READ_EXTERNAL_STORAGE 权限。
  2. 使用 query() API 查询媒体集合
  3. 在应用的自定义界面中显示结果。

使用系统选择器

使用 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 ... >
  <!-- This attribute is "false" by default on apps targeting
       Android 10. -->
  <application android:requestLegacyExternalStorage="true" ... >
    ...
  </application>
</manifest>

如需测试以 Android 10 或更低版本为目标平台的应用在使用分区存储时的行为,您可以通过将 requestLegacyExternalStorage 的值设置为 false,选择启用该行为。如果在搭载 Android 11 的设备上进行测试,您还可以使用应用兼容性标记来测试应用在使用和不使用分区存储时的行为。

其他资源

如需详细了解 Android 存储空间,请查看以下资料:

博文