IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 分区存储 -> 正文阅读

[移动开发]Android 分区存储

1.背景

Google于 2019年9月3日发布了Android10 release版本,为了更好的保护用户数据并限制设备冗余文件增加,Android 10版本变更了设备外部存储访问方式,外部存储新特性称为分区存储(Scoped Storage), 分区存储遵循以下三个原则对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件

文件更好的归属: 系统记录文件由哪个应用创建,应用不需要存储权限即可以访问应用自己创建文件

应用数据保护: 添加外部存储应用私有目录文件访问限制, 应用即使申请了存储权限也不能访问其他应用外部存储私有目录文件

用户数据保护: 添加pdf、office、doc等文件的访问限制,用户即使申请了存储权限也不能访问其他应用创建的pdf、office、doc等文件

2.Android存储目录

Android的存储目录被分为内部存储和外部存储两个部分,对于外部存储进行了重新设计,外部存储被分为应用私有目录以及共享目录两个部分。

在这里插入图片描述

①内部存储:

  • getFilesDir() - 应用内部存储 放在data/data/packagename/files/

  • getCacheDir() - 应用内部存储 放在data/data/packagename/cache/

    主要用于存储应用的私密数据,用户在手机上是看不见这块区域的。在进行存储的过程中不需要存储权限,当应用被卸载后数据会被删除。应用内部存储目录访问方式与之前Android版本一致,可以通过file path获取资源

②外部存储的私有目录:

  • getExternalFilesDir - 放在外部存储Android/data/packagename/files/ 外部存储私有目录

    • 应用卸载就会删除
    • 5.0及以上不需要WRITE_EXTERNAL_STORAGE READ_EXTERNAL_STORAGE
    • 不安全,别的应用可以写入数据到此目录
    • Media扫描不出来,不会出现在相册
  • getExternalCacheDir - 存放临时缓存数据 放在外部存储Android/data/packagename/cache/

    ? 对应设置选项设置->应用->应用详情里面的"清除数据"与"清除缓存“选项"

    ? 应用外部存储私有目录访问方式与Android10之前版本一致,可以通过file path获取资源

**③外部存储共享目录:**外部存储共享目录 除了外部存储的私有目录以外的目录,都是共享目录。程序保存在共享目录中的数据,在应用被删除后,仍然保留。存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录

  • getExternalStorageDirectory - 在sd卡目录mnt/sdcard,在Android10 以前版本中 可以通过file path获取资源,获取资源的时候需要获取

    存储权限。卸载后保留创建的文件

  • 在Android10 版本之后,共享目录文件需要通过MediaStore API或者Storage Access Framework方式访问

    ? MediaStore API在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限

    ? MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过

    ? ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常;

    ? MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访问;

3.文件迁移

3.1文件迁移是将应用共享目录文件迁移到应用私有目录或者Android10要求的media集合目录

①针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过File path方式访问文件资源,降低适配成本

②允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到Android10要求的media集合目录

img

3.2 文件访问兼容性适配

共享目录文件不能够通过File path方式读取,需要使用MediaStore API或者Storage Access Framework框架进行访问

img

4.Android10 以后使用MediaStore 进行文件的存储操作

MediaStore API 简介

MediaStore API

系统会自动扫描外部存储,添加文件到系统已定义的Images、Videos、Audio files、Downloaded files集合中,Android 10通过MediaStore.Images、MediaStore.Video、MediaStore.Audio、MediaStore.Downloads 访问共享目录文件资源

