访问应用专属文件

在许多情况下,您的应用会创建其他应用无需访问或不应访问的文件。系统提供以下位置来存储此类应用专属文件

  • 内部存储目录:这些目录既包含用于存储持久性文件的专用位置,也包含用于存储缓存数据的另一个位置。系统会阻止其他应用访问这些位置,并且在 Android 10(API 级别 29)及更高版本上,这些位置是加密的。这些特性使这些位置成为存储只有您的应用本身才能访问的敏感数据的好地方。

  • 外部存储目录:这些目录既包含用于存储持久性文件的专用位置,也包含用于存储缓存数据的另一个位置。虽然其他应用如果具有适当的权限可以访问这些目录,但存储在这些目录中的文件仅供您的应用使用。如果您明确打算创建其他应用应能访问的文件,您的应用应将这些文件存储在外部存储空间的共享存储空间部分。

当用户卸载您的应用时,应用专属存储空间中保存的文件将被移除。由于这种行为,您不应使用此存储空间保存任何用户期望独立于您的应用而持久存在的内容。例如,如果您的应用允许用户拍摄照片,用户会期望即使在卸载应用后,他们也能访问这些照片。因此,您应该改用共享存储空间将这些类型的文件保存到相应的媒体收藏夹

以下各部分介绍了如何在应用专属目录中存储和访问文件。

从内部存储空间访问

对于每个应用,系统都会在内部存储空间中提供目录,应用可以在其中组织其文件。一个目录用于存储应用的持久性文件,另一个目录包含应用的缓存文件。您的应用不需要任何系统权限即可读写这些目录中的文件。

其他应用无法访问内部存储空间中存储的文件。这使得内部存储空间成为存储其他应用不应访问的应用数据的好地方。

然而,请记住,这些目录通常很小。在将应用专属文件写入内部存储空间之前,您的应用应查询设备上的可用空间

访问持久性文件

您的应用通常的持久性文件位于一个目录中,您可以使用上下文对象的 filesDir 属性访问该目录。该框架提供了几种方法来帮助您访问和存储此目录中的文件。

访问和存储文件

您可以使用 File API 访问和存储文件。

为帮助保持应用性能,请勿多次打开和关闭同一文件。

以下代码片段演示了如何使用 File API

Kotlin

val file = File(context.filesDir, filename)

Java

File file = new File(context.getFilesDir(), filename);

使用流存储文件

作为使用 File API 的替代方案,您可以调用 openFileOutput() 以获取一个写入 filesDir 目录中文件的 FileOutputStream

以下代码片段演示了如何将一些文本写入文件

Kotlin

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

Java

String filename = "myfile";
String fileContents = "Hello world!";
try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) {
    fos.write(fileContents.toByteArray());
}

允许其他应用访问存储在内部存储此目录中的文件,请使用带有 FLAG_GRANT_READ_URI_PERMISSION 属性的 FileProvider

使用流访问文件

要将文件作为流读取,请使用 openFileInput()

Kotlin

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

Java

FileInputStream fis = context.openFileInput(filename);
InputStreamReader inputStreamReader =
        new InputStreamReader(fis, StandardCharsets.UTF_8);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
    String line = reader.readLine();
    while (line != null) {
        stringBuilder.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Error occurred when opening raw file for reading.
} finally {
    String contents = stringBuilder.toString();
}

查看文件列表

您可以通过调用 fileList() 来获取一个包含 filesDir 目录中所有文件名称的数组,如以下代码片段所示

Kotlin

var files: Array<String> = context.fileList()

Java

Array<String> files = context.fileList();

创建嵌套目录

您还可以通过在 Kotlin 代码中调用 getDir(),或者在 Java 代码中将根目录和新目录名称传递给 File 构造函数来创建嵌套目录或打开内部目录

Kotlin

context.getDir(dirName, Context.MODE_PRIVATE)

Java

File directory = context.getFilesDir();
File file = new File(directory, filename);

