上架Google Store的App, 必须把TargetSDKVersion指向30,也就是Android 11。意味着你必须适配分区存储了。当然只上国内市场的,那不用管,因为TargetSDKVersion只要求28。
分区存储的目的:
解决在根目录下,根目录乱建文件夹,隐私安全的问题。根目录文件夹在app卸载或清理缓存时,不会自动删除,由此导致根目录乱七八糟的,浪费用户空间。
缺点,在Android/data/和Android/obb/这两个文件夹下的垃圾文件再也无法通过垃圾清理软件删除了,很多app针对log文件,临时文件等,都没有做定期清理,导致越来越庞大。以后用户更加需要手动清理缓存了,但是用户也不可能每个APP都点一遍清理缓存。
分区存储的影响:
外卡(外部存储卷), 只能访问所谓的共享文件夹,或者说公共目录。这些目录都是存储多媒体文件的。
图片(包括照片和屏幕截图),存储在 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)及更低版本中不可用。在Picture/下只能创建图片文件。在Music/下只能创建音乐文件。在Movie/下只能创建视频文件。以上三个文件夹,都能创建文件夹。Download文件夹只能搜出多媒体文件,以及文件夹。不能访问的两个目录:Android/data/ ,Android/obb/。除非是自己app对应的目录。
媒体库还包含一个名为 MediaStore.Files 的集合。1. 如果启用了分区存储,集合只会显示您的应用创建的照片、视频和音频文件。2. 如果分区存储不可用或未使用,集合将显示所有类型的媒体文件。
分区存储的适配:
针对Android 9及以前,依旧维持以前的做法。
针对Android 10依旧可以不打开分区存储。在 Manifest 中增加 就可以豁免了。如果要检测是否已豁免,使用 Environment.isExternalStorageLegacy() 函数。
针对Android 11及以后, 在Google store市场上架, 必须适配分区存储。
在Android 10上打开分区存储,只能使用MediaStore API访问文件。从 Android 11 开始,具有 READ_EXTERNAL_STORAGE 权限的应用可以使用直接文件路径和原生库来读取设备的多媒体文件。
缓存非媒体文件时,你可以选择以下两类文件夹,其他app是无法访问到的。
小文件或包含敏感信息的文件:请使用 Context#getCacheDir()。在内卡,/data/0/packageName大型文件或不含敏感信息的文件:请使用 Context#getExternalCacheDir()。在外卡,/Android/data/packageName
当应用卸载或者清除数据后,该区域文件会被删除。
分区存储的文件访问:
文件访问的方式有两中,MediaStore API, 以及存储访问框架(SAF)。
Android系统针对文件类型进行了分类,图片、音频、视频这三类文件将可以通过MediaStore API来进行访问,而其他类型的文件则需要使用系统的文件选择器来进行访问。
权限
我们的应用程序向媒体库贡献的图片、音频或视频,将会自动拥有其读写权限,不需要额外申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。而如果你要读取其他应用程序向媒体库贡献的图片、音频或视频,则必须要申请READ_EXTERNAL_STORAGE权限才行。WRITE_EXTERNAL_STORAGE权限将会在未来的Android版本中废弃。
系统会将每个媒体文件归因于一个应用,未请求存储权限时可以访问的文件。每个文件只能归因于一个应用。因此,如果您的应用创建的媒体文件存储在照片、视频或音频文件媒体集合中,应用便可以访问该文件。通常无法更新其他应用存放到媒体库中的媒体文件。但是,如果用户卸载并重新安装您的应用,您必须请求 READ_EXTERNAL_STORAGE 才能访问应用最初创建的文件。此权限请求是必需的,因为系统认为文件归因于以前安装的应用版本,而不是新安装的版本。
存储访问框架(SAF)
既可以选取多媒体文件,也可以选取其他类型文件。当你需要访问非多媒体文件时,或者访问根目录下的非共享文件夹时(可能是连接电脑后,在电脑上创建的),那么只能使用SAF。
- 选择文件
使用 ACTION_OPEN_document intent 要求用户使用系统选择器选择要打开的文件。如果您想过滤系统选择器提供给用户选择的文件类型,您可以使用 setType() 或 EXTRA_MIME_TYPES。
用户选择后,会返回选择文件的Uri。通过Uri用MediaStore获取文件信息,读取文件内容。
例如,您可以使用以下代码查找所有 PDF、ODT 和 TXT 文件:
//查找多种具体文件格式的方式:
private ActivityResultLauncher
用下面的代码查找所有图片类型:
//指定一种文件类型。 private ActivityResultLauncher
同时,Android 11对存储访问框架(SAF)添加了以下限制:
使用 ACTION_OPEN_document_TREE 或 ACTION_OPEN_document,无法浏览到Android/data/ 和 Android/obb/ 目录。
使用 ACTION_OPEN_document_TREE无法授权访问存储根目录、Download文件夹。
2. 读取文件
共享文件夹的内容,可以和以前一样依赖全路径,用File类读取。但是需要READ_EXTERNAL_STORAGE 权限。也可以用MediaStore读取类容,代码如下:
文件描述符
// Open a specific media item using ParcelFileDescriptor.
ContentResolver resolver = getApplicationContext()
.getContentResolver();
// "rw" for read-and-write;
// "rwt" for truncating or overwriting existing file contents.
String readonlyMode = "r";
try (ParcelFileDescriptor pfd =
resolver.openFileDescriptor(content-uri, readOnlyMode)) {
// Perform operations on "pfd".
} catch (IOException e) {
e.printStackTrace();
}
文件流
// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext()
.getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
// Perform operations on "stream".
}
文件Uri的获取:
//利用Query获取到文件ID后,拼接成Uri
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
//或者
Uri photoUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
MediaStore
1. 删除文件
删除文件需要询问用户,只有当用户同意以后才能删除。可以批量处理。
try {
PendingIntent deleteRequest = null;
deleteRequest = MediaStore.createDeleteRequest(context.getContentResolver(), uris);
context.startIntentSenderForResult(deleteRequest.getIntentSender(), requestCode, null, 0, 0, 0, null);
} catch (Exception e) {
e.printStackTrace();
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EDIT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "文件已删除", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "用户没有授权删除", Toast.LENGTH_SHORT).show()
}
}
}
}
2. 创建/更新文件
更新文件也需要询问用户,只有当用户同意以后才能更新。可以批量处理。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val urisToModify = listOf(uri1, uri2, uri3, uri4)
val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
null, 0, 0, 0)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
EDIT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "用户已授权", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "用户没有授权", Toast.LENGTH_SHORT).show()
}
}
}
}
3. 还有两个不太常用的
createFavoriteRequest() 用于请求将多个文件加入到Favorite(收藏)的权限。createTrashRequest() 用于请求将多个文件移至回收站的权限。
4. 创建文件
fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
val outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
bitmap.compress(compressFormat, 100, outputStream)
outputStream.close()
}
}
}
Android11之前的版本中并没有RELATIVE_PATH,所以我们要使用DATA常量(已在Android 10中废弃),并拼装出一个文件存储的绝对路径才行。values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES.plus("/hello")) 就会在把图片放在/Pictures/hello/目录下
分区存储后门
分区存储也还留有后门的,app可以申请MANAGE_EXTERNAL_STORAGE权限。这是针对那些文件管理App的,比如es explore, 他们必须有这样的权限,要不然文件列表都无法列出来了,尤其是非媒体类型。但是这个权限在上架google play时需要申请的。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
ActivityCompat.startActivity(v.getContext(), intent, null);
return;
}
}
好了,关于分区存储就讲这么多。 首先要搞明白你的app是不是适配, 是国内市场的还是国外市场的。国内市场的就不需要复杂适配。
Android 存储用例和最佳做法 | Android 开发者 | Android Developers
访问共享存储空间中的媒体文件 | Android 开发者 | Android Developers



