背景
在适配分区存储之前,有三个问题需要了解下
1. 什么是分区存储?
分区存储是Android10中提出的文件存储策略,用于更好的管理私有数据和共享数据,减少之前的文件和数据存放混乱的问题。
2. 为什么要适配分区存储?
从Android10开始的分区存储策略,可能会导致之前的文件存储功能,在之后的版本使用中出现问题,如:读取和写入文件失败、文件出现异常等,因此需要适配分区存储。
3. 如何适配分区存储?
请看下面。
导航
- 简介
- 内容
- 1.自身APP操作
- 1.1内部存储空间和外部存储空间
- 1.2 共享存储空间
- 2.其他APP操作
- 2.1 外部存储空间
- 2.2 共享存储空间
- 知识点
- 1.SAF存储访问框架
- 2.共享存储空间对应表
- 3.权限请求表
- 问题点
- 1.File api受限
- 2.Media api效率
简介
分区存储主要将APP的存储空间分为私有存储空间和共享存储空间。私有存储空间存放只允许APP自身进行读取的文件和数据,如APP的配置信息、图片缓存等;共享存储空间存放需要提供给其他APP使用的文件,如office文档、相册图片等。
使用分区存储,主要便于保护APP的隐私和减少卸载残留。
内容
1.自身APP操作
下列操作都是由okhttp下载图片到内部存储空间/files/test/Test.jpg中,然后将图片写入到对应的存储空间
1.1内部存储空间和外部存储空间
内部目标文件位置位于 内部存储/files/Test.jpg
外部目标文件位置位于 外部存储空间/Pictures/Test.jpg因为内部存储和外部存储的操作基本一致,不同的是操作的路径,因此将内部存储和外部存储的操作方法合并。获取内部存储的路径方法为:getFilesDir(),获取外部存储的路径方法为:getExternalFilesDir()。
(1)写文件
//写入文件到内/外部存储
private void writeImgToInternalOrExternal(String path){
//判断文件是否存在
File newFile = new File(path);
if (newFile.exists()){
newFile.delete();
}
//写入文件
try {
InputStream is = new FileInputStream(downloadPath);
OutputStream os = new FileOutputStream(internalPath);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
os.close();
is.close();
Bitmap bitmap = BitmapFactory.decodeFile(path);
ivShow.setImageBitmap(bitmap);
tvShow.setText("保存到内/外部存储成功,路径:"+path);
}catch (IOException e){
tvShow.setText("保存到内/外部存储失败:"+e.getMessage());
ivShow.setImageResource(0);
}
}
(2)读文件
//读取文件从内/外部存储
private void readImgFromInternalOrExternal(String path){
File newFile= new File(path);
if (!newFile.exists()){
tvShow.setText("内/外部存储无文件");
ivShow.setImageResource(0);
return;
}
Bitmap bitmap = BitmapFactory.decodeFile(path);
ivShow.setImageBitmap(bitmap);
tvShow.setText("内/外部存储文件路径:"+path);
}
(3)更新文件
这里使用OkHttp从网络中获取图片数据并写入
建议:这里仅供参考,请删除原先的文件后重新创建文件写入,推荐使用写文件的操作
//更新文件从内/外存储
private void updateImgFromExternal(InputStream is){
try {
OutputStream os = new FileOutputStream(externalPath);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
os.close();
tvShow.setText("修改内/外存储文件成功");
}catch (Exception e){
tvShow.setText("修改内/外存储文件失败:"+e.getMessage());
}
}
(4)删除文件
//删除文件从内/外部存储
private void deleteImgFromInternal(String path){
File file = new File(path);
if (!file.exists()){
tvShow.setText("内/外部存储文件不存在");
ivShow.setImageResource(0);
return;
}
file.delete();
tvShow.setText("删除内/外部存储文件成功");
ivShow.setImageResource(0);
}
1.2 共享存储空间
目标文件位置位于 Pictures/Test.jpg
(1)写文件
//写入文件到共享存储
private void writeImgToShareStorage(){
//检查文件是否存在(这里使用这种方式速度太慢,待后期进行改进)
try {
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//文件存在
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
int raw = getContentResolver().delete(uri,null,null);
cursor.close();
}
//保存文件
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,"Test.jpg");
contentValues.put(MediaStore.Images.Media.MIME_TYPE,"image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,Environment.DIRECTORY_PICTURES);
}else {
//创建文件
String dataPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test.jpg";
File file = new File(dataPath);
if (file.exists()){
file.delete();
}else {
//这里根据实际路径判断是否创建父类的文件夹
file.createNewFile();
}
contentValues.put(MediaStore.Images.Media.DATA,dataPath);
}
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues);
//写入
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"w");
InputStream is = new FileInputStream(downloadPath);
OutputStream os = new FileOutputStream(pfd.getFileDescriptor());
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
os.close();
is.close();
String bitmapPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test.jpg";
ivShow.setImageURI(uri);
tvShow.setText("保存到共享存储成功,路径为:"+bitmapPath);
}catch (Exception e){
tvShow.setText("保存文件到共享存储失败:"+e.getMessage());
ivShow.setImageResource(0);
}
}
(2)读文件
//读取文件从共享存储
private void readImgFromShareStorage(){
try {
//检查是否存在
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
//回调结果
String[] projection = new String[]{MediaStore.Images.Media._ID,MediaStore.Images.Media.DISPLAY_NAME,MediaStore.Images.Media.RELATIVE_PATH,MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//文件存在
String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
String path = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.RELATIVE_PATH))+File.separator+name;
}else {
path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
}
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"r");
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
ivShow.setImageBitmap(bitmap);
tvShow.setText("读取共享存储的文件成功,路径为:"+path);
cursor.close();
}else {
//文件不存在
tvShow.setText("共享存储文件不存在");
ivShow.setImageResource(0);
}
}catch (Exception e){
tvShow.setText("从共享存储读取文件失败:"+e.getMessage());
ivShow.setImageResource(0);
}
}
(3)更新文件
更新文件名称
//更新文件名称从共享存储
private void updateImgNameFromShareStorage(){
try {
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//文件存在
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
//更新数据
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME,"Test123.jpg");
contentValues.put(MediaStore.Images.Media.MIME_TYPE,"image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH,Environment.DIRECTORY_PICTURES);
}else {
//创建文件
String dataPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test123.jpg";
File file = new File(dataPath);
if (file.exists()){
file.delete();
}else {
file.createNewFile();
}
contentValues.put(MediaStore.Images.Media.DATA,dataPath);
}
int raw = getContentResolver().update(uri,contentValues,null,null);
String bitmapPath = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+Environment.DIRECTORY_PICTURES+File.separator+"Test123.jpg";
tvShow.setText("更新共享存储的文件成功,路径为:"+bitmapPath);
ivShow.setImageResource(0);
return;
}
tvShow.setText("未找到共享存储的文件");
ivShow.setImageResource(0);
}catch (Exception e){
tvShow.setText("更新共享存储的文件出错:"+e.getMessage());
}
}
更新文件内容
非常不建议这种操作,仅供参考。
建议:1.将原来的数据读出来,追加需要添加的数据,2.删除此文件,重新创建并写入
//更新文件内容从共享存储
private void updateImgContentFromShareStorage(InputStream is){
//保存文件
try {
//这里只测试android10及以上的,android10及以下的暂未测试
//检查是否存在
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
//回调结果
String[] projection = new String[]{MediaStore.Images.Media._ID,MediaStore.Images.Media.DISPLAY_NAME,MediaStore.Images.Media.RELATIVE_PATH,MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//存在
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
//更新文件
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri,"w");
OutputStream os = new FileOutputStream(pfd.getFileDescriptor());
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes))!=-1){
os.write(bytes,0,len);
}
os.close();
tvShow.setText("修改共享存储文件完成:"+Environment.DIRECTORY_PICTURES);
ivShow.setImageURI(uri);
return;
}
tvShow.setText("共享存储文件不存在");
ivShow.setImageResource(0);
}catch (Exception e){
tvShow.setText("更新共享存储文件内容异常:"+e.getMessage());
}
}
(4)删除文件
//删除文件从共享存储
private void deleteImgFromShareStorage(){
try {
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"Test.jpg"};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//文件存在
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
int raw = getContentResolver().delete(uri,null,null);
cursor.close();
tvShow.setText("删除共享存储的文件成功");
ivShow.setImageResource(0);
return;
}
tvShow.setText("未找到共享存储的文件");
ivShow.setImageResource(0);
}catch (Exception e){
tvShow.setText("删除共享存储的文件出错:"+e.getMessage());
}
}
2.其他APP操作
这里展示读取、编辑和删除其他APP在外部存储空间和共享存储空间中写入的文件。根据Android10和Android11中的描述,内部存储空间是无法被其他应用查看和使用的,因此不进行读取操作。
2.1 外部存储空间
如果使用SAF框架无法找到外部存储空间的文件,请点击右上角,选择“显示内部存储设备”,选择Android/data/包名/file下查找文件
(1)写文件
//创建文件在存储中
//通过onActivityResult获取到写入的文件的uri,这里的文件是空的,需要将数据写入
private void writeImgToOtherExternal(){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TITLE,"newTest.jpg");
startActivityForResult(intent,2000);
}
(2)读文件
//读取文件从其他app的外部存储
//通过onActivityResult获取到文件的uri
private void readImgFromOtherExternal(){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent,2001);
}
(3)修改文件
这里的操作方式为:使用SAF框架获取uri之后,将修改之后的数据写入
注意:这里写入的是txt文件,如果需要追加数据而不是覆盖数据,则需要将数据读取后进行追加写入
//这里获取到uri之后,直接写入修改后的数据
Uri updateUri = data.getData();
try {
OutputStream outputStream = getContentResolver().openOutputStream(updateUri);
outputStream.write("测试外部存储,使用另一个app更新的".getBytes());
outputStream.close();
tvShow.setText("更新其他app的外部存储空间文件完成");
}catch (Exception e){
tvShow.setText("更新其他app的外部存储的文本异常:"+e.getMessage());
}
(4)删除文件
//这里在获取到文件的uri之后,针对文件进行删除
Uri deleteUri = data.getData();
try {
boolean delete = DocumentsContract.deleteDocument(getContentResolver(),deleteUri);
if (delete){
tvShow.setText("删除存储空间的文件成功");
}else {
tvShow.setText("删除存储空间的文件失败");
}
}catch (Exception e){
tvShow.setText("删除存储空间的文件失败");
}
2.2 共享存储空间
其实SAF框架也可以操作共享存储空间的文件,但是需要用户自行选择沐浴露和目标文件。这里采用Media api操作文件主要是为了减少用户操作,直接使用默认的逻辑处理文件。
(1)写文件
因为存储空间所有APP共用,当写入文件后,该APP自动获取此文件的读写权限,所以不存在写入其他APP的共享存储空间
(2)读文件
获取单个图片
读文件的操作和读取自身的共享存储空间的文件方式一致
使用Media api获取其他APP的共享存储空间的文件需要请求权限
这里使用的已知的图片名称和图片位置进行测试,如果不知道图片位置,可以使用SAF框架获取图片
查询所有的图片
//获取所有的图片文件
private void getAllImg(){
StringBuilder builder = new StringBuilder();
try {
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,null,null,null,null);
if (cursor!=null){
while (cursor.moveToNext()){
String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.RELATIVE_PATH));
builder.append("名称:"+name+"---路径:"+path+"\n");
}
tvShow.setText(builder.toString());
}else {
tvShow.setText("没有图片文件");
}
}catch (Exception e){
tvShow.setText("查询图片文件出错");
}
}
(3)修改文件和删除文件
这里建议使用SAF框架读写其他app的共享存储空间的文件,原因如下:
在Android10及以上,使用Media api修改、删除其他APP的文件,需要根据异常信息进行处理,需要用户手动授权才能处理;Android9及以下,可以使用File api进行操作使用SAF框架修改共享存储空间的文件的操作,与修改外部存储空间的操作一样,可参考上面的内容
使用Media api删除文件的操作,可作为参考:
与删除共享存储空间文件的操作基本一致,只是增加了对RecoverableSecurityException异常的处理,建议使用onActivityResult接收回调信息并执行之后的操作
//删除文件从其他app的共享存储
private void deleteImgFromOtherShare(){
try {
//判断条件
String selection = MediaStore.Images.Media.RELATIVE_PATH+"=? and "+MediaStore.Images.Media.DISPLAY_NAME+"=?";
//判断结果
String[] selectionArgs = new String[]{Environment.DIRECTORY_PICTURES+File.separator,"test.jpg"};
Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null,
null);
if (cursor!=null&&cursor.moveToFirst()){
//文件存在
int id = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,id);
int raw = getContentResolver().delete(uri,null,null);
cursor.close();
tvShow.setText("删除共享存储的文件成功");
ivShow.setImageResource(0);
return;
}
tvShow.setText("未找到共享存储的文件");
ivShow.setImageResource(0);
}catch (RecoverableSecurityException e){
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
startIntentSenderForResult(e.getUserAction().getActionIntent().getIntentSender(),3000,null,0,0,0);
}
}catch (IntentSender.SendIntentException e2){
}
}
}
知识点
1.SAF存储访问框架
SAF存储访问框架,即Storage Access Framework,为Android4.4引入的存储访问框架。
它主要通过文件管理器的形式,选择需要的文件,并将uri回调给用户。
具体用法如下:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivity(intent,requestCode);
并使用onActivityResult接收回调数据即可。
2.共享存储空间对应表
文件类型 | Uri地址 | 文件夹名称 |
图片 (image/*) | MediaStore.Images.Media.EXTERNAL_CONTENT_URI (content://media/external/images/media) | DIRECTORY_PICTURES |
DIRECTORY_DCIM | ||
视频 (video/*) | MediaStore.Video.Media.EXTERNAL_CONTENT_URI (content://media/external/video/media) | DIRECTORY_MOVIES |
DIRECTORY_DCIM | ||
音频 (audio/*) | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI (content://media/external/audio/media) | DIRECTORY_ALARMS |
DIRECTORY_MUSIC | ||
DIRECTORY_NOTIFICATIONS | ||
DIRECTORY_PODCASTS | ||
DIRECTORY_RINGTONES | ||
下载 (*/*) | MediaStore.Downloads.EXTERNAL_CONTENT_URI (content://media/external/downloads) | DIRECTORY_DOWNLOADS |
其他 (*/*) | MediaStore.Files.getContentUri("external") (content://media/external/file) | DIRECTORY_DOWNLOADS |
DIRECTORY_DOCUMENTS |
上表为文件类型的对应表,具体解释如下:
文件类型: 存放文件的种类,如:图片、视频、音频等,建议将对应的文件类型存放在相应的文件夹中。
uri地址: 插入数据库的uri地址,不同的文件类型使用的uri地址不一样,需要根据类型选择。
文件夹名称: 存放文件的文件夹名称,这里的文件夹位于手机存储中。
3.权限请求表
这里的Media api只测试了Android10以上的版本,Android10以下可以使用File api存储文件
操作 | 框架 | ||||
SAF框架 | Media api | File api | |||
自身 | 内部存储 | 读文件 | 无法读取 | 无法读取 | 不需要权限 |
写文件 | 不需要权限 | ||||
修改文件 | 不需要权限 | ||||
删除文件 | 不需要权限 | ||||
外部存储 | 读文件 | Android11无法读取 Android10可以读取,不需要权限 | 无法读取 | 不需要权限 | |
写文件 | 不需要权限 | ||||
修改文件 | 不需要权限 | ||||
删除文件 | 不需要权限 | ||||
共享存储 | 读文件 | 不需要权限 | 不需要权限 | Android10无法读取 Android11需要存储权限 | |
写文件 | 不需要权限 | 不需要权限 | |||
修改文件 | 不需要权限 | 不需要权限 | |||
删除文件 | 不需要权限 | 不需要权限 | |||
其他 | 外部存储 | 读文件 | Android11无法读取 Android10可以读取,不需要权限 | 无法读取 | 未进行测试 |
写文件 | |||||
修改文件 | |||||
删除文件 | |||||
共享存储 | 读文件 | 不需要权限 | 需要权限 | 未进行测试 | |
写文件 | 不需要权限 | 无必要 | |||
修改文件 | 不需要权限 | 需要权限,需要单独处理异常信息 | |||
删除文件 | 不需要权限 | 需要权限,需要单独处理异常信息 | |||
问题点
1.File api受限:
在Android9及以下版本中,File api可以正常使用;Android10以上版本中,尽量避免使用File api,可以使用SAF框架和Media api操作文件
2.Media api效率:
在实际的测试中,当存在大量的文件时,使用Media api查询文件等操作会耗费大量的时间,可以使用SAF框进行文件的操作。但是使用SAF框架会对用户的使用不够友好,因此需要根据实际情况处理。