媒体类型Uriuri 定义的常量默认创建目录允许创建目录Mime Type
Imagecontent://media/external/images/mediaMediaStore.Images.Media.EXTERNAL_CONTENT_URIPicturesDCIM
Pictures
Pictures图片(image/*)
Videocontent://media/external/video/mediaMediaStore.Video.Media.EXTERNAL_CONTENT_URIMoviesDCIM,
Movies
视频(video/*)
Audiocontent://media/external/audio/mediaMediaStore.Audio.Media.EXTERNAL_CONTENT_URIMusicAlarms,
Music,
Notifications,
podcast

Ringtones
音频(audio/*)
Downloadcontent://media/external/downloadsMediaStore.Downloads.EXTERNAL_CONTENT_URIDownloadDownloadNA
Filecontent://media/external/MediaStore.Files.getContentUri(“external”)DocumentsDocumentsfile/*

4.1 创建/保存文件

通过 ContentResolver.insert(Uri url,ContentValues values) 插入到对应的目录中,该方法会返回一个 Uri,通过对该 Uri 进行文件流的操作。

① insert()方法的第一个参数表示你要插入哪一个目录的Uri ,就是上表中定义的Uri常量。

② insert()方法的第二个参数,ContentValues ,首先构造一个 ContentValues 对象,

  • ContentValues 的 key 值可以通过 MediaStore.XXX.Media.YYY 获取到
    XXX: 对应的媒体类型
    YYY: 对应的字段常量

  • RELATIVE_PATH

    注意RELATIVE_PATH需要targetVersion>=29 ,表示在公共媒体路径下创建子目录relativePath。例如上文我们将这个图片保存到了 Pictures/DemoPicture 文件夹下,如果不设置这个值,则会被默认保存到对应的媒体类型的文件夹下,例如,图片文件(mimeType = image/*)会被保存到 Pictures(Environment#DIRECTORY_PICTURES) 中,需要注意的是,不能将文件放置到不对应的顶级文件夹下,比如将一个 mimeType 为 audio/mpeg 放大 Pictures 这样的行为是不被允许的,也就是如果设置 MIME_TYPE = audia/* 并将 RELATIVE_PATH 设置为 Environment#DIRECTORY_PICTURES 这样是会 Throw IllegalArgumentException 的

    例如:

    //创建一个相对路径在vido路径下面, key表示video路径下,value =Movies/ucamera 表示我在movies路径下面创建了一个ucamra子目录。允许创建的目录如上面的表格。
    values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES +"/ucamera");
    

? Environment 类中定义了一些我们常用的目录的字段;

    public static String DIRECTORY_MUSIC = "Music";

    public static String DIRECTORY_PODCASTS = "Podcasts";

    public static String DIRECTORY_RINGTONES = "Ringtones";

    public static String DIRECTORY_ALARMS = "Alarms";
    public static String DIRECTORY_NOTIFICATIONS = "Notifications";
    public static String DIRECTORY_PICTURES = "Pictures";
    public static String DIRECTORY_MOVIES = "Movies";

    public static String DIRECTORY_DOWNLOADS = "Download";
    public static String DIRECTORY_DCIM = "DCIM";
    public static String DIRECTORY_DOCUMENTS = "Documents";
    public static String DIRECTORY_SCREENSHOTS = "Screenshots";
    public static String DIRECTORY_AUDIOBOOKS = "Audiobooks";

创建返回Uri示例代码如下:

  /***
     *
     * @param context  上下文
     * @param videoName  视频文件名字带有后缀名如 :"123.mp4"
     * @param mineType  视频格式类型如:"video/mp4"
     * @param subDir   创建的子目录如:"/ucam"
     * @return  uri
     */
    public Uri getSaveToGalleryVideoUri(Context context, String videoName, String mineType, String subDir) {
        ContentValues values = new ContentValues();
        //视频的名字
        values.put(MediaStore.Video.Media.DISPLAY_NAME,  videoName);
        //视频类型;
        values.put(MediaStore.Video.Media.MIME_TYPE, mineType);
        //修改时间
        values.put(MediaStore.Video.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //大于等于10 的版本设置子目录
            values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + subDir);
        }else{
            //数据库表中列字段存储的是数据的绝对路径;android 10 之前,将绝对路径设置到这个key中;这个字段可能在后面的数据库查询可能会用到;
            values.put(MediaStore.MediaColumns.DATA, getpath());
        }
        Uri  mInsert = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
        return mInsert;

    }

