将文件保存到外部存储

对于您要与其他应用共享或允许用户使用计算机访问的文件,将其存储在外部存储上是很好的选择。

外部存储通常是通过可移动设备(例如 SD 卡)来提供的。Android 使用路径(例如 /sdcard)来表示这些设备。

在您请求存储权限确认存储可用后,可以保存以下类型的文件:

  • 公开文件:应可供其他应用和用户自由访问的文件。在用户卸载您的应用后,这些文件应该仍然可供用户使用。例如,您的应用拍摄的照片应保存为公开文件。
  • 私有文件:存储在特定于应用的目录中的文件(使用 Context.getExternalFilesDir() 来访问)。这些文件在用户卸载您的应用时会被清除。尽管这些文件在技术上可被用户和其他应用访问(因为它们存储在外部存储上),但它们不能为应用之外的用户提供价值。可以使用此目录来存储您不想与其他应用共享的文件。

本指南介绍如何管理存储在设备的外部存储设备上的文件。要了解如何使用内部存储中的文件,请参阅有关如何管理内部存储中的文件的指南。

设置虚拟外部存储设备

在没有可移动外部存储的设备上,可使用以下命令启用虚拟磁盘进行测试:

    adb shell sm set-virtual-disk true
    

请求外部存储权限

Android 包含以下访问外部存储中的文件的权限:

READ_EXTERNAL_STORAGE
允许应用访问外部存储设备中的文件。
WRITE_EXTERNAL_STORAGE
允许应用在外部存储设备中写入和修改文件。拥有此权限的应用也会自动获得 READ_EXTERNAL_STORAGE 权限。

从 Android 4.4(API 级别 19)开始,在特定于应用的目录中读取或写入文件不再需要任何与存储相关的权限。因此,如果您的应用支持 Android 4.3(API 级别 18)及更低版本,并且您只想访问特定于应用的目录,则应添加 maxSdkVersion 属性,声明仅在较低版本的 Android 上请求权限:

    <manifest ...>
        <!-- If you need to modify files in external storage, request
             WRITE_EXTERNAL_STORAGE instead. -->
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                         android:maxSdkVersion="18" />
    </manifest>
    

验证外部存储是否可用

由于外部存储可能会不可用(比如,当用户将存储安装到另一台机器或移除了提供外部存储的 SD 卡时),因此在访问之前,您应该始终先确认相应的卷可用。您可以通过调用 getExternalStorageState() 来查询外部存储的状态。如果返回的状态为 MEDIA_MOUNTED,则可以读取和写入文件。如果返回的是 MEDIA_MOUNTED_READ_ONLY,则只能读取文件。

例如,以下方法可用于确定存储的可用性:

Kotlin

    /* Checks if external storage is available for read and write */
    fun isExternalStorageWritable(): Boolean {
        return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
    }

    /* Checks if external storage is available to at least read */
    fun isExternalStorageReadable(): Boolean {
         return Environment.getExternalStorageState() in
            setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
    }
    

Java

    /* Checks if external storage is available for read and write */
    public boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }
        return false;
    }

    /* Checks if external storage is available to at least read */
    public boolean isExternalStorageReadable() {
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }
        return false;
    }
    

保存到公开目录

如果要将文件保存到其他应用可以访问的外部存储上,请使用以下 API 之一:

  • 如果要保存照片、音频文件或视频剪辑,请使用 MediaStore API。
  • 如果要保存任何其他文件(如 PDF 文档),请使用 ACTION_CREATE_DOCUMENT intent,这是存储访问框架的一部分。

如果您不希望媒体扫描程序发现您的文件,请在特定于应用的目录中添加名为 .nomedia 的空文件(请注意文件名中的句点前缀)。这可以防止媒体扫描程序读取您的媒体文件并通过 MediaStore API 将它们提供给其他应用。

保存到私有目录

如果您想将应用专用文件保存在外部存储上,可以通过调用 getExternalFilesDir() 并传入指明您想要的目录类型的名称来获取特定于应用的目录。通过这种方式创建的每个目录都会被添加到一个父目录中,该目录包含了应用的所有外部存储文件,当用户卸载应用时,系统会清除这些文件。

以下代码段展示了如何为单个相册创建目录:

Kotlin

    fun getPrivateAlbumStorageDir(context: Context, albumName: String): File? {
        // Get the directory for the app's private pictures directory.
        val file = File(context.getExternalFilesDir(
                Environment.DIRECTORY_PICTURES), albumName)
        if (!file?.mkdirs()) {
            Log.e(LOG_TAG, "Directory not created")
        }
        return file
    }
    

Java

    public File getPrivateAlbumStorageDir(Context context, String albumName) {
        // Get the directory for the app's private pictures directory.
        File file = new File(context.getExternalFilesDir(
                Environment.DIRECTORY_PICTURES), albumName);
        if (!file.mkdirs()) {
            Log.e(LOG_TAG, "Directory not created");
        }
        return file;
    }
    

请务必使用由 DIRECTORY_PICTURES 这类 API 常量提供的目录名称。这些目录名称可确保系统正确地处理这些文件。例如,保存在 DIRECTORY_RINGTONES 中的文件会被系统媒体扫描程序归类为铃声,而不是音乐。

如果没有适合您文件的预定义子目录名称,您可以调用 getExternalFilesDir() 并传入 null。这会返回应用在外部存储上的专用目录的根目录。

在多个存储位置之间选择

有时,分配内部存储分区作为外部存储的设备也会提供 SD 卡插槽。这意味着该设备会有两个不同的外部存储目录,因此在将“私有”文件写入外部存储时,需要选择使用哪个目录。

从 Android 4.4(API 级别 19)开始,您可以通过调用 getExternalFilesDirs() 来访问这两个位置,这会返回一个 File 数组,其中包含了每个存储位置的条目。数组中的第一个条目被视为主要外部存储,除非该位置已满或不可用,否则应该一律使用该位置。

如果您的应用支持 Android 4.3 及更低版本,则应使用支持库的静态方法 ContextCompat.getExternalFilesDirs()。这始终会返回一个 File 数组,但如果设备搭载的是 Android 4.3 及更低版本,数组中将仅包含主要外部存储的条目。(如果有第二个存储位置,您将无法在 Android 4.3 及更低版本上访问它。)

唯一卷名称

面向 Android 10(API 级别 29)或更高版本的应用可以访问系统分配给每个外部存储设备的唯一名称。此命名系统可帮助您高效地整理内容并将内容编入索引,还可让您控制新内容的存储位置。

主要共享存储设备始终名为 VOLUME_EXTERNAL_PRIMARY。您可以通过调用 MediaStore.getExternalVolumeNames() 来发现其他卷。

要查询、插入、更新或删除特定卷,请将卷名称传入 MediaStore API 中提供的任何 getContentUri() 方法,如以下代码段中所示:

    // Assumes that the storage device of interest is the 2nd one
    // that your app recognizes.
    val volumeNames = MediaStore.getExternalVolumeNames(context)
    val selectedVolumeName = volumeNames[1]
    val collection = MediaStore.Audio.Media.getContentUri(selectedVolumeName)
    // ... Use a ContentResolver to add items to the returned media collection.
    

其他资源

要详细了解如何将文件保存到设备存储中,请参考以下资源。

Codelab