Android NDK开发详解应用数据和文件之访问应用专属文件
在很多情况下,您的应用会创建其他应用不需要访问或不应访问的文件。系统提供以下位置,用于存储此类应用专属文件:
内部存储空间目录:这些目录既包括用于存储持久性文件的专属位置,也包括用于存储缓存数据的其他位置。系统会阻止其他应用访问这些位置,并且在 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<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);
注意:filesDir 始终是此新目录的父级目录。
创建缓存文件
如果您只需要暂时存储敏感数据,应使用应用在内部存储空间中的指定缓存目录保存数据。与所有应用专属存储空间一样,当用户卸载应用后,系统会移除存储在此目录中的文件,但也可以更早地移除此目录中的文件。
注意:此缓存目录旨在存储应用的少量敏感数据。如需确定应用当前可用的缓存空间大小,请调用 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<out File> =
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;
}
请务必使用 DIRECTORY_PICTURES 等 API 常量提供的目录名称。这些目录名称可确保系统正确处理文件。如果没有适合您文件的预定义子目录名称,您可以改为将 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);
}
注意:您无需检查可用空间即可保存文件。您可以尝试立即写入文件,然后在出现 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 操作会严重影响设备的电池续航时间,并且可能会从设备上移除大量的文件。
其他资源
如需详细了解如何将文件保存到设备存储空间中,请参阅以下资源。
视频
为采用分区存储做好准备(2019 年 Android 开发者峰会)
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2023-10-18。