4.2使用我们插入之后的Uri 进行我们的一些存储操作

4.2.1 设置视频录制(MediaRecorder.setOutputFile(FileDescriptor fd)所需要的 FileDescriptor 实例对象
    /** 
     *  获取视频录制我们需要设置给MediaRecorder.setOutputFile(FileDescriptor fd )的FileDescriptor 实例对象
     *  Uri uri 插入成功后的uri 
     * @return
     * @throws
     */
    public static FileDescriptor getSaveToGalleryVideoOutputStream(Uri uri) throws FileNotFoundException {
            ParcelFileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "w");
            FileDescriptor mfileDescriptor = fileDescriptor.getFileDescriptor();
            return  mfileDescriptor;
        }

    }
//上面获取的FileDescriptor 对象, 我们在视频录制时就可以设置 mMediaRecorder.setOutputFile(fd);实现视频录制
    FileDescriptor fd = getSaveToGalleryVideoOutputStream(uri);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //输出的目录设置;
            mMediaRecorder.setOutputFile(fd);
        }
4.2.2 Cursor query(@RequiresPermission.Read @NonNull Uri uri,@Nullable String[] projection, @Nullable String selection,@Nullable String[] selectionArgs, @Nullable String sortOrder) 方法查询数据库中的数据操作。

参数解释:

参数类型释义
uriUri提供检索内容的 Uri,其 scheme 是content://
projectionString[]返回所要查询的数据库列的集合,如果传递 null 则所有列都返回(效率低下)
selectionString过滤条件,即 SQL 中的 WHERE 语句(但不需要写 where 本身),如果传 null 则返回所有的数据
selectionArgsString[]如果你在 selection 的参数加了 ? 则会被本字段中的数据按顺序替换掉
sortOrderString用来对数据进行排序,即 SQL 语句中的 ORDER BY(单不需要写ORDER BY 本身),如果传 null 则按照默认顺序排序(可能是无序的)

Cursor的一些理解:

Cursor是怎么一回事,怎么取它的数据,和它的数据为什么那么取。

img

上面的表代表的是一个cursor对象。当我们用query方法查询到的是一个指向cursor对象第一行的前面一行数据,即上图中的空行。所以我们使用cursor.moveToNext()方法时,是指向cursor的第一行数据,而不是第二行。如何得到cursor中的数据呢?当我们使用了cursor.movetonext()后,cursor指向了第一行数据,由于是数据库中的表,所以每一行每一列都有对应的数据类型和列名。当我们要取出cursor对象中的一个对应的数据,首先我们要先确定它的数据类型,用cursor.getxxx()。xxx代表的是你要取出的数据的数据类型,当前我们要取出第一行的“列名2”的数据。就可以用cursor.getString()。然后我们确定了数据类型后,肯定要确定列名,所以我们要得到cursor对象中的列名,用cursor.getColumnIndex(“列名”)来获得。这样我们要取出第一行第二列的数据时,就可以用cursor.getString(cursor.getColumnIndex(“列名2”))来获得。取第二行的数据只要继续cursor.moveToNext(),到了第二行后,跟取第一行的数据是一样的。

关于 Cursor
在你理解和使用 Android Cursor 的时候你必须先知道关于 Cursor 的几件事情:
Cursor 是每行的集合。
使用 moveToFirst() 定位第一行。
你必须知道每一列的名称。
你必须知道每一列的数据类型。
Cursor 是一个随机的数据源。
所有的数据都是通过下标取得。
关于 Cursor 的重要方法:

close() 
关闭游标,释放资源
copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) 
在缓冲区中检索请求的列的文本,将将其存储
getColumnCount() 
返回所有列的总数
getColumnIndex(String columnName) 
返回指定列的名称,如果不存在返回-1
getColumnIndexOrThrow(String columnName) 
从零开始返回指定列名称,如果不存在将抛出IllegalArgumentException 异常。
getColumnName(int columnIndex) 
从给定的索引返回列名
getColumnNames() 
返回一个字符串数组的列名
getCount() 
返回Cursor 中的行数
moveToFirst() 
移动光标到第一行
moveToLast() 
移动光标到最后一行
moveToNext() 
移动光标到下一行
moveToPosition(int position) 
移动光标到一个绝对的位置
moveToPrevious() 
移动光标到上一行