创建缓存文件

如果您只需要临时存储敏感数据,则应使用应用在内部存储空间中的指定缓存目录来保存数据。与所有应用专属存储空间一样,此目录中存储的文件在用户卸载您的应用时会被移除,尽管此目录中的文件可能更早被移除

要创建缓存文件,请调用 File.createTempFile()

Kotlin

File.createTempFile(filename, null, context.cacheDir)

Java

File.createTempFile(filename, null, context.getCacheDir());

您的应用使用上下文对象的 cacheDir 属性和 File API 访问此目录中的文件

Kotlin

val cacheFile = File(context.cacheDir, filename)

Java

File cacheFile = new File(context.getCacheDir(), filename);

移除缓存文件

尽管 Android 有时会自动删除缓存文件,但您不应依赖系统为您清理这些文件。您应始终在内部存储空间中维护应用的缓存文件。

要从内部存储空间中的缓存目录中移除文件,请使用以下方法之一

  • 表示文件的 File 对象上的 delete() 方法

    Kotlin

    cacheFile.delete()

    Java

    cacheFile.delete();
  • 应用上下文的 deleteFile() 方法,传入文件名

    Kotlin

    context.deleteFile(cacheFileName)

    Java

    context.deleteFile(cacheFileName);

从外部存储空间访问

如果内部存储空间不足以存储应用专属文件,请考虑改用外部存储空间。系统在外部存储空间中提供了目录,应用可以在其中组织仅在您的应用中对用户有价值的文件。一个目录用于应用的持久性文件,另一个目录包含应用的缓存文件

在 Android 4.4(API 级别 19)或更高版本上,您的应用无需请求任何与存储相关的权限即可访问外部存储空间中的应用专属目录。存储在这些目录中的文件在您的应用卸载时会被移除。

在运行 Android 9(API 级别 28)或更低版本的设备上,只要您的应用具有适当的存储权限,它就可以访问属于其他应用的应用专属文件。为了让用户更好地控制其文件并限制文件混乱,默认情况下,以 Android 10(API 级别 29)及更高版本为目标的应用被授予对外部存储空间(即分区存储)的范围访问权限。启用分区存储后,应用无法访问属于其他应用的应用专属目录。

验证存储空间是否可用

由于外部存储位于用户可能可移除的物理卷上,因此在尝试从外部存储读取或向外部存储写入应用专属数据之前,请验证该卷是否可访问。

您可以通过调用 Environment.getExternalStorageState() 查询卷的状态。如果返回的状态是 MEDIA_MOUNTED,则您可以在外部存储空间中读写应用专属文件。如果状态是 MEDIA_MOUNTED_READ_ONLY,则只能读取这些文件。

例如,以下方法有助于确定存储可用性

Kotlin

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

// Checks if a volume containing 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 a volume containing external storage is available
// for read and write.
private boolean isExternalStorageWritable() {
    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}

// Checks if a volume containing external storage is available to at least read.
private boolean isExternalStorageReadable() {
     return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ||
            Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED_READ_ONLY);
}

在没有可移除外部存储的设备上,使用以下命令启用虚拟卷以测试您的外部存储可用性逻辑

adb shell sm set-virtual-disk true

选择物理存储位置

有时,将内部内存的一个分区分配为外部存储的设备也提供 SD 卡槽。这意味着设备有多个可能包含外部存储的物理卷,因此您需要选择要用于应用专属存储的卷。

要访问不同的位置,请调用 ContextCompat.getExternalFilesDirs()。如代码片段所示,返回数组中的第一个元素被认为是主要外部存储卷。除非该卷已满或不可用,否则请使用此卷。

Kotlin

val externalStorageVolumes: Array<out File> =
        ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]

Java

File[] externalStorageVolumes =
        ContextCompat.getExternalFilesDirs(getApplicationContext(), null);
File primaryExternalStorage = externalStorageVolumes[0];

访问持久性文件

