天天看点

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

前言

        针对逐渐总结下AndroidQ R的适配。

一 Android存储

        Android的文件系统分为内部存储(Internal Storage)和外部存储(External Storage)。

1.内部存储

(1)概念

        该存储区域主要存储的是数据库、SharedPreferences等系统数据。

        主要有三种类型的文件内容:

  • (1)system/:存放的系统数据
  • (2)data/:存放应用相关的数据 ,该区域的数据在应用卸载时里面的文件会全部删除。
  • (3)vendor/:用于存放厂商定制化的数据 

       这些内部存储的文件内容对普通的手机用户是无法查看的,但是开发者可以使用Android Studio自带的Device File Explore,如图:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

 或者通过adb shell进行查看。其中data/data/应用包名下的文件夹下的内容如下:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

        当执行设置的存储与缓存中的“删除数据”,会将该目录下的除去lidb的其他文件夹里面的内容给全部清空。

(2)Android提供的API

  •   1) context.getCacheDir().getAbsolutePath()

         获取的是/data/user/0/应用包名/cache,对应的上图中的data/data/应用包名/cache。当执行设置的存储与缓存中的“清空缓存”,会将这里面的内容给全部清空。

  •   2) context.getFilesDir().getAbsolutePath()

         获取的是/data/user/0/应用包名/files,对应的上图中的data/data/应用包名/files。

  •    3)Environment.getDataDirectory().getAbsolutePath()

        当然也可以通过该方法获取/data目录

        像getSharedPreferencesPath(String name)等其他API可以在使用的时候再去研究下。

2.外部存储

(1)基本概念     

         主要存储的是任意想存储的文件内容

         在Android4.4之前,外部存储指的是SD卡。Android4.4之后,主要是手机内置的外部存储和可支持的SD卡。

        该区域的内容对于普通的手机用户就可以通过手机自带的文件管理器查看,如图:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

        该部分区域有分为两个区域:

  • (1)Android文件夹下对应的区域,如果应用删除,该包名对应的文件夹的内容删除。该部分区域仅为当前应用可以访问。
  • (2)其他区域,如果应用删除,该应用创建的文件夹的内容不会删除。该区域为公共存储,如Download、DCIM、Pictures、Docments、Music等
AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

(2)Android提供的API

        在Context和Environment下都提供了API供开发者调用。

  • 1)context.getExternalFilesDir(type).getAbsolutePath()

        获取的是Android文件夹下的对应该包名的文件夹,type为传入的需要获取的文件夹的名字,返回的路径为/storage/emulated/0/Android/data/应用包名/files/文件夹名字。

        该区域下创建文件或文件夹,是不需要注册和动态申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。 

  • 2)context.getExternalCacheDir()

        获取的是Android文件夹下的对应该包名下的cache文件夹,返回的路径为/storage/emulated/0/Android/data/应用包名/cache。