查询代码的示例:

    /***
     * 查询video 表中的所有数据
     * @param cr
     */
public void query(ContentResolver cr) {
        //按照修改时间进行排序;
        String sortOrder = MediaStore.Images.Media.DATE_MODIFIED + " DESC";
        //查询video表获取cursor对象;
        Cursor c = cr.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null, null, sortOrder);
       //获取总共有多少列数据
        int colnum = c.getColumnCount();
        StringBuilder res = new StringBuilder();
        //遍历获取列的字段名字和列的索引进行打印;因为默认cursor
        for (int i = 0; i < colnum; i++) {
            //"\t" 表示一个tab
            res.append(c.getColumnName(i)).append(c.getColumnIndex(c.getColumnName(i))).append("\t");
        }
        res.append("\n");
       //将cursor 移动到数据的第一行
        while (c.moveToNext()) {
            for (int i = 0; i < colnum; i++) {
                try {
                   // 打印每一列中的第一行数据
                    res.append(c.getString(i)).append("\t");
                } catch (Exception e) {
                    res.append(c.getType(i)).append("\t");
                }

            }
            
            res.append("\n");
        }
    //移动光标到第一行
        c.moveToFirst();
        //log打印出我们查询的数据库表数据;
        Log.d("deng", "zzzh createCursor: addImage " + cr + " count " + c.getCount() + " \n" + res);

    }

通过整个的查询, 我截取了数据库表的一部分,可以看到数据库列字段 _data 中存放的是绝对路径, 其所在的列索引为33;

在这里插入图片描述

4.23通过uri 去获取缩略图
 /***
     * 录制视频的保存路径;
     * 需要先想明白需要存的数据是属于app私有的还是需要分享的,如果是app私有的,存在getExternalFilesDir()返回的文件夹下,
     * 也就是Android/data/包名/files/文件夹;如果是需要分享的,
     * 需要采用媒体库(MediaStore)的方式来存取,后面会讲怎么存取。
     * 需要指出的是在分区存储模型下存取共享媒体文件是不需要存储权限的,
     * 而旧的存储模型是需要存储权限的。
     * @param context
     * @return
     */
    private String getVideoFilePath(Context context) {
        final File dir = context.getExternalFilesDir(null);
        return (dir == null ? "" : (dir.getAbsolutePath() + "/"))
                + System.currentTimeMillis() + ".mp4";
    }


    /***
     * 我们通过cursor 查询出来的path 可以进行new file 操作;
     * @param cr
     * @param uri
     * @return
     */
  //获取缩略图;
    public Bitmap getVideoThumbnail(ContentResolver cr, Uri uri) {
        query(cr);
        Cursor cursor = null;
        String path;
        try {
            //数据库表中_data列的字段存储着数据的路径;
            String[] proj = {MediaStore.Video.Media.DATA};
            //查询获取_data这一列的cursor对象,
            cursor = cr.query(uri, proj, null, null, null);
            StringBuilder res = new StringBuilder();
            //获取到列的索引
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
            res.append(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)+"").append("\t\t");
            //表示指向查询到的列的第一行数据;
            cursor.moveToFirst();
            res.append("\n");
            //取出这一列,第一行数据;
            path = cursor.getString(column_index);
            res.append(cursor.getString(column_index)).append("\t\t");
            Log.e("deng", "createCursor: getvideothumbnail "  + cr + " count " + cursor.getCount() + " \n" + res);

        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND);
    }
4.2.4通过uri可以得到Bitmap
    public  Bitmap getBitmap(ContentResolver rs ,Uri uri) throws FileNotFoundException {
        ParcelFileDescriptor fd = rs.openFileDescriptor(uri, "r");
            Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
            return bitmap;
    }
