访问应用专用文件

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

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

允许其他应用访问存储在此内部存储目录中的文件,请使用FileProvider 以及FLAG_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。

其他资源

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

视频