Android数据和文件存储
Android提供几种选择来保存数据
- App-specific storage 存储只有 app 内可用的数据,可以存储在内部也可以存储在外部。使用内部存储的目录来保存其他应用程序不应该访问的敏感信息。
- Shared storage 存储 app 中想要去共享给其他 app 的文件。包括 Media、documents、files
- Preferences 用key-value来存储私有数据和原始数据
- Database 用 Room 持久化库来将结构化的数据存储在私有的数据库中
Type of content | Access method | Permissons needed | Can other apps access? | Files removed on app uninstall? | |
---|---|---|---|---|---|
App-specific storage | 只app使用数据 | 内部存储 getFilesDir() getCacheDir() 外部存储 getExternalFilesDir() getExternalCacheDir() | 无需权限 | No | Yes |
Media | 共享的媒体文件(images,audio files, videos) | MediaStore API | Android 11 (API 30) 及以上 READ_EXTERNAL_STORAGE Android 10(API 29) 及以下 READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE | Yes,但需要权限 READ_EXTERNAL_STORAGE | No |
Documents and other files | 其他类型的共享文件 | Storage Access Framework | 无需权限 | Yes | No |
App preferences | Key-value | Jetpack Preferences library | 无需权限 | No | Yes |
Database | 结构化数据 | Room persistence library | 无需权限 | No | Yes |
注意:可以保存文件的确切位置可能因设备而异。因此,不要使用硬编码的文件路径。
访问应用专属文件
从内部存储空间访问
访问持久性文件
应用内的普遍持久性文件存在于上下文对象的 filesDir 属性中。
访问和存储文件
可以使用 File 来访问和存储文件
val file = File(context.filesDir, filename)
注意:为确保应用的性能,请勿多次打开和关闭同一文件。
使用信息流存储文件
除了使用 File 之外,还可以调用 openFileOutput() 获取会写入 filesDir 目录中的文件的 FileOutputStream。
val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(fileContents.toByteArray())
}
注意:在搭载 Android 7.0(API 级别 24)或更高版本的设备上,必须将 Context.MODE_PRIVATE 文件模式传递到 openFileOutput(),否则会产生SecurityException
使用信息流访问文件
context.openFileInput(filename).bufferedReader().useLines { lines ->
lines.fold("") { some, text ->
"$some\n$text"
}
}
注意:如果在安装时需要以信息流的形式访问文件,请将文件保存在项目的 /res/raw 目录中。可以使用 openRawResource() 打开这些文件,传入带有 R.raw 前缀的文件名作为资源 ID。此方法将返回一个 InputStream,可以使用它读取文件,但无法写入原始文件。
创建缓存文件
如果需要暂时存储敏感数据,应该使用应用在内部存储空间的指定的缓存目录来保存数据。可以在使用之前调用 getCacheQuotaBytes() 先确定好当前可用的缓存空间大小。之后调用 File.createTempFile() 来创建文件。
File.createTempFile(filename, null, context.cacheDir)
val cacheFile = File(context.cacheDir, filename)
注意:当设备的内部存储空间不足时,Android 可能会删除这些缓存文件以回收空间。因此,请在读取前检查缓存文件是否存在。
移除缓存文件
即使 Android 会自行删除缓存,也不应该存粹依赖系统清理文件。应该主动维护存储空间中的缓存文件。应选用以下方法之一从内部存储空间的缓存目录中移除缓存文件。
// 对代表该文件的 File 对象使用 delete() 方法
cacheFile.delete()
// 应用上下文的 deleteFile() 方法,并传入文件名
context.deleteFile(cacheFileName)
从外部存储空间访问
除了内部存储空间,还可以选用外部存储空间来整理和存放应用对用户有价值的文件。系统在外部存储空间提供两个目录,一个是为应用对持久性文件设计的,一个是包含应用的缓存文件。
在 Android 4.4(API 级别 19)或更高版本中,应用无需请求任何与存储空间相关的权限即可访问外部存储空间中的应用专属目录。卸载应用后,系统会移除这些目录中存储的文件。
在搭载 Android 9(API 级别 28)或更低版本的设备上,只要其他应用具有相应的存储权限,任何应用都可以访问外部存储空间中的应用专属文件。
以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储)。启用分区存储后,应用将无法访问属于其他应用的应用专属目录。
验证存储空间的可用性
由于外部存储空间可能位于可以移除的移动物理卷上,因此在从外部存储空间读取应用专属数据或者将专属数据写入外部存储空间之间,要先验证卷是否可用。
// 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)
}
选择物理存储位置
有的时候,设备具有多个可能包含外部存储空间的物理卷,因此需要选择用于应用专属存储空间的物理卷。调用 ContextCompat.getExternalFilesDirs()。返回数组中的第一个元素被视为主外部存储卷。
val externalStorageVolumes: Array<out File> =
ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]
访问持久性文件
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)
创建缓存文件
val externalCacheFile = File(context.externalCacheDir, filename)
移除缓存文件
externalCacheFile.delete()
选择媒体内容
如果应用支持仅在该应用内对用户有价值的媒体文件,最好将这些文件存储在外部存储空间的应用专属目录中。
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
}
字段 | 描述 |
---|---|
DIRECTORY_ALARMS | 用户可以选择的警报列表 |
DIRECTORY_AUDIOBOOKS | 用户可以选择的有声读物列表 |
DIRECTORY_DCIM | 相机照片和视频的存储位置 |
DIRECTORY_DOCUMENTS | 用户创建的文档位置 |
DIRECTORY_DOWNLOADS | 用户下载文件的存放位置 |
DIRECTORY_MOVIES | 用户放置电影/视频的位置 |
DIRECTORY_MUSIC | 用户放置音乐的位置 |
DIRECTORY_NOTIFICATIONS | 用户可以选择的通知列表 |
DIRECTORY_PICTURES | 用户存放图片的位置 |
DIRECTORY_PODCASTS | 用户可以选择的播客列表 |
DIRECTORY_RECORDINGS | 用户可以选择的应用程序录制的录音列表 |
DIRECTORY_RINGTONES | 用户可以选择的铃声列表 |
DIRECTORY_SCREENSHOTS | 用户存放的屏幕截图的目录 |
查询可用空间
如果事先知道要存储的数据量,可以通过调用 getAllocatableBytes() 查处设备可以提供多少空间。
注意:getAllocatableBytes() 的返回值可能大于设备上的当前可用空间,因为系统已经识别出可以从其他应用的缓存目录中移除的文件。
// 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.
}
如果有足够的空间保存您的应用数据,请调用 allocateBytes() 声明空间。否则,应用可以调用包含 ACTION_MANAGE_STORAGE 操作的 intent。此 intent 会向用户显示提示,要求他们在设备上选择要移除的文件,以便您的应用拥有所需空间。
访问共享存储空间中的媒体文件
为了提供更丰富的用户体验,许多应用允许用户提供和访问位于外部存储卷上的媒体。框架提供经过优化的媒体集合索引,称为媒体库,可以更轻松地检索和更新这些媒体文件。
与媒体库抽象互动
使用 ContentResolver 对象
val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause
applicationContext.contentResolver.query(
MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)?.use { cursor ->
while (cursor.moveToNext()) {
// Use an ID column from the projection to get
// a URI representing the media item itself.
}
}
- 图片(包括照片和屏幕截图),存储在 DCIM/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Images 表格中。
- 视频,存储在 DCIM/、Movies/ 和 Pictures/ 目录中。系统将这些文件添加到 MediaStore.Video 表格中。
- 音频文件,存储在 Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/ 目录中,以及位于 Music/ 或 Movies/ 目录中的音频播放列表中。系统将这些文件添加到 MediaStore.Audio 表格中。
- 下载的文件,存储在 Download/ 目录中。在搭载 Android 10(API 级别 29)及更高版本的设备上,这些文件存储在 MediaStore.Downloads 表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。
- 媒体库还包含一个名为 MediaStore.Files 的集合。如果启用了分区存储,集合只会显示您的应用创建的照片、视频和音频文件。如果分区存储不可用或未使用,集合将显示所有类型。
请求权限
存储权限
已启用分区存储
如果您的应用使用分区存储,则应仅针对搭载 Android 9(API 级别 28)或更低版本的设备请求存储相关权限。您可以通过将 android:maxSdkVersion 属性添加到应用清单文件中的权限声明来应用此条件。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
注意:请勿为搭载 Android 10 或更高版本的设备不必要地请求存储相关权限。
媒体位置权限
如果应用使用分区存储,您需要在应用的清单中声明 ACCESS_MEDIA_LOCATION 权限,然后在运行时请求此权限,应用才能从照片中检索未编辑的 Exif 元数据。
查询媒体集合
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.
// Container for information about each video.
data class Video(val uri: Uri,
val name: String,
val duration: Int,
val size: Int
)
val videoList = mutableListOf<Video>()
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
val query = ContentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
while (cursor.moveToNext()) {
// Get values of columns for a given video.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList += Video(contentUri, name, duration, size)
}
}
加载文件缩略图
如果应用显示多个媒体文件,并请求用户选择其中一个文件,加载文件的预览版本(或缩略图)会比加载文件本身效率更高。
// Load thumbnail of a specific media item.
val thumbnail: Bitmap =
applicationContext.contentResolver.loadThumbnail(
content-uri, Size(640, 480), null)
打开媒体文件
文件描述符
// Open a specific media item using ParcelFileDescriptor.
val resolver = applicationContext.contentResolver
// "rw" for read-and-write;
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
resolver.openFileDescriptor(content-uri, readOnlyMode).use { pfd ->
// Perform operations on "pfd".
}
文件流
// Open a specific media item using InputStream.
val resolver = applicationContext.contentResolver
resolver.openInputStream(content-uri).use { stream ->
// Perform operations on "stream".
}
存储卷
以 Android 10 或更高版本为目标平台的应用可以访问系统为每个外部存储卷分配的唯一名称。主要共享存储卷始终名为 VOLUME_EXTERNAL_PRIMARY。可以通过调用 MediaStore.getExternalVolumeNames() 查看其他存储卷
val volumeNames: Set<String> = MediaStore.getExternalVolumeNames(context)
val firstVolumeName = volumeNames.iterator().next()
照片中位置信息
- 在应用的清单中请求 ACCESS_MEDIA_LOCATION 权限。
- 通过调用 setRequireOriginal(),从 MediaStore 对象获取照片的确切字节,并传入照片的 URI。
val photoUri: Uri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex)
)
// Get location data using the Exifinterface library.
// Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
photoUri = MediaStore.setRequireOriginal(photoUri)
contentResolver.openInputStream(photoUri)?.use { stream ->
ExifInterface(stream).run {
// If lat/long is null, fall back to the coordinates (0, 0).
val latLong = latLong ?: doubleArrayOf(0.0, 0.0)
}
}
注意:由于此位置信息属于敏感信息,如果应用使用了分区存储,默认情况下 Android 10 会对应用隐藏此信息。
添加媒体项目
// Add a specific media item.
val resolver = applicationContext.contentResolver
// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY)
// Publish a new song.
val newSongDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}
// Keeps a handle to the new song's URI in case we need to modify it
// later.
val myFavoriteSongUri = resolver
.insert(audioCollection, newSongDetails)
更新媒体项目
// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver
// When performing a single item update, prefer using the ID
val selection = "${MediaStore.Audio.Media._ID} = ?"
// By using selection + args we protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())
// Update an existing song.
val updatedSongDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}
// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
myFavoriteSongUri,
updatedSongDetails,
selection,
selectionArgs)
移除媒体项目
// Remove a specific media item.
val resolver = applicationContext.contentResolver
// URI of the image to remove.
val imageUri = "..."
// WHERE clause.
val selection = "..."
val selectionArgs = "..."
// Perform the actual removal.
val numImagesRemoved = resolver.delete(
imageUri,
selection,
selectionArgs)
android开发者指南:https://developer.android.google.cn