4.2.5 删除文件

如果需要编辑修改甚至删除其他应用的文件,则需要申请 WRITE_EXTERNAL_STORAGE 权限。如果当应用没有 WRITE_EXTERNAL_STORAGE 权限时,去修改其他 App 的文件时,则会 throw java.lang.SecurityException: xxxx has no access to content://media/external/images/media/243 的异常

当应用拥有了 WRITE_EXTERNAL_STORAGE 权限后,当修改其他 App 的文件时,会 throw 另一个 Exception android.app.RecoverableSecurityException: xxxxxx has no access to content://media/external/images/media/243

如果我们将这个 RecoverableSecurityException 给 Catch 住,并向用户申请修改该图片的权限,用户操作后,我们就可以在 onActivityResult 回调中拿到结果进行操作了

4.2.5.1 删除应用自己创建的文件, 不需要权限,

    /***
     * 根据文件名字,  找到对应文件名字的这一行数据中列 _id 这个字段的数据, 生成Uri提供给删除使用;
     * @param contentResolver
     * @param name
     */
   //_display_name  删除名字 为 这个的数据 1609835774439.mp4
    public void delete(ContentResolver contentResolver,String name) {
        try {

            //根据日期降序查询
            String sortOrder = MediaStore.Images.Media.DATE_MODIFIED + " DESC";
            //查询条件 根据名字进行查询;
            String selection = MediaStore.Images.Media.DISPLAY_NAME + "='" + name + "'";
            //查询获取cursor,所以相当于找带有name的这一行,只显示两列 分别为_id _display_name ;
            cursor = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Video.VideoColumns._ID,MediaStore.Images.Media.DISPLAY_NAME}, selection, null, sortOrder);

            if (cursor != null ) {
                //获取字段为_id 的这个字段列的索引
                int columnId = cursor.getColumnIndex(MediaStore.Images.Media._ID);
                //移动到第一行数据
                cursor.moveToFirst();
                //获取第一行列字段_id 下面的数据
                int mediaId = cursor.getInt(columnId);
                //根据_id 生成uri;
                Uri uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaId);
                contentResolver.delete(uri,null,null);
            }
        }catch (Exception e){
            e.toString();
        }
        cursor.close();
    }

4.2.5.2 删除其他应用所创建的文件;

在 Android 11 上运行请使用以下方法:

  1. 使用MediaStore.createWriteRequest() 和MediaStore.createTrashRequest()为应用的写入或删除请求创建待定 intent,然后通过调用该 intent 提示用户授予修改一组文件的权限。
  2. 评估用户的响应:
    • 如果授予了权限,请继续修改或删除操作。
    • 如果未授予权限,请向用户说明您的应用中的功能为何需要该权限。

详细了解如何使用 Android 11 中提供的这些方法执行批量操作

在 Android 10 上运行

如果您的应用以 Android 10(API 级别 29)为目标平台,请停用分区存储,继续使用适用于 Android 9 及更低版本的方法来执行此操作。

在 Android 9 或更低版本上运行

请使用以下方法:

  1. 按照请求应用权限中所述的最佳做法,请求 WRITE_EXTERNAL_STORAGE 权限。
  2. 使用 MediaStore API 修改或删除媒体文件。

参考文档

4.2.6 存储图片
/***
     *
     * @param uri  写入图片数据的路径地址Uri
     * @param bytes 图片的字节数组;
     */
    public static void  SavePic(Uri uri, byte[] bytes) {

        if (uri != null) {
            //使用流将内容写入该uri中即可
            OutputStream outputStream = null;
            try {
                outputStream = MyApplication.getContext().getContentResolver().openOutputStream(uri);
                outputStream.write(bytes);
                outputStream.flush();
                outputStream.close();

            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

测试示例

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-23 10:54:40  更:2021-07-23 10:56:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/20 10:01:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码