android 分区存储适配总结
一、分区存储概念
Android 中存储可以分为两大类:私有存储和共享存储
1、私有存储 (Private Storage) : 每个应用在内部存储种都拥有自己的私有目录,其它应用看不到,彼此也无法访问到该目录。
地址:/storage/emulated/0/Android/data/包名/files
私有目录存放app的私有文件,会随着App的卸载而删除。
2、共享存储 (Shared Storage) : 除了私有存储以外,其他的一切都被认定是共享存储,比如:Downloads、Documents、Pictures 、DCIM、Movies、Music等。
地址:/storage/emulated/0/Downloads(Pictures)等
公有目录下的文件不会跟随APP卸载而删除。
在分区存储之前,某些应用中,即使功能很简单,大部分都不需要这么宽泛的权限。
这就使得某些应用程序
- 1、乱占空间 :各种各样的文件散布在磁盘的各个地方,当用户卸载应用之后,这些被遗弃的 “孤儿 “ 被滞留在原地,无家可归,占用了磁盘空间,最终结果就会导致磁盘不足
- 2、随意读取用户的数据
- 3、随意读取应用的数据
因此 —— 分区存储产生了,也叫沙盒存储~
以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储), 便于用户更好的管理外部存储文件。如果不符合条件的会以兼容模式运行,兼容模式跟以前一样,根据路径可以直接存储文件。
从另一个角度来说,分区存储的推出更好的保护用户的隐私。默认情况下,对于以 Android 10 及更高版本为目标平台的应用,其访问权限范围限定为外部存储,即分区存储。此类应用可以查看外部存储设备内以下类型的文件,无需请求任何与存储相关的用户权限:
应用外部特定目录中的文件(使用getExternalFilesDir()访问)。
应用自己创建的照片、视频和音频(通过MediaStore访问)。
意思是说,我们的app在外部存储设备(即SD卡)上存文件的时候,需要先想明白需要存的数据是属于app私有的还是需要分享的,如果是app私有的,存在getExternalFilesDir()返回的文件夹下,也就是Android/data/包名/files/文件夹;如果是需要分享的,需要采用媒体库(MediaStore)的方式来存取。需要指出的是在分区存储模型下存取共享媒体文件是不需要存储权限的,而旧的存储模型是需要存储权限的。
下表总结了分区存储如何影响文件访问:
类型 | 位置 | 访问应用自己生成的文件 | 访问其他应用生成的的文件 | 访问方法 | 卸载应用是否删除文件 |
---|---|---|---|---|---|
外部存储 | Photo/ Video/ Audio/ | 无需权限 | 需要权限READ_EXTERNAL_STORAGE | MediaStore Api | 否 |
外部存储 | Downloads | 无需权限 | 无需权限 | 通过存储访问框架SAF,加载系统文件选择器 | 否 |
外部存储 | 应用特定的目录 | 无需权限 | 无法直接访问 | getExternalFilesDir()获取到属于应用自己的文件路径 | 是 |
为什么要适配?
在分区存储模型下,外部存储设备的公共区域是不让访问的,如果强行访问,会在创建或读写文件的api上报错.
那么有没有办法关闭分区存储模型呢?
有两种办法:
第一种是app的targetSdkVersion永远低于29,这个是不现实的;
第二种办法是在targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage=“true”,就可以不启动分区存储,让以前的文件读取正常使用,但是targetSdkVersion = 30中不行了,强制开启分区存储。 当然,作为人性化的android,还是为开发者留了一小手,如果是覆盖安装呢,可以增加android:preserveLegacyExternalStorage=“true”,暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要卸载重装,就会失效了。
以下是关于分区存储会遇到的所有情况,给大家罗列出来了,先上代码:
fun saveFile() {
if (checkPermission()) {
//getExternalStoragePublicDirectory被弃用,分区存储开启后就不允许访问了
val filePath = Environment.getExternalStoragePublicDirectory("").toString() + "/test3.txt"
val fw = FileWriter(filePath)
fw.write("hello world")
fw.close()
showToast("文件写入成功")
}
}
分情况运行:
1) targetSdkVersion = 28,运行后正常读写。
2) targetSdkVersion = 29,不删除应用,targetSdkVersion 由28修改到29,覆盖安装,运行后正常读写。
3) targetSdkVersion = 29,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied))
4) targetSdkVersion = 29,添加android:requestLegacyExternalStorage=“true”(不启用分区存储),读写正常不报错
5) targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,读写报错,程序崩溃(open failed: EACCES (Permission denied))
6) targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,增加android:preserveLegacyExternalStorage=“true”,读写正常不报错
7) targetSdkVersion = 30,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied))
关于requestLegacyExternalStorage和preserveLegacyExternalStorage在不同版本的表现,总结如下表:
关于targetSdkVersion,Google Play的规定是从今年8月开始,所有新上线的应用的目标API,即targetSdkVersion必须升级到30以上,对现有应用更新新的版本,这个政策的要求将自 11 月开始生效。抛开Google Play的规定不谈,关于Gradle中的minSdkVersion、compileSdkVersion以及targetSdkVersion的具体作用,参考此篇博客:
https://blog.csdn.net/qq_23062979/article/details/81294550
二、分区适配方案
分区存储需要遵循三个原则:
- 更好的文件属性:系统应用知道什么文件属于哪一个app, 让用户更加容易管理他们自己的文件。当 app 被卸载了,被应用创建的内容,除非用户希望保留,否则不应该保留下来。
- 用户的数据安全 :当用户下载文件,比如敏感的电子邮件附件,这些文件对大多数应用程序都不应该可见
- 应用的数据安全 :当 app 将特定于应用程序的文件写入外部存储时,其他应用程序不应该可见这些文件
1、 应用分区存储的文件访问规定
(1).应用专属目录
每个应用向自己的私有目录读写文件,不需要读写权限。
应用即使获取了读写权限,也无法访问其他应用的私有目录。
私有文件目录具体路径:storage/emulated/0/android/data/packageName/…
访问方式:
this.getExternalMediaDirs() ==[/storage/emulated/0/Android/media/com.yoshin.tspsdk]
this.getExternalCacheDir() == /storage/emulated/0/Android/data/com.yoshin.tspsdk/cache this.getExternalFilesDir(Environment.DIRECTORY_SCREENSHOTS) == /storage/emulated/0/Android/data/com.yoshin.tspsdk/files/Screenshots
其中,这几个方法在调用的时候,如果还没有对应文件夹,都会进行创建。
Environment.DIRECTORY_SCREENSHOTS 是可以被替他参数代替的,就会访问其他文件夹了,然后低版本会有异常,不建议这么用。
(2).共享目录文件
共享目录文件需要通过MediaStore API或者Storage Access Framework方式访问
(1) MediaStore API 在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请任何存储权限,所有者拥有文件的所有权;
(2)MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频),
需要申请存储权限,未申请存储权限,通过ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常;
(3)MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访。
2、MediaStore API介绍
系统会自动扫描外部存储,添加文件到系统已定义的Images、Videos、Audio files、Downloaded files集合中,Android 10通过MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问共享目录文件资源
MediaStore API创建文件
Android 10版本 MeidaStore API只允许在共享目录指定目录创建文件, 非指定目录创建文件会抛出IllegalArgumentException, 创建文件目录汇总如下:
媒体类型 | Uri | 默认创建目录 | 允许存储文件 |
---|---|---|---|
Image | content://media/external/images/media | Pictures | 只能放图片 |
Audio | content://media/external/audio/media | Music | 只能放音频 |
Video | content://media/external/video/media | Movies | 只能放视频 |
Download | content://media/external/downloads | Download | 任意类型 |
MediaStore.Downloads.EXTERNAL_CONTENT_URI是Android10版本新增API,用于创建、访问非媒体文件,需要注意的是:虽然获取了存储权限,但是依然不能够删除和修改其他应用的文件。
MediaStore API 文件访问
val external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Images.Media._ID)
val cursor = contentResolver.query(external, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
queryUri = ContentUris.withAppendedId(external, cursor.getLong(0))
// queryUri即上图中对应的uri
cursor.close()
}
在Android Q以下版本,使用该方法可以拿到媒体文件的绝对路径(比如external/DCIM/xxx.png),即DATA字段,但是在Android Q及以上版本,DATA字段被弃用且不再可靠,新增了RELATIVE_PATH字段表示相对地址,通过该字段可以设置媒体文件保存的位置(具体见下文)。
Android Q以下版本可以通过DATA字段拿到绝对路径并转换成File类型,对文件进行操作,Android Q之后不再可行。要访问这个uri,通用的方法是通过文件描述符FileDescriptor来实现,示例代码如下:
var pfd: ParcelFileDescriptor? = null
try {
pfd = contentResolver.openFileDescriptor(queryUri!!, "r")
if (pfd != null) {
val bitmap = BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor)
imageIv.setImageBitmap(bitmap)
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
pfd?.close()
}
读取MedisStore文件时,如果未申请READ_EXTERNAL_STORAGE权限,那么读取到的图片只有自己应用保存的图片,换句话说,应用读取和操作自己保存的媒体文件不需要申请READ_EXTERNAL_STORAGE权限,但是要访问其他应用创建的媒体文件,需要申请权限。
在Android Q以下只使用DATA字段,Android Q及以上不使用DATA字段,改为使用RELATEIVE_PATH字段。
3、Storage Access Framework介绍
上面提到MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访。
对文件和目录访问使用 SAF (存储访问框架–Storage Access Framework),SAF访问方式不需要申请权限
示例代码:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");//设置类型,我这里是任意类型,任意后缀的可以这样写。
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, READ_REQUEST_CODE);
4、Android Q以上中使用ContentResolver进行文件的增删改查
1、获取(创建)自身目录下的文件夹
获取及创建,如果手机中没有对应的文件夹,则系统会自动生成
//在自身目录下创建apk文件夹
File apkFile = context.getExternalFilesDir("apk");
2、创建自身目录下的文件
生成需要下载的路径,通过输入输出流读取写入
String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "temp.apk");
OutputStream os = null;
try {
os = new FileOutputStream(newFile);
if (os != null) {
os.write("file is created".getBytes(StandardCharsets.UTF_8));
os.flush();
}
} catch (IOException e) {
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e1) {
}
}
3、创建并获取公共目录下的文件路径
通过MediaStore.insert写入
//这里的fileName指文件名,不包含路径
//relativePath 包含某个媒体下的子路径
private static Uri insertFileIntoMediaStore (String fileName, String fileType,String relativePath) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return null;
}
ContentResolver resolver = context.getContentResolver();
//设置文件参数到ContentValues中
ContentValues values = new ContentValues();
//设置文件名
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
//设置文件描述,这里以文件名为例子
values.put(MediaStore.Downloads.DESCRIPTION, fileName);
//设置文件类型
values.put(MediaStore.Downloads.MIME_TYPE,"application/vnd.android.package-archive");
//注意RELATIVE_PATH需要targetVersion=29
//故该方法只可在Android10的手机上执行
values.put(MediaStore.Downloads.RELATIVE_PATH, relativePath);
//EXTERNAL_CONTENT_URI代表外部存储器
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
//insertUri表示文件保存的uri路径
Uri insertUri = resolver.insert(external, values);
return insertUri;
}
4、公共目录下的指定文件夹下创建文件
结合上面代码,我们主要是在公共目录下创建文件或文件夹拿到本地路径uri,不同的Uri,可以保存到不同的公共目录中。接下来使用输入输出流就可以写入文件
下面代码仅是以文件复制存储举例,sourcePath
表示原文件地址,复制文件到新的目录; 文件下载直接下载本地,无需文件复制。
重点:AndroidQ中不支持file://类型访问文件,只能通过uri方式访问
private static void saveFile(Context context, Uri insertUri){
if(insertUri == null) {
return;
}
String mFilePath = insertUri.toString();
InputStream is = null;
OutputStream os = null;
try {
os = resolver.openOutputStream(insertUri);
if(os == null){
return;
}
int read;
File sourceFile = new File(sourcePath);
if (sourceFile.exists()) {
// 文件存在时
is = new FileInputStream(sourceFile); // 读入原文件
byte[] buffer = new byte[1024];
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5、使用MediaStore读取公共目录下的文件
通过ContentResolver openFileDescriptor接口,选择对应的打开方式。
例如”r”表示读,”w”表示写,返回ParcelFileDescriptor类型的文件描述符。
private static Bitmap readUriToBitmap (Context context, Uri uri) {
ParcelFileDescriptor parcelFileDescriptor = null;
FileDescriptor fileDescriptor = null;
Bitmap tagBitmap = null;
try {
parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null && parcelFileDescriptor.getFileDescriptor() != null) {
fileDescriptor = parcelFileDescriptor.getFileDescriptor();
//转换uri为bitmap类型
tagBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
// 你可以做的~~
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (parcelFileDescriptor != null) {
parcelFileDescriptor.close();
}
} catch (IOException e) {
}
}
}
6、使用MediaStore删除文件
public static void deleteFile (Context context, Uri fileUri) {
context.getContentResolver().delete(fileUri, null, null);
}
三、所有文件访问权限
虽然说了这么多,但是还有些应用就要访问所有文件,比如杀毒软件,文件管理器。放心,有办法!MANAGE_EXTERNAL_STORAGE 这不来了吗。 这个权限就是用来获取所有文件的管理权限。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
val intent = Intent()
intent.action= Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
startActivity(intent)
//判断是否获取MANAGE_EXTERNAL_STORAGE权限:
val isHasStoragePermission= Environment.isExternalStorageManager()
如果是文件管理器、备份及存储类的应用。你需要在 Google Play Developer Console 上填写声明表格说明为什么需要 MANAGE_EXTERNAL_STORAGE 权限,提交之后会被审核是否加入白名单,一旦加入成功以后,您的应用就可以向用户索要权限了,如果用户也同意您应用的访问权限请求,MadiaStore 访问将不再过滤,包括非媒体库文件。但是获得此权限的应用仍然无法访问这些目录在存储卷上显示为 Android/data/ 的子目录,也就是属于其他应用的应用专属目录。
四、分区存储模型下,访问SD卡公共区域错误举例
FileOutputStream|FileInputStream
在分区存储模型下,SD卡的公共目录是不让访问的,除了共享媒体的那几个文件夹。所以,用一个公共目录的路径实例化FileOutputStream或者FileInputStream会报FileNotFoundException异常
java.io.FileNotFoundException: /storage/emulated/0/xxx/SharePic/1603277403193.jpg: open failed: ENOENT (No such fileor directory)
at libcore.io.IoBridge.open(IoBridge.java:496)
at java.io.FileOutputStream.<init>(FileOutputStream.java:235)
at com.xxx.ui.QrCodeActivity.askSDCardSaveImgPermission(QrCodeActivity.java:242)
...
java.io.FileNotFoundException: /storage/emulated/0/xxx/data/testusf: open failed: EACCES (Permission denied)
at libcore.io.IoBridge.open(IoBridge.java:496)
at java.io.FileInputStream.<init>(FileInputStream.java:159)
at java.io.FileReader.<init>(FileReader.java:72)
at com.android.xxx.sdk.common.toolbox.FileUtils.readSingleLineStringFromFile(FileUtils.java:747)
五、文件存放位置选择
文件存储规范建立之后,当有新的业务需要建立单独文件目录时,可以遵循以下规律决定存放的位置
六、总结
- 特定于应用的目录 –> 无需权限 –> 访问方法 getExternalFilesDir () –> 卸载应用时移除文件
- 媒体集合 (照片、视频、音频) –> 需要权限 READ_EXTERNAL_STORAGE (仅当访问其他应用的文件时) –> 访问方法 MediaStore –> 卸载应用时不移除文件
- 下载内容(文档和电子书籍)–> 无需权限 –> 存储访问框架(加载系统的文件选择器)–> 卸载应用时不移除文件
今天的文章android 分区存储适配总结分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/27111.html