访问应用专属文件

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

  • 内部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。系统会阻止其他应用访问这些位置,并且在 Android 10(API 级别 29)及更高版本中,系统会对这些位置进行加密。这些特征使得这些位置非常适合存储只有应用本身才能访问的敏感数据。

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

如果用户卸载应用,系统会移除保存在应用专属存储空间中的文件。由于这一行为,您不应使用此存储空间保存用户希望独立于应用而保留的任何内容。例如,如果应用允许用户拍摄照片,用户会希望即使卸载应用后仍可访问这些照片。因此,您应改为使用共享存储空间将此类文件保存到适当的媒体集合中。

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

从内部存储空间访问

对于每个应用,系统都会在内部存储空间中提供目录,应用可以在该存储空间中整理其文件。一个目录专为应用的持久性文件而设计,而另一个目录包含应用的缓存文件。您的应用不需要任何系统权限即可读取和写入这些目录中的文件。

其他应用无法访问存储在内部存储空间中的文件。这使得内部存储空间非常适合存储其他应用不应访问的应用数据。

但是,请注意,这些目录的空间通常比较小。在将应用专属文件写入内部存储空间之前,应用应查询设备上的可用空间

访问持久性文件

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

访问和存储文件

您可以使用 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() == Environment.MEDIA_MOUNTED;
}

// Checks if a volume containing external storage is available to at least read.
private boolean isExternalStorageReadable() {
     return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED ||
            Environment.getExternalStorageState() == 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(),如以下代码段所示:

Kotlin

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

Java

File appSpecificExternalDir = new File(context.getExternalFilesDir(), 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;
}

请务必使用 DIRECTORY_PICTURES 等 API 常量提供的目录名称。这些目录名称可确保系统正确处理文件。如果没有适合您文件的预定义子目录名称,您可以改为将 null 传递到 getExternalFilesDir()。这将返回外部存储空间中的应用专属根目录。

查询可用空间

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

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

如果有足够的空间保存您的应用数据,请调用 allocateBytes() 声明空间。否则,应用可以调用包含 ACTION_MANAGE_STORAGE 操作的 intent。此 intent 会向用户显示提示,要求他们在设备上选择要移除的文件,以便您的应用拥有所需空间。如果需要,此提示可以显示设备上的可用空间量。如需显示此人性化信息,请使用以下计算结果:

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

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

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 {
        action = ACTION_MANAGE_STORAGE
    }
    // Display prompt to user, requesting that they choose files to remove.
}

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 {
    Intent storageIntent = new Intent();
    storageIntent.setAction(ACTION_MANAGE_STORAGE);
    // Display prompt to user, requesting that they choose files to remove.
}

其他资源

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

视频