当执行设置的存储与缓存中的“清空缓存”,会将这里面的内容给全部清空。

  • 3)Environment.getExternalStoragePublicDirectory(type)

        获取的是其他公共区域的文件夹,type为传入的需要获取的文件夹的名字,返回的路径为/storage/emulated/0/文件夹名字。

        注意从Android6.0开始,如果对外部存储进行读写文件,要向用户动态申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。 

        但是从Android10开始该方法已经废弃,开发者只能通过{@link Context#getExternalFilesDir(String)}, {@link MediaStore} or {@link Intent#ACTION_OPEN_DOCUMENT}来访问这些公共区域的文件夹,在后面适配的时候,在详细看下这里的使用方式。

        上面的两个type对应的类型为:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

二 Android10及以上的分区存储的适配

        从Android10开始,Google新增的分区存储(scoped storage)功能。该功能主要针对的是内置的外部存储区域。从上一节中可以看出外部存储区域主要包括两部分内容:一部分是APP私有的区域,另一部分就公有区域。

        在Android10之前,只要用户同意授权READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE之后,就可以在外部存储区域创建和读取文件,在应用卸载的时候,只要应用本身不做特殊处理,就不会删除这些文件,造成用户空间浪费;并且所有的应用都可以访问这些文件,所以回造成信息存在安全隐患,并造成用户存储空间有这些垃圾文件。

        从Android10之后,新增了分区存储的功能。对于APP私有区域,不需要用户授权,该APP就可以直接进行读写文件,其他APP也不可以访问,增加了本身应用的安全性;另外对于外部公有区域,只能访问在公共的多媒体文件夹如DCIM、Pictures等文件夹的文件;在读取文件类型的文档需要通过系统的文件选择器SAF。

1.分区存储

        在Android10中将外部存储空间的分区存储特性:

  • 1)APP的私有目录(App-specific):

        不需要读写权限就可以读写文件,可通过context.getExternalFilesDir(type)对“/storage/emulated/0/Android/data/应用包名”目录下的文件进行读写操作。但是该区域的文件会随着APP的卸载而清空。

        应用不能直接访问其他应用的私有目录。当执行设置的存储与缓存中的“删除数据”,会将该目录下的所有内容都删除。如果APP 想要在卸载时保留私有目录下的数据,要在AndroidManifest.xml中声明android:hasFragileUserData="true",这样在 APP卸载时就会有弹出框提示用户是否保留应用数据。

        访问该目录下的文件的相关API:

//返回的路径为:/storage/emulated/0/Android/media/应用包名
context.getExternalMediaDirs().getAbsolutePath()
//返回的路径为: /storage/emulated/0/Android/obb/应用包名
context.getObbDir().getAbsolutePath()
//返回的路径为:/storage/emulated/0/Android/data/应用包名/cache
context.getExternalCacheDir().getAbsolutePath()
//返回的路径为:/storage/emulated/0/Android/data/应用包名/files/对应type
context.getExternalFilesDir(type).getAbsolutePath()
           
  • 2)公共目录

        对于公共目录大体可分为两种类型:

        一种就是存放多媒体文件,通常图片存储在DCIM和Pictures、视频文件存储在DCIM、Movies、Pictures、音频文件存储在Alarms、Music、Ringtones等;APP本身可以无需任何权限就可以在这些文件夹内增删改APP本身创建的对应类型的文件,但是如果读取其他APP创建的相关文件,需要用户授权READ_EXTERNAL_STORAGE,并且如果对非APP本身创建的多媒体文件进行修改或删除还需要用户额外授权同意对文件的修改。主要通过MediaStore下的一些API进行操作具体可见后面介绍。

        另外一种存放非多媒体文件,如PDF文件、txt文件等类型。下载的文件存储在Download;文档或者其他文件存储在Documents。同多媒体文件,如果APP本身创建的非多媒体文件可以进行增删改,但是对于非APP本身创建的,即使用户授权READ_EXTERNAL_STORAGE也无法进行访问,所以通过需要启动系统的文件选择器,让用户去选择可以访问哪些文件或者文件夹。当用户授权可以访问该文件之后,无需其他额外权限(即使无READ_EXTERNAL_STORAGE),就可以对该文件进行读取、编辑或删除。主要通过SAF来启动文件选择器。

        一般要按照文件的类型放置到对应的类型的文件夹下面。

        当然也可以在获取到读写权限之后,通过直接路径也可以访问,但是直接路径访问性能会不如使用MediaStore等方式。

        在Android10及以上,将外部存储区域进行分区存储之后,就对应着Legacy模式和Filtered模式两种模式:

  • 1)Legacy模式:只要用户同意授权READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE,就可以对外部存储的区域进行读写文件,即Android10之前的方式;
  • 2)Filtered模式:采用Android10出现的分区存储特性

        对于上面两种模式的设置通过在Application标签上设置android:requestLegacyExternalStorage来区分。如果设置了应用的targetSdkVersion>=29的时候,会默认的为false,即设置为Filtered模式,开启了Android10的分区存储功能,在读取需要按照分区存储的特性进行读写文件;如果设置为true,则保持Android10之前的读写方式,但是Android11之后,停用了该功能,就是只能采用Filtered模式。

