在许多情况下,您的应用会创建其他应用无需访问或不应访问的文件。系统提供以下位置来存储此类应用专属文件
内部存储目录:这些目录既包含用于存储持久性文件的专用位置,也包含用于存储缓存数据的另一个位置。系统会阻止其他应用访问这些位置,并且在 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());
}
注意: 在运行 Android 7.0(API 级别 24)或更高版本的设备上,除非您将 Context.MODE_PRIVATE 文件模式传递给 openFileOutput(),否则会发生 SecurityException。
要允许其他应用访问存储在内部存储此目录中的文件,请使用带有 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();
}
注意: 如果您需要在安装时将文件作为流访问,请将文件保存在项目的 /res/raw 目录中。您可以使用 openRawResource() 打开这些文件,将文件名前缀为 R.raw 作为资源 ID 传入。此方法返回一个 InputStream,您可以使用它读取文件。您无法写入原始文件。
查看文件列表
您可以通过调用 fileList() 来获取一个包含 filesDir 目录中所有文件名称的数组,如以下代码片段所示
Kotlin
var files: Array
Java
Array
创建嵌套目录
您还可以通过在 Kotlin 代码中调用 getDir(),或者在 Java 代码中将根目录和新目录名称传递给 File 构造函数来创建嵌套目录或打开内部目录
Kotlin
context.getDir(dirName, Context.MODE_PRIVATE)
Java
File directory = context.getFilesDir();
File file = new File(directory, filename);
注意: dataDir 始终是此新目录的祖先目录。
创建缓存文件
如果您只需要临时存储敏感数据,则应使用应用在内部存储空间中的指定缓存目录来保存数据。与所有应用专属存储空间一样,此目录中存储的文件在用户卸载您的应用时会被移除,尽管此目录中的文件可能更早被移除。
注意: 此缓存目录旨在存储少量应用的敏感数据。要确定您的应用目前有多少可用缓存空间,请调用 getCacheQuotaBytes()。
要创建缓存文件,请调用 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 可能会删除这些缓存文件以恢复空间。因此,在读取缓存文件之前,请检查它们是否存在。
移除缓存文件
尽管 Android 有时会自动删除缓存文件,但您不应依赖系统为您清理这些文件。您应始终在内部存储空间中维护应用的缓存文件。
要从内部存储空间中的缓存目录中移除文件,请使用以下方法之一
表示文件的 File 对象上的 delete() 方法
Kotlin
cacheFile.delete()
Java
cacheFile.delete();
应用上下文的 deleteFile() 方法,传入文件名
Kotlin
context.deleteFile(cacheFileName)
Java
context.deleteFile(cacheFileName);
从外部存储空间访问
如果内部存储空间不足以存储应用专属文件,请考虑改用外部存储空间。系统在外部存储空间中提供了目录,应用可以在其中组织仅在您的应用中对用户有价值的文件。一个目录用于应用的持久性文件,另一个目录包含应用的缓存文件。
在 Android 4.4(API 级别 19)或更高版本上,您的应用无需请求任何与存储相关的权限即可访问外部存储空间中的应用专属目录。存储在这些目录中的文件在您的应用卸载时会被移除。
注意: 这些目录中的文件不能保证始终可访问,例如当可移动 SD 卡从设备中取出时。如果您的应用功能依赖于这些文件,您应该改将文件存储在内部存储空间中。
在运行 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
ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]
Java
File[] externalStorageVolumes =
ContextCompat.getExternalFilesDirs(getApplicationContext(), null);
File primaryExternalStorage = externalStorageVolumes[0];
注意: 如果您的应用在运行 Android 4.3(API 级别 18)或更低版本的设备上使用,则数组只包含一个元素,表示主要外部存储卷。
访问持久性文件
要从外部存储访问应用专属文件,请调用 getExternalFilesDir()。
为帮助保持应用性能,请勿多次打开和关闭同一文件。
以下代码片段演示了如何调用 getExternalFilesDir()
Kotlin
val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)
Java
File appSpecificExternalDir = new File(context.getExternalFilesDir(null), filename);
注意: 在 Android 11(API 级别 30)及更高版本上,应用无法在外部存储上创建自己的应用专属目录。
创建缓存文件
要将应用专属文件添加到外部存储中的缓存中,请获取对 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
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);
}
注意: 在保存文件之前,您无需检查可用空间量。您可以直接尝试写入文件,然后捕获可能发生的 IOException。如果您不确定需要多少空间,可能需要这样做。例如,如果您在保存文件之前更改其编码(通过将 PNG 图像转换为 JPEG),您事先不知道文件的大小。
创建存储管理 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。
注意: ACTION_CLEAR_APP_CACHE intent 操作可能会显著影响设备电池续航时间,并可能从设备中移除大量文件。
更多资源
有关将文件保存到设备存储空间的更多信息,请查阅以下资源。
视频
准备分区存储(Android 开发者峰会 '19)