Scoped Storage介绍
Scoped Storage由来
Android长久以来都支持外置存储空间这个功能,也就是我们常说的SD卡存储。这个功能使用得极其广泛,几乎所以开发者在开发的时都喜欢在SD卡的根目录下建立一个自己应用的专属的目录,用来存放各类文件和数据。导致SD卡的文件管理变的异常混乱。而且用户即使我卸载了一个完全不再使用的程序,它所产生的垃圾文件却可能会一直保留在我的手机上,不会被自动删除,这就使用户的存储空间一直处于十分紧张的状态,还浪费了大量的存储资源。
为了解决上述问题,Google 在Android 10当中加入了Scoped Storage分区存储机制。
Scoped Storage机制
为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能看到本应用专有的目录(通过 context.getExternalFilesDir() 访问)以及本应用所创建的特定类型的媒体文件。如果你的应用不符合该条件的会以兼容模式运行,兼容模式跟以前一样,根据路径可以直接存储文件。但很有可能随着SDK的更新而无法使用,所以建议尽早完成Scoped Storage 的适配。
Scoped Storage特点
分区存储(Scoped Storage)机制,是一种安全机制,用于防止应用读取其他应用的数据;并具有以下特点:
-
每个应用程序都有自己的存储空间,即?应用专属目录; -
应用程序不能翻过自己的目录,去访问公共目录; -
应用程序请求的数据都要通过权限检测,不符合要求不会被放行; -
使用?MediaStore?相关API可以让你访问共享的存储空间
注意:
通过android:requestLegacyExternalStorage="true"设置兼容模式,在Android 11中以上配置依然有效,但仅限于targetSdkVersion小于或等于29的情况。如果你的targetSdkVersion >=30,Scoped Storage就会被强制启用,android:requestLegacyExternalStorage="true"标记将会被忽略。
Android保存图片到本地(兼容Android 10+)
Android10+系统会自动扫描外部存储卷,并将媒体文件添加到以下明确定义的集合中:
图片保存在DCIM公开目录下,API<=28版本使用new File()保存文件到指定DCIM公开目录,API>=29使用MediaStore保存文件到指定DCIM公开目录;
保存图片到DCIM公开目录下代码如下:
public class ImageSaveUtil {
/**
* 保存图片到公共目录DCIM
* API<=28,需要提前申请文件读写权限
* API>=29,不需要权限
* 保存的文件在 DCIM 目录下
*
* @param context 上下文
* @param bitmap 需要保存的bitmap
* @param format 图片格式
* @param quality 压缩的图片质量
* @param recycle 完成以后,是否回收Bitmap,建议为true
* @return 文件的 uri
*/
@Nullable
public static Uri saveAlbum(Context context, Bitmap bitmap, Bitmap.CompressFormat format, int quality, boolean recycle) {
String suffix;
if (Bitmap.CompressFormat.JPEG == format)
suffix = "JPG";
else
suffix = format.name();
String fileName = System.currentTimeMillis() + "_" + quality + "." + suffix;
if (Build.VERSION.SDK_INT < 29) {
if (!isGranted(context)) {
Log.e("ImageUtils", "save to album need storage permission");
return null;
}
File picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
File destFile = new File(picDir, fileName);
if (!save(bitmap, destFile, format, quality, recycle))
return null;
Uri uri = null;
if (destFile.exists()) {
uri = Uri.parse("file://" + destFile.getAbsolutePath());
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(uri);
context.sendBroadcast(intent);
}
return uri;
} else {
// Android 10 使用
Uri contentUri;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else
contentUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI;
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/*");
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/");
// 告诉系统,文件还未准备好,暂时不对外暴露
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1);
Uri uri = context.getContentResolver().insert(contentUri, contentValues);
if (uri == null) return null;
OutputStream os = null;
try {
os = context.getContentResolver().openOutputStream(uri);
bitmap.compress(format, quality, os);
// 告诉系统,文件准备好了,可以提供给外部了
contentValues.clear();
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0);
context.getContentResolver().update(uri, contentValues, null, null);
return uri;
} catch (Exception e) {
e.printStackTrace();
// 失败的时候,删除此 uri 记录
context.getContentResolver().delete(uri, null, null);
return null;
} finally {
try {
if (os != null)
os.close();
} catch (IOException e) {
// ignore
}
}
}
}
private static boolean save(Bitmap bitmap, File file, Bitmap.CompressFormat format, int quality, boolean recycle) {
if (isEmptyBitmap(bitmap)) {
Log.e("ImageUtils", "bitmap is empty.");
return false;
}
if (bitmap.isRecycled()) {
Log.e("ImageUtils", "bitmap is recycled.");
return false;
}
if (!createFile(file, true)) {
Log.e("ImageUtils", "create or delete file <$file> failed.");
return false;
}
OutputStream os = null;
boolean ret = false;
try {
os = new BufferedOutputStream(new FileOutputStream(file));
ret = bitmap.compress(format, quality, os);
if (recycle && !bitmap.isRecycled()) bitmap.recycle();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (os != null)
os.close();
} catch (IOException e) {
// ignore
}
}
return ret;
}
private static boolean isEmptyBitmap(Bitmap bitmap) {
return bitmap == null || bitmap.isRecycled() || bitmap.getWidth() == 0 || bitmap.getHeight() == 0;
}
private static boolean createFile(File file, boolean isDeleteOldFile) {
if (file == null) return false;
if (file.exists()) {
if (isDeleteOldFile) {
if (!file.delete()) return false;
} else
return file.isFile();
}
if (!createDir(file.getParentFile())) return false;
try {
return file.createNewFile();
} catch (IOException e) {
return false;
}
}
private static boolean createDir(File file) {
if (file == null) return false;
if (file.exists())
return file.isDirectory();
else
return file.mkdirs();
}
private static boolean isGranted(Context context) {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE));
}
}
参考
Android 10的ScopedStorage(分区存储)的介绍_卡哇伊的萝莉的博客-CSDN博客
Android 保存图片到本地 兼容Android 10+_OneGreenHand的博客-CSDN博客_android 保存图片到本地
安卓10(Android10\API29)保存图片到相册DCIM/Camera - 简书
数据和文件存储概览 ?|? Android 开发者 ?|? Android Developers
|