访问应用专用文件

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

  • 内部存储目录:这些目录包括一个用于存储持久性文件的专用位置,以及另一个用于存储缓存数据的另一个位置。系统阻止其他应用访问这些位置,并且在 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()以获取一个FileOutputStream,该文件写入filesDir目录中的文件。

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

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());
}

允许其他应用访问内部存储中此目录中存储的文件,请将FileProviderFLAG_GRANT_READ_URI_PERMISSION属性一起使用。

使用流访问文件

要将文件读取为流,请使用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);
}

创建存储管理活动

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

请求用户删除某些设备文件

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

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

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

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

其他资源

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

视频