2.公共目录下多媒体文件操作的相关API

           对多媒体文件主要通过MediaStore相关API来操作。关键点就是要找到对应的Uri ,配合ContentResolver就可以找到对应的相关文件的OutputStream和InputStream,然后通过Java IO就可以读写文件。

        Uri又分为External、Internal以及可移动存储三种类型,每种文件类型对应的Uri的API可具体查看MediaStore下面,这里只列举几个:

        如Image类型如下:

Uri类型 External Uri Internal Uri 可移动存储Uri
API

MediaStore.Images.Media.

EXTERNAL_CONTENT_URI

MediaStore.Images.Media.

INTERNAL_CONTENT_URI

MediaStore.Images.Media.

getContentUri

对应的路径

 content://media/external/

images/media

    content://media/internal/

images/media

content://media/<volumeName>/

images/media

       像Audio、Video类型的只需要将Images缓存Audio、Video即可得到对应的三个Uri,而Downloads的Uri如下:

Uri类型 External Uri Internal Uri 可移动存储Uri
API

MediaStore.Downloads.

EXTERNAL_CONTENT_URI

MediaStore.Downloads.

INTERNAL_CONTENT_URI

MediaStore.Downloads.

getContentUri

对应的路径

 content://media/external/

downloads

content://media/internal/

idownloads

content://media/<volumeName>/

download

        File类型的Uri只不过改成了MediaStore.Downloads.EXTERNAL_CONTENT_URI和MediaStore.Files.Media.getContentUri。

       使用MediaStore在读写文件的时候,不管有没有获取READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,都能读写本APP在公共目录下新建的文件。若获取到READ_EXTERNAL_STORAGE权限可以读取任意的Audio、Image、Video等公共目录下的多媒体文件,但是不能读取File和Download下的有其他应用创建的非多媒体文件。

        遗留问题:需要再次验证下这个问题 

       使用模拟器 Android10、华为手机nova 7 SE Android10 、OPPO  A92s Android 11、验证下结论如下:

  • 1.不管有没有去申请READ_EXTERNAL_STORAGE权限、或者拒绝或同意READ_EXTERNAL_STORAGE权限,都可以直接使用MediaStore来读写本应用创建的文件;
  • 2.其他应用创建的文件需要申请READ_EXTERNAL_STORAGE权限,只有同意该权限,才可以访问公共目录下的所有多媒体文件,但是Docments和Downloads下的非本应用创建的非多媒体文件不可读。

 (1)使用MediaStore新建文件      

        新建文件一个是要知道要把文件存放目录的Uri, 一个是新建文件的OutputStream。 Uri就是执行通过ContentResolver执行insert操作返回的Uri,有了Uri就可以执行ContentResolver.openOutputStream()得到文件的OutputStream,而文件的属性内容通过ContentValues来赋值,代码如下:

Uri externalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
 ContentResolver resolver = context.getContentResolver();
//配置文件的属性ContentValues values = new ContentValues();
//可在Pictures的目录下在创建一个test的文件夹
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
values.put(MediaStore.Images.Media.DISPLAY_NAME, "image.png");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
//....当然这里的属性还可以增加其他配置
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
//插入
Uri insertUri = context.getContentResolver().insert(externalUri, values);
//得到OutputStream
OutputStream os = resolver.openOutputStream(insertUri);
           

        这样有了 OutputStream就可以直接使用Java的IO操作进行写文件了。注意RELATIVE_PATH这个属性是可以在公共目录下设置子文件夹,并且只能在公共目录下的创建子文件夹,否则会抛出以下异常:

