稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Android Scoped Storage Puzzles

安卓对于文件存储这块,其实是相当混乱的,在早期的版本中对存储甚至是没有所谓的管理的,有多种方法可以操作文件存储,比如通过Java原生的方式(File/InputStream/OutputStream),以及通过ContentProvider的方式(openFile/openOutputStream)。文件的路径也是相当混乱的,有公共存储区域,App的沙盒,外部存储中的App沙盒,但其实整个文件系统中的文件都是可读的,除了系统目录,其他应用的沙盒(内部和外部)以外也都是可写的。这就造成了,安卓的文件系统特别的混乱,加之应用使用不规范,对存储造成极大的滥用,外部存储空间产生大量的垃圾文件,应用卸载后仍然存在,这也催生了一系列的清理软件。

但随着用户隐私问题越来越被重视,谷歌也开始收紧了存储权限管理。

存储权限的变迁史

强制动态申请权限

从Android 6.0(Android M, API 23)开始,存储权限除了在AndroidManifest中静态声明以外,还需要在运行时动态的申请,用户授权了后才可以使用。但这并没有本质的变化,只要用户授权了,就跟以前一样,所有文件可读,大量位置的文件也都可写,App依然可以随便乱搞。

Scoped Storage

后来到了Android 10 (Android Q, API 29)时提出了scoped storage的概念,并把权限进一步的收紧,仅留下几个公共的文件夹可写,其他地方均不可写。并且写媒体文件时(Image/Video/Audio),要用MediaStore API的方式,直接以Java的文件方式将不再可用。Android 10并未强制开启scoped storage,通过在Manifest中设置requestLegacyStorageManage=true,依然可以像以往那样搞,但到了Android 11时,就强制开启了scoped storage。

强制Scoped Storage

到了Android 11(Android R, API 30)时,就强制了,只有应用自己的沙盒可自由读写。创建媒体文件必须 通过MediaStore API。只有少数公共区域可读可写(Documents/Downloads/Pictures)等,并且这些区域只有自己创建的文件是可见和可读的。这就导致了,应用能乱搞的区间相当有限了。

细分文件种类权限

到了Android 13(Android T, API 33),更加变态了,把原来的WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE权限细分为对具体文件种类的操作,拆分为Image/Audio/Video三种媒体文件类型的读写。

注意:只有当targetSdk=33时,并且运行的设备是Android 13时,才需要做权限的细分处理。也许后面的版本就会强制了,如果强制那无论taregtSdk是多少都会强制使用细分 权限。

存储问题的适配

前面简单的介绍了安卓存储权限的变化过程,重点还是要了解一下如何适配。因为现在已经是2023年了,Android 13(Android T),API 33也快要普及了,所以就直接总结下现在应该怎么做。

动态申请存储权限

对于现在来说,动态申请权限已经是必备的技能之一了,不用多说了。就是先在应用的入口处,或者将要使用存储的地方去申请

在manifest中声明requestLegacyExternalStorage=true

用MediaStore API来操作媒体文件(Image/Audio/Video)

首先要声明具体的权限,然后用MediaStore API来操作。

申请另外一个权限MANAGE_EXTERNAL_STORAGE

注意: 这个不管你的targetSDK是多少,只要目标运行设备是Android 11,就必须 这样做。

从Android 11开始,要想看到公共区域(Documents/Downloads)中的所有文件,还必须申请另外一个权限MANAGE_EXTERNAL_STORAGE。因为理论上来说,公共区域你只能看见你自己创建的文件就够了,想看所有文件就越权了。但对于有些应用来说,如文件浏览器,或者请记录阅读器来说,肯定要能看到所有的文件。这个权限需要再次单独申请,用户授权后即可以看到公共区域的所有的文件了。

需要注意,这个权限像常规权限那样去申请是没用的,它的状态永远是denied的,所以,逻辑要这样写:

  1. 先正常申请存储相关的权限
  2. 正常权限有了后,判断Environment.isExternalStorageManager(),如果 是false则需要再次申请另外一个叫做access all files的权限,通过ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION或者ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   // after read/write permission granted
   if (!Environment.isExternalStorageManager()) {
        requestAllFilesAccess();
    }
    // ...

   private void requestAllFilesAccess() {
    Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
    intent.addCategory(Intent.CATEGORY_DEFAULT);
    Uri uri = Uri.fromParts("package", getPackageName(), null);
    intent.setData(uri);
    try {
        startActivity(intent);
    } catch (ActivityNotFoundException e) {
        Log.d(LOG_TAG, "Exception: " + e.getMessage());
    }
}

需要注意,有可能会有Activity not found exception,注意try-catch下。

因为这个权限又会导致失控,只要申请了就可以在公共区域随意创建文件了,所以谷歌对这个权限管控也是比较严的。虽然你在研发测试阶段可以申请这个权限,但是在发布阶段,要给应用商店一个使用此权限的合理理由,否则Play store会拒绝上线的(当然,如果 不在Play Store上面发布也就无所谓了)。

合理使用公共区域

作为App,最应该做的就是从业务上区分文件的属性,然后合理的选择存储空间进行存储。对于App本身运行相关的文件,如配置,如算法的模型,如数据库,缓存和日志等等应用本身运行密切相关的文件,一定不可以放在公共区域,一定要放在应用自己的沙盒内(通过context.getFilesDir), context.getCacheDir), context.getExternalFilesDir), context.getExternalCacheDir), context.openFileOutput))等来读写,应用自己的沙盒也是属于App的自由空间,你可以随意玩。

对于用户生成的文件则要放在公共区域,比如说相机产出的图片和视频,编辑软件产生的文档,这些属于用户生成文件,它的所有权是用户的,可能会通过其他软件再次编辑或者分享,这些肯定要放在公共区域的。对于媒体类Image/Audio/Video则要使用MediaStore API的方式来创建和管理。对于其他文档类型,在获得了WRITE权限 后也可以在(Documents/Downloads)中创建和管理的,绝大多数情况下,这就够了。

总的来说,在用户隐私越来越被重视的今天,安卓现在存储管理策略在保护用户隐私的前提下是能够满足开发者使用的。

参考资料

Comments