分区存储如何影响文件访问:android
文件位置 | 所需权限 | 访问方法 (*) | 卸载应用时是否移除文件? |
---|---|---|---|
特定于应用的目录 | 无 | getExternalFilesDir() |
是 |
媒体集合 (照片、视频、音频) |
READ_EXTERNAL_STORAGE (仅当访问其余应用的文件时) |
MediaStore |
否 |
下载内容 (文档和 电子书籍) |
无 | SAF存储访问框架 (加载系统的文件选择器) |
否 |
对应于
MediaStore
类中仅包含五种文件类型Image/Video/Audio
以及Files
和Download
, 其中Image/Video/Audio
直接使用MediaStore
+ContentResolver
API便可访问 , 而Files
和Download
则是使用SAF
存储访问框架访问。缓存
⭐ 注意:使用分区存储的应用对于 /sdcard/DCIM/IMG1024.JPG 这类路径不具备直接内核访问权限。要访问此类文件,应用必须使用
MediaStore
,并调用ContentResolver.openFile()
等方法。bash
FileProvider
而且配置了external-files-path
和external-cache-path
,应用会在启动时自动建立 cache
和files
目录:<!--context.getExternalFilesDirs()-->
<external-files-path
name="ando_file_external_files"
path="." />
<!-- getExternalCacheDirs() 此标签须要 support 25.0.0以上才可使用-->
<external-cache-path
name="ando_file_external_cache"
path="." />
复制代码
FileProvider
:app
<provider
android:name=".common.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
复制代码
分区存储会施加如下媒体数据限制:框架
若您的应用未得到 ACCESS_MEDIA_LOCATION 权限,照片文件中的 Exif 元数据会被修改。要了解详情,请参阅介绍如何访问照片中的位置信息的部分。ide
MediaStore.Files 表格自己会通过过滤,仅显示照片、视频和音频文件。例如,表格中不显示 PDF 文件。 必须使用 MediaStore 在 Java 或 Kotlin 代码中访问媒体文件。请参阅有关如何从原生代码访问媒体文件
的指南。 该指南介绍了如何处理媒体文件,并提供了有关访问 MediaStore 内的单个文档和文档树的最佳作法。若是您的应用使用分区存储,则须要使用这些方法来访问媒体。post
系统会自动扫描外部存储,并将媒体文件添加到如下定义好的集合中:ui
DCIM/
and Pictures/
directories. The system adds these files to the MediaStore.Images
table.DCIM/
, Movies/
, and Pictures/
directories. The system adds these files to the MediaStore.Video
table.Alarms/
, Audiobooks/
, Music/
, Notifications/
, Podcasts/
, and Ringtones/
directories, as well as audio playlists that are in the Music/
or Movies/
directories. The system adds these files to the MediaStore.Audio
table.Download/
directory. On devices that run Android 10 (API level 29) and higher, these files are stored in the MediaStore.Downloads
table. This table isn't available on Android 9 (API level 28) and lower.若是您的应用使用范围存储,则它仅应针对运行Android 9(API级别28)或更低版本的设备请求与存储相关的权限。 您能够经过在应用清单文件中的权限声明中添加android:maxSdkVersion属性来应用此条件:this
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
复制代码
不要为运行Android 10或更高版本的设备没必要要地请求与存储相关的权限。 您的应用程序能够参与定义明确的媒体集合,包括MediaStore.Downloads集合,而无需请求任何与存储相关的权限。 例如,若是您正在开发相机应用程序,则无需请求与存储相关的权限,由于您的应用程序拥有您要写入媒体存储区的图像。
照片:存储在 MediaStore.Images 中。
视频:存储在 MediaStore.Video 中。
音频文件:存储在 MediaStore.Audio 中。
MediaStore 还包含一个名为 MediaStore.Files 的集合,该集合提供访问全部类型的媒体文件的接口。其余文件,例如 PDF 文件,没法访问到。
复制代码
注意:若是您的应用使用分区存储,MediaStore.Files 集合将仅显示照片、视频和音频文件。
若要加载媒体文件,请从 ContentResolver 调用如下方法之一:
openFileDescriptor()
。loadThumbnail()
,并传入要加载的缩略图的大小。ContentResolver.query()
。🌰查询一个媒体文件集合
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didnt 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)
}
}
复制代码
若是您的应用程序执行一些可能很是耗时的操做,好比写入媒体文件,那么在文件被处理时对其进行独占访问是很是有用的。在运行Android 10或更高版本的设备上,您的应用程序能够经过将IS_PENDING
标志的值设置为1
来得到这种独占访问。只有您的应用程序能够查看该文件,直到您的应用程序将IS_PENDING
的值更改回0。
为正在存储的媒体文件提供待处理状态
在搭载 Android 10(API 级别 29)及更高版本的设备上,您的应用能够经过使用 IS_PENDING 标记在媒体文件写入磁盘时得到对文件的独占访问权限。
如下代码段展现了在将图片存储到 MediaStore.Images 集合所对应的目录时如何使用 IS_PENDING 标记:
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val resolver = context.getContentResolver()
val collection = MediaStore.Images.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = resolver.insert(collection, values)
resolver.openFileDescriptor(item, "w", null).use { pfd ->
// Write data into the pending image.
}
// Now that we're finished, release the "pending" status, and allow other apps // to view the image. values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(item, values, null, null) 复制代码
Android Q 上,MediaStore 中添加了一个 IS_PENDING Flag,用于标记当前文件是 Pending 状态。
其余 APP 经过 MediaStore 查询文件,若是没有设置 setIncludePending 接口,就查询不到设置为 Pending 状态的文件,这就能使 APP 专享此文件。
这个 flag 在一些应用场景下可使用,例如在下载文件的时候:下载中,文件设置为 Pending 状态;下载完成,把文件 Pending 状态置为 0。
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "myImage.PNG");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri item = resolver.insert(uri, values);
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null);
// write data into the pending image.
} catch (IOException e) {
LogUtil.log("write image fail");
}
// clear IS_PENDING flag after writing finished.
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);
复制代码
Note: You can move files on disk during a call to update() by changing MediaColumns.RELATIVE_PATH or MediaColumns.DISPLAY_NAME.
注意:您能够在调用update 的过程当中经过更改 MediaColumns.RELATIVE_PATH 或MediaColumns.DISPLAY_NAME 在磁盘上移动文件。
复制代码
相关视频 (Youtube):
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。借助 SAF,用户可轻松在其全部首选文档存储提供程序中浏览并打开文档、图像及其余文件。用户可经过易用的标准界面,以统一方式在全部应用和提供程序中浏览文件,以及访问最近使用的文件。
⭐ Note: The DocumentFile class's
canWrite()
method doesn't necessarily indicate that your app can edit a document. That's because this method returns true ifDocument.COLUMN_FLAGS
contains eitherFLAG_SUPPORTS_DELETE
orFLAG_SUPPORTS_WRITE
. To determine whether your app can edit a given document, query the value ofFLAG_SUPPORTS_WRITE
directly.
Android 7.0 在存储访问框架中加入了虚拟文件的概念。即便虚拟文件没有二进制表示形式,客户端应用也可将其强制转换为其余文件类型,或使用 ACTION_VIEW Intent 查看这些文件,从而打开文件中的内容。
如要打开虚拟文件,您的客户端应用需包含可处理此类文件的特殊逻辑。若想获取文件的字节表示形式(例如为了预览文件),则需从文档提供程序请求另外一种 MIME 类型。
为得到应用中虚拟文件的 URI,您首先需建立 Intent 来打开文件选择器界面(如先前搜索文档中的代码所示)。
⭐ 重要说明:因为应用不能使用 openInputStream() 方法直接打开虚拟文件,所以若是您在 ACTION_OPEN_DOCUMENT Intent 中加入 CATEGORY_OPENABLE 类别,则您的应用不会收到任何虚拟文件。
经过上面的分析能够看出, MediaStore 仅能够处理公共目录中的 图片/视频/音频
文件, 当涉及到分组文件和其它类型文件的时候显得捉襟见肘。
- [操做一组文件](https://developer.android.google.cn/training/data-storage/shared/media#manage-groups-files)
- [操做文档和其余文件](https://developer.android.google.cn/training/data-storage/shared/media#other-file-types)
- [把数据分享给其它应用](https://developer.android.google.cn/training/data-storage/shared/media#companion-apps)
复制代码
若是您提早知道要存储多少数据,则能够经过调用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.
}
复制代码
⭐ 保存文件以前,不须要检查可用空间量。 相反,您能够尝试当即写入文件,而后在发生异常时捕获IOException。
AndroidManifest.xml中声明:android:hasFragileUserData="true",卸载应用会有提示是否保留 APP数据。默认应用卸载时App-specific目录下的数据被删除,但用户能够选择保留。
Youtube 👉 www.youtube.com/watch?v=UnJ…
Bilibili 👉 www.bilibili.com/video/BV1NE…