Crash Info: 
java.lang.IllegalArgumentException: Primary directory Picture not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]
	at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:170)
	at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
	at android.content.ContentProviderProxy.insert(ContentProviderNative.java:481)
	at android.content.ContentResolver.insert(ContentResolver.java:1844)
	at 
           

        当然 ContentResolver不仅提供了获取到OutputStream的方法,还提供了下面获取一系列相关类型的文件的方式:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

 (2)使用MediaStore读取文件

        在读取文件的时候,也是两个关键的元素:一个是该文件的Uri,一个是该文件的InputSteam,有了InputStream就可以直接使用Java读写文件的方式进行读写文件了。

        那么多媒体文件可以直接通过ContentResolver.query来查出对应文件的Uri,如果是本APP创建的文件,可以直接通过ContentResolver.query得到文件对应的Uri,但是如果是非APP本身创建的文件,如多媒体文件,需要用户授权READ_EXTERNAL_STORAGE才可以获取到非APP本身创建的多媒体文件,而非多媒体文件是无法通过MediaStore来获取。

        首先先看如何获取该公共目录下的所有文件,代码如下:

Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
String[] projection = new String[] {MediaStore.Images.Media._ID};
Cursor cursor = resolver.query(external, projection, null, null, null);
if(cursor == null){
    return;
}
while (cursor.moveToNext()) {
    //这个就是循环输出所有的多媒体文件的Uri
   Uri uri =  ContentUris.withAppendedId(external, cursor.getLong(0)); 
    //TODO 添加相应的逻辑   
}
cursor.close();
           

        那如果是要找某个特定的文件呢?这个还没有研究明白,因为在设置selection条件的时候,发现MediaStore.MediaColumns.TITLE和MediaStore.MediaColumns.DISPLAY_NAME还是有区别的(通过几个手机验证发现不管给MediaStore.MediaColumns.TITLE赋值的字符串有没有后缀名,会自动去掉后缀名后面的内容,而MediaStore.MediaColumns.DISPLAY_NAME是所有的文件名字),这个条件语句暂时不知道该怎么写。(遗留问题:怎么设置这个条件语句呢?暂时使用的是匹配的MediaStore.MediaColumns.DISPLAY_NAME与给定的fileName相等,则该Uri就是该文件对应的Uri)

        暂时先使用前面的while循环中获取到的Uri作为要读取文件的Uri,有了Uri之后,就可以得到InputStream了,代码如下:

