背景

在适配分区存储之前,有三个问题需要了解下

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效率

简介

android分区存储是什么 android分区存储是什么意思_存储空间


分区存储主要将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){

        }
    }
}

android分区存储是什么 android分区存储是什么意思_Test_02

知识点

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框架会对用户的使用不够友好,因此需要根据实际情况处理。