在Android 10中引入了分区储存功能,在外部存储设备中为每个应用提供了一个“隔离存储沙盒”。其他应用无法直接访问应用的沙盒文件。由于文件是应用的私有文件,不再需要任何权限即可访问和保存自己的文件。此变更并有助于减少应用所需的权限数量,同时保证用户文件的隐私性。
READ_EXTERNAL_STORAGE
和 WRITE_EXTERNAL_STORAGE
权限。android:requestLegacyExternalStorage="true"
,暂时停用分区存储。继续按上面的方式读写文件。Android 10仍然使用READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAGE
作为存储相关运行时权限,但现在即使获取了这些权限,访问外部存储也受到了限制,只能访问自身目录下的文件和公共内体文件。
无权限 | READ_EXTERNAL | |
---|---|---|
Audio | 可读写APP自己创建的文件,但不可直接使用路径访问 | 可以读其他APP创建的媒体类文件,删改操作需要用户授权 |
Image | 可读写APP自己创建的文件,但不可直接使用路径访问 | 可以读其他APP创建的媒体类文件,删改操作需要用户授权 |
File | 可读写APP自己创建的文件,但不可直接使用路径访问 | 不可读写其他APP创建的非媒体类文件 |
Downloads | 可读写APP自己创建的文件,但不可直接使用路径访问 | 不可读写其他APP创建的非媒体类文件 |
手机内存又有内部存储和外部存储,在以前手机容量不大时,会有外接SD卡,外接的SD卡就是外部存储。如今大部分手机不支持外接SD卡,手机内部就已经自带了外部存储,但不排除少量特殊机型。所以在使用外部存储前应该检查外部存储是否存在:
/**
* 检查外部存储是否可读写
*/
fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}
/**
* 检查外部存储是否至少可读取
*/
fun isExternalStorageReadable(): Boolean {
return Environment.getExternalStorageState() in
setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}
应用无需请求任何与存储空间相关的权限即可访问应用专属目录。卸载应用后,系统会移除这些目录中存储的文件。而该目录下又会有两个子目录,一个目录专为应用的持久性文件而设计,而另一个目录包含应用的缓存文件。
一般用来放一些长时间保存的数据。
a.内部存储
通过context.filesDir
方法可以获取到 /data/data/【应用包名】/files 文件路径
您可以使用 File API 访问和存储文件:
val file = File(context.filesDir, filename)
//or
val file = context.getDir(dirName, Context.MODE_PRIVATE)
除使用 File API 之外,您还可以调用 openFileOutput()
获取会写入 filesDir 目录中的文件的 FileOutputStream
。
val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(fileContents.toByteArray())
}
如需以信息流的形式读取文件,请使用 openFileInput()
context.openFileInput(filename).bufferedReader().useLines {
lines ->
lines.fold("") {
some, text ->
"$some\n$text"
}
}
b.外部存储
通过context.getExternalFilesDir
方法可以获取到 sdcard/Android/data/【应用包名】/files 文件路径
val file = File(context.getExternalFilesDir(), filename)
暂时存储敏感数据,应使用应用在内部存储空间中的指定缓存目录保存数据。当设备的内部存储空间不足时,Android 可能会删除这些缓存文件以回收空间。
a.内部存储
通过context.cacheDir
方法可以获取到 sdcard/Android/data/【应用包名】/cache文件路径
val cacheFile = File(context.cacheDir, filename)
//or
val cacheFile = File.createTempFile(filename, null, context.cacheDir)
b.外部存储
通过context.externalCacheDir
方法可以获取到 sdcard/Android/data/【应用包名】/cache 文件路径
val file = File(context.getExternalFilesDir(), filename)
如果用户数据可供或应可供其他应用访问,并且即使在用户卸载应用后也可对其进行保存,请使用共享存储空间。Android 10更改了应用对设备外部存储设备中的文件(如:/sdcard )的访问方式。继续使用 READ_EXTERNAL_STORAGE
和 WRITE_EXTERNAL_STORAGE
权限,只不过当拥有这些权限的时候,你只能访问媒体文件,无法访问其他文件。
Android 提供用于存储和访问以下类型的可共享数据的 API:
Android 10中使用ContentResolver
进行文件的增删改查
如需查找满足一组特定条件(例如时长为 5 分钟或更长时间)的视频文件,请使用类似于以下代码段中所示的类似 SQL 的选择语句:
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
)
// sql占位符与占位符变量
val selection = "${
MediaStore.Video.Media.DURATION} >= ?"
// 占位符变量的值
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// sql排序语句
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)
}
}
上述代码中,参数projection
、selection
、selectionArgs
、sortOrder
都有对应的说明,如果没有特殊要求,可以为null
,对于media-type
系统会自动扫描外部存储,并将媒体文件添加到以下明确定义的集合中:
MediaStore.Images
表格中。MediaStore.Video
表格中。MediaStore.Audio
表格中。MediaStore.Downloads
表格中。此表格在 Android 9(API 级别 28)及更低版本中不可用。媒体库还包含一个名为 MediaStore.Files
的集合。其内容取决于您的应用是否使用分区存储(适用于以 Android 10 或更高版本为目标平台的应用):
在应用中执行此类查询时,请注意以下几点:
getColumnIndexOrThrow()
。用于打开媒体文件的具体逻辑取决于媒体内容最佳表示形式是文件描述符还是文件流:
文件描述符
如果需要获取文件描述信息,使用文件描述符打开媒体文件
// "rw" for read-and-write;
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
applicationContext.contentResolver.openFileDescriptor(uri, readOnlyMode).use {
// Perform operations on "ParcelFileDescriptor".
}
文件流
如需使用文件流打开媒体文件,请使用类似于以下代码段所示的逻辑:
applicationContext.contentResolver.openInputStream(uri).use {
stream ->
// Perform operations on "stream".
}
以保存图片到DCIM为例
fun saveImage(bitmap:Bitmap){
val details = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "image.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/JPEG")
//兼容Android Q和以下版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path)
}
}
val uri = applicationContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, details)
uri?.let {
contentResolver.openOutputStream(it)?.use {
outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.close()
}
}
}
上述代码主要分为三个步骤:
ContentValues
对象,然后向这个对象中添加三个重要的数据。DISPLAY_NAME
:图片显示的名称。MIME_TYPE
,:图片的mime类型。RELATIVE_PATH/DATA
:图片存储的路径,这个值在Android 10和之前的系统版本中的处理方式不一样。Android 10中新增了一个RELATIVE_PATH
常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM
、DIRECTORY_PICTURES
、DIRECTORY_MOVIES
、DIRECTORY_MUSIC
等,分别表示相册、图片、电影、音乐等目录。而在之前的系统版本中并没有RELATIVE_PATH
,所以我们要使用DATA
常量(已在Android 10中废弃),并拼装出一个文件存储的绝对路径才行。insert()
方法即可获得插入图片的Uri。openOutputStream()
方法获得文件的输出流,然后将Bitmap对象写入到该输出流当中即可。如果您的应用执行可能非常耗时的操作,那么在处理文件时对其进行独占访问非常有用。在搭载 Android 10 或更高版本的设备上,您的应用可以通过将 IS_PENDING
标记的值设为 1 来获取此独占访问权限。如此一来,只有您的应用可以查看该文件,直到您的应用将 IS_PENDING
的值改回 0。上面的代码就可以改成:
fun saveImage(bitmap:Bitmap){
val details = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, "image.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/JPEG")
//兼容Android Q和以下版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
//标志媒体文件的待处理状态
put(MediaStore.Audio.Media.IS_PENDING, 1)
} else {
put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).path)
}
}
val uri = applicationContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, details)
uri?.let {
contentResolver.openOutputStream(it)?.use {
outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.close()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 写入完成后,清除媒体文件的待处理状态
details.clear()
details.put(MediaStore.Audio.Media.IS_PENDING, 0)
applicationContext.contentResolver.update(uri, details, null, null)
}
}
如需更新应用拥有的媒体文件,请运行类似于以下内容的代码:
val details = ContentValues().apply {
//修改文件名字
put(MediaStore.Audio.Media.DISPLAY_NAME, "image2.jpg")
//移动文件
//兼容Android Q和以下版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
} else {
put(MediaStore.Images.Media.DATA,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).path)
}
}
applicationContext.contentResolver.update(uri, details, null, null)
如果您的应用使用分区存储,它通常无法更新其他应用存放到媒体库中的媒体文件。
不过,您仍可通过捕获平台抛出的 RecoverableSecurityException
来征得用户同意修改文件。然后,您可以请求用户授予您的应用对此特定内容的写入权限,如以下代码段所示:
try {
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use {
//修改媒体文件
}
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException =
securityException as? RecoverableSecurityException ?: throw RuntimeException(
securityException.message,
securityException)
val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(intentSender, uri, null, 0, 0, 0, null)
}
} else {
throw RuntimeException(securityException.message, securityException)
}
}
// URI of the image to remove.
val imageUri = "..."
// WHERE clause.
val selection = "..."
val selectionArgs = "..."
// Perform the actual removal.
val numImagesRemoved = applicationContext.contentResolver.delete(
imageUri,
selection,
selectionArgs)
如果分区存储不可用或未启用,您可以使用上述代码段移除其他应用拥有的文件。但是,如果启用了分区存储,您就需要为应用要移除的每个文件捕获 RecoverableSecurityException
,如修改媒体文件所述。
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。借助 SAF,用户可轻松浏览和打开各种文档、图片及其他文件,而不用管这些文件来自其首选文档存储提供程序中的哪一个。用户可通过易用的标准界面,跨所有应用和提供程序以统一的方式浏览文件并访问最近用过的文件。
使用 ACTION_OPEN_DOCUMENT
intent 操作,打开文件选择器来选择需要打开的文件
fun openFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
//指定文件选择器在首次加载时应显示的目录的 URI(可选)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, 1)
}
我们还有使用更为简便的方式,具体的内容可参考:Android onActivityResult的替代方法—registerForActivityResult
registerForActivityResult(ActivityResultContracts.GetContent()){
}.launch("application/pdf")
使用 ACTION_CREATE_DOCUMENT
intent 操作,加载系统文件选择器,支持用户选择要写入文件内容的位置。此流程类似于其他操作系统使用的“另存为”对话框中使用的流程。ACTION_CREATE_DOCUMENT
无法覆盖现有文件。如果您的应用尝试保存同名文件,系统会在文件名的末尾附加一个数字并将其包含在一对括号中。
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
activityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
}
}
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, "invoice.pdf")
//指定文件选择器在首次加载时应显示的目录的 URI(可选)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
activityResultLauncher.launch(intent)
}
使用 ACTION_OPEN_DOCUMENT_TREE
intent 操作,它支持用户授予应用对整个目录树的访问权限。然后,您的应用便可以访问所选目录及其任何子目录中的任何文件。
fun openDirectory(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
//授予读操作
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
//授予写操作
flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
//指定文件选择器在首次加载时应显示的目录的 URI(可选)
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
activityResultLauncher.launch(intent)
}
当您的应用打开文件进行读取或写入时,系统会向应用授予对该文件的 URI 的访问权限,该授权在用户重启设备之前一直有效。但是,如果需要在应用中保存最近访问的文件,例如历史记录。可以获取系统提供的永久性 URI 访问权限,如以下代码段所示:
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
applicationContext.contentResolver.takePersistableUriPermission(uri, takeFlags)
与4.1.3 打开媒体文件类似
private fun alterDocument(uri: Uri) {
try {
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(("Overwritten at ${
System.currentTimeMillis()}\n").toByteArray())
}
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
如果您获得了文档的 URI,并且该文档的 Document.COLUMN_FLAGS
包含 SUPPORTS_DELETE
,您便可以删除该文档。例如:
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)