要从外部存储访问应用专属文件,请调用 getExternalFilesDir()

为帮助保持应用性能,请勿多次打开和关闭同一文件。

以下代码片段演示了如何调用 getExternalFilesDir()

Kotlin

val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)

Java

File appSpecificExternalDir = new File(context.getExternalFilesDir(null), filename);

创建缓存文件

要将应用专属文件添加到外部存储中的缓存中,请获取对 externalCacheDir 的引用

Kotlin

val externalCacheFile = File(context.externalCacheDir, filename)

Java

File externalCacheFile = new File(context.getExternalCacheDir(), filename);

移除缓存文件

要从外部缓存目录中移除文件,请使用表示该文件的 File 对象上的 delete() 方法

Kotlin

externalCacheFile.delete()

Java

externalCacheFile.delete();

媒体内容

如果您的应用处理的媒体文件仅在您的应用中对用户有价值,最好将它们存储在外部存储中的应用专属目录中,如以下代码片段所示

Kotlin

fun getAppSpecificAlbumStorageDir(context: Context, albumName: String): File? {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    val file = File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName)
    if (!file?.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

Java

@Nullable
File getAppSpecificAlbumStorageDir(Context context, String albumName) {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (file == null || !file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

重要的是要使用由 API 常量提供的目录名称,例如 DIRECTORY_PICTURES。这些目录名称可确保文件被系统正确处理。如果预定义的子目录名称都不适合您的文件,您可以改为将 null 传递给 getExternalFilesDir()。这将返回外部存储中的根应用专属目录。

查询可用空间

许多用户的设备上没有太多可用存储空间,因此您的应用应谨慎地使用空间。

如果您提前知道要存储多少数据,可以通过调用 getAllocatableBytes() 来了解设备可以为您的应用提供多少空间。getAllocatableBytes() 的返回值可能大于设备上当前可用空间量。这是因为系统已识别出可以从其他应用的缓存目录中删除的文件。

如果有足够的空间保存您的应用数据,请调用 allocateBytes()。否则,您的应用可以请求用户从设备中移除一些文件或从设备中移除所有缓存文件

以下代码片段显示了您的应用如何查询设备上可用空间的示例

Kotlin

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        // To request that the user remove all app cache files instead, set
        // "action" to ACTION_CLEAR_APP_CACHE.
        action = ACTION_MANAGE_STORAGE
    }
}

Java

// App needs 10 MB within internal storage.
private static final long NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

StorageManager storageManager =
        getApplicationContext().getSystemService(StorageManager.class);
UUID appSpecificInternalDirUuid = storageManager.getUuidForPath(getFilesDir());
long availableBytes =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid);
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
            appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP);
} else {
    // To request that the user remove all app cache files instead, set
    // "action" to ACTION_CLEAR_APP_CACHE.
    Intent storageIntent = new Intent();
    storageIntent.setAction(ACTION_MANAGE_STORAGE);
}

创建存储管理 activity

您的应用可以声明和创建一个自定义 activity,当启动时,该 activity 允许用户管理您的应用存储在用户设备上的数据。您可以使用清单文件中的 android:manageSpaceActivity 属性声明此自定义“管理空间” activity。文件管理器应用可以调用此 activity,即使您的应用未导出该 activity;也就是说,当您的 activity 将 android:exported 设置为 false 时。

请求用户移除部分设备文件

要请求用户选择设备上要移除的文件,请调用包含 ACTION_MANAGE_STORAGE 操作的 intent。此 intent 会向用户显示提示。如果需要,此提示可以显示设备上可用的空闲空间量。要显示此用户友好的信息,请使用以下计算结果

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

请求用户移除所有缓存文件

另外,您可以请求用户清除设备上所有应用的缓存文件。为此,请调用包含 ACTION_CLEAR_APP_CACHE intent 操作的 intent。

更多资源

有关将文件保存到设备存储空间的更多信息,请查阅以下资源。

视频