//前面的while循环中的uri
Uri uri = ;
try {
     InputStream is = context.getContentResolver().openInputStream(uri);
//使用Java IO来读文件的内容
           

(3)使用MediaStore修改文件

        前面多次提到获取文件的方式:       

        如果是修改本APP创建的文件,可直接通过ContentResolver.query来查出对应文件的Uri ,然后通过ContentResolver.openOutputStream(uri)得到文件的OutputStream,然后就可以直接通过Java IO进行操作文件就可以了。

        但是如果是其他APP创建的多媒体文件,首先要通过申请READ_EXTERNAL_STORAGE权限,经用户同意之后才可以访问其他应用创建的多媒体文件,然后通过ContentResolver.query来查出对应文件的Uri ,然后通过ContentResolver.openOutputStream(uri)得到文件的OutputStream,但是在进行对OutputStream进行写操作的时候,会抛出RecoverableSecurityException异常:

try {
            //对outputstream进行写操作,代码省略
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (RecoverableSecurityException e) {
            //修改其他应用创建的多媒体文件会抛出该异常
            handlerRecoverableSecurityException(context, e);
        }
           

        通常需要捕获该异常的时候,要进行弹框提示用户授权,代码如下:

/**
     * 弹框提示用户是否允许修改或删除此文件.
     * 用户操作的结果,将通过onActivityResult回调返回到APP.如果用户允许,APP将获得该Uri 的修改权限,直到设备下一次重启.
     *
     * @param context
     * @param e
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private void handlerRecoverableSecurityException(Context context, RecoverableSecurityException e) {
        try {
            ((Activity) context).startIntentSenderForResult(e.getUserAction().getActionIntent().getIntentSender(), 100, null, 0, 0, 0);
        } catch (IntentSender.SendIntentException sendIntentException) {
            sendIntentException.printStackTrace();
        }
    }
           

         用户界面如下:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

         最终通过 onActivityResult()返回给开发者,可以通过获取到的requestCode进行处理后续逻辑。

 (4)使用MediaStore删除文件

        通过ContentResolver.query来查出对应文件的Uri ,可直接通过context.getContentResolver().delete(uri, null),同样对于其他应用创建的多媒体文件要申请READ_EXTERNAL_STORAGE权限,经用户同意之后才可以删除该文件,同样在删除的时候也会抛出RecoverableSecurityException异常,同  3)使用MediaStore修改文件。

        但是该功能是Android11及以上才可以使用的API。

 (5)使用MediaStore批量处理文件

        从Android11之后,可以批量处理文件。主要API如下:

  • createWriteRequest(ContentResolver resolver,  Collection<Uri> uris, boolean value)

        向用户申请对指定的多媒体文件组的写入访问权限的请求

  • createFavoriteRequest(ContentResolver resolver,  Collection<Uri> uris, boolean value)

        向用户申请对指定的多媒体文件组标记为收藏的请求。任何具有READ_EXTERNAL_STORAGE权限的应用都可以看到被用户标记为收藏的文件

  • createTrashRequest(ContentResolver resolver,  Collection<Uri> uris, boolean value)

        向用户申请将指定的多媒体文件组放入到设备的垃圾箱的请求。有效期默认为7天之后删除

  • createDeleteRequest(ContentResolver resolver,  Collection<Uri> uris, boolean value)

        向用户申请将指定的多媒体文件组立即删除的请求。

        遗留问题:这个还没有搞懂怎么用

3.公共目录下非多媒体文件的操作的相关API

        从Android4.4就引入了存储访问框架(SAF)。包括三个元素:

  •         文档提供程序:本地存储服务或云存储服务封装到DocmentsProvider,为客户端提供和管理文件;
  •         客户端应用:客户端通过发送ACTION_CREATE_DOCUMENT、ACTION_OPEN_DOCUMENT 和 ACTION_OPEN_DOCUMENT_TREE intent 操作,就可以接收文档提供程序返回的文件;
  •         访问器:系统界面。供用户访问来选择对应的文档

        所以不仅仅针对的是非多媒体文件,多媒体文件也可以通过该API获取。

   (1)选择单个文件

        通过下面的方法可选择单个文件 

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.setType("*/*");
        startActivityForResult(intent, CODE_ACTION_OPEN_DOCUMENT);
           

        然后就会弹出界面选择对应需要操作的文件,仅仅是公共目录下的相关文件夹,如图:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

         当用户选中对应的文件之后,就会通过onActivityResult()将该文件对应的Uri返回给开发者,那么有了该Uri就可以获取到OutputStream和InputStream,然后进行操作文件了。

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK) {
            return;
        }
        switch (requestCode) {
            case CODE_ACTION_OPEN_DOCUMENT: {
                Uri uri = data.getData();
        //......省略代码
           

        只要用户选择了对应的文件(不管是不是该APP创建的文件还是其他APP创建的文件),不需要在申请其他权限(包括像在前面提出的RecoverableSecurityException异常),就可以对文件进行修改或删除。

         但是在OPPO  A92s Android 11上进行修改文件(不管是否为本APP创建的文件)的时候,抛出下面的异常:

021-08-26 16:24:13.327 24953-24953/com.android.pattern W/System.err: java.lang.IllegalArgumentException: Media is read-only
2021-08-26 16:24:13.327 24953-24953/com.android.pattern W/System.err:     at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
2021-08-26 16:24:13.327 24953-24953/com.android.pattern W/System.err:     at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:153)
2021-08-26 16:24:13.327 24953-24953/com.android.pattern W/System.err:     at android.content.ContentProviderProxy.openAssetFile(ContentProviderNative.java:705)
2021-08-26 16:24:13.327 24953-24953/com.android.pattern W/System.err:     at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1825)
           

        遗留问题:但是其他手机包括模拟器Android11都是不会抛出这个异常,这个不知道是OPPO是不是做了特殊处理,但是第二天在调试的时候,就发现这个问题没有了,不知道当时是怎么出现的这个问题(包括后来把READ_EXTERNAL_STORAGE权限打开关闭都不会复现这个现象了)。

(2)选择整个目录

        可以通过下面的方式来选择目录,代码如下:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
       startActivityForResult(intent, CODE_ACTION_OPEN_DOCUMENT_TREE);
           

        然后在弹出的界面中选择对应的文件夹之后,会弹出授权界面如下:

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

        注意从在Android 11上,无法通过SAF选择External Storage根目录、Downloads目录以及APP的私有目录(Android/data、Android/obb)的相关内容。

         同样最后的操作结果通过 onActivityResult()将该文件对应的Uri返回给开发者,开发者就可以直接该Uri进行处理,主要此时的Uri为treeuri,为类似于:content://com.android.externalstorage.documents/tree/primary%3ADownload%2F123,主要返回的是该文件夹的Uri,如果需要对里面的文件进行处理,需要通过下面的代码获取到每个文件的Uri:

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode != RESULT_OK) {
            return;
        }
        switch (requestCode) {
            case CODE_ACTION_OPEN_DOCUMENT_TREE: {
                Uri uri = data.getData();
                DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri);
                for (DocumentFile doc : documentFile.listFiles()) {
                    Uri fileUri = doc.getUri();
                    //TODO 增加相应的文件处理逻辑
                }
                break;
    //....代码省略
           

(3)新建文件

        同样可以直接通过Intent添加新建文件的一些基本信息,然后通过下面的代码:

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        //设置文件的类型
        intent.setType("text/*");
        //设置文件的名字
        intent.putExtra(Intent.EXTRA_TITLE, "123.txt");
        startActivityForResult(intent, CODE_ACTION_CREATE_DOCUMENT);
           

        调起系统的访问器,然后就会显示文件的名字以及存放的位置: 

AndroidQ R的适配(一)-分区存储的适配前言一 Android存储二 Android10及以上的分区存储的适配三 总结

        同样最后的操作结果通过 onActivityResult()将该文件对应的Uri返回给开发者,开发者就可以直接使用该Uri对文件进行增删改。

(4)删除文件

        获取了文件的Uri,就可以通过下面的代码来删除该文件:

DocumentsContract.deleteDocument(context.getContentResolver(), uri);
           

4.访问APP本身的私有目录

        在1.内部存储已经提到了通过context.getExternalFilesDir(type)等方法就可以直接在APP本身的私有目录读写文件,并且不需要任何权限。

5.访问其他APP提供的私有目录

        通过实现自定义的DocmentsProvider来实现将本APP的私有目录提供给其他APP使用,因为这次主要总结的是AndroidQ及以上的适配问题,这个等后面单独去总结。

三 总结

        之前一直没有关注Android10以上的适配,经过这周发现这部分内容也是蛮有意思的一件事情:
  • 1.Android存储分为内部存储和外部存储;
    • (1).Android的内部存储主要存放着APP创建的SharedPreferences、数据库等用户的私密数据。通常普通用户是无法直接查看的,开发者可以通过Android Studio查看本APP的内容;
    • (2).当执行设置的存储与缓存的“删除数据”的操作,主要删除的是data/data/本APP包名下的除去lidb下的其他所有内容,当用户卸载APP之后,该部分的内容被清空;当执行的是"清除缓存"的操作,仅会清空cache目录下的内容;
    • (3)主要通过context.getFilesDir()、context.getCacheDir()获取APP本身的内部存储里面的内容;
    • (4)Android的外部存储在Android4.4之前主要指的是SD卡;从Android4.4之后,外部存储通常指的是机身的外部存储和支持SD卡的SD卡。该部分的内容用户可以直接通过手机里面的文件管理查看到对应的内容;
  • 2.Android10之前,只要用户授权应用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限之后,开发者根据文件的直接路径是可以任意访问该手机上的外部存储空间的内容;所以就容易引起用户信息安全隐患和APP可以在用户的外部存储空间任意存储信息,造成用户空间的浪费;
  • 3.Android10开发,Google推出了分区存储(Scope Storage)的概念。所谓的分区存储就是将用户的外部存储空间分成了APP的私有目录和公共目录。
    • (1)对于APP的私有目录,主要是指的Android/data/应用包名该目录。可以通过context.getExternalFilesDir(type)获取到该空间,进行读写文件。对于该目录,不需要用户授权任何权限,开发者就可以在该目录下读写文件。当然也不能访问其他APP的私有目录的内容;
    • (2)对于APP的私有目录,当执行设置的存储与缓存的“删除数据”的操作,该目录下的内容会被全部清空,当然如果用户卸载APP的话,该部分的内容会被清空;当执行的是"清除缓存"的操作,仅会清空cache目录下的内容;
    • (3)对于公共目录是指的外部存储空间的几个特定的目录,主要根据文件类型分为存放多媒体文件的目录和存放非多媒体文件的目录,通常要按照文件的类型存放到对应的目录中;
    • (4)对于存放多媒体文件的目录如:存储图片的DCIM、Pictures;存储视频的DCIM、Movies、Pictures;存放音频的Alarms、Music等;
      • 1)对于存放多媒体文件目录下的文件,如果是本APP创建的文件,不需要用户授权任何权限就可以直接对文件进行增删改;但是如果是其他APP创建的文件,必须用户同意授权READ_EXTERNAL_STORAGE权限才可以读这些多媒体文件,但是如果是修改或着删除,还需要用户在授权同意修改的权限;
      • 2)通常通过MediaStore来对多媒体文件进行读写操作。有两个基本要素:文件的Uri和读写操作的InputStream/OutputStream。
      • 3)Uri通常不同的文件类型会对应三种Uri:external、internal以及可移动存储的,通常需要通过ContentReslover的query或insert得到多媒体文件的Uri,有个Uri就可以通过ContentReslover来获取InputStream和OutputSteam,最后通过Java IO来对文件进行增删改;
    • (5)对于存放非多媒体文件的目录:如存放下载文件的Downloads、存放文档的Docments等。
      • 1)本APP当然可以直接使用MediaStore来对文件进行增删改,同样也是不需要用户授权任何权限就可以完成该操作;但是需要通过SAF框架来访问其他应用创建的非多媒体文件,是无法通过MediaStore对其他应用创建的非多媒体文件进行删除或修改(MediaStore是无法查询到其他应用创建的非多媒体文件);
      • 2)在使用SAF框架对非多媒体文件进行操作的时候,通常就是通过Intent来打开系统访问器,然后由用户来选择让APP来访问哪个文件或者目录,一旦用户选择了文件,就不再需要任何权限就可以对文件进行修改或删除
    • (6)本APP是无法访问其他APP的私有目录,但除非其他APP提供访问权限;
      • 1)实现自定义的DocmentsProvider来将私有目录提供给其他APP访问;
  • 4.当然也可以直接通过文件的绝对路径来对文件进行修改,但是性能肯定不如MediaStore;
  • 5.Android10提供了android:requestLegacyExternalStorage,可以让开发者关闭分区存储的模式:设置为true,即保持Android10之前的存储模式;设置为false,则采用分区存储模式,但是Android11该功能废弃,已经强制打开分区存储模式;
  • 6.Android11开始不在提供WRITE_EXTERNAL_STORAGE权限
  • 7.Android11当使用SAF选择目录的时候,无法通过SAF选择External Storage根目录、Downloads目录以及APP的私有目录(Android/data、Android/obb);

        终于勉强汇总完了,后面在细化下,继续加油。