总所周知Android上的存储权限一直在更改,从Android增加file provider,到Android10增加分区存储,Google对于存储权限管理越来越严格。我们聊一下Android上的存储Api兼容性适配。
 
1. 应用存储空间 
应用保存数据的方式有如下:
文件和媒体数据可以保存在“应用专属存储空间”和“公共存储空间之中” 
短数据或者偏好设置可以通过sharePreference保存 
数据库 
 
外部存储 
以前的手机是存在SDcard的,但目前很多手机都取消了SDcard,Android上引入了映射机制来创建虚拟的SDcard,我们通过文件管理器看到的路径storage/emulated/0就是虚拟SDcard,也就是我们俗称的“外部存储空间”或者“公共存储空间”
app申请的读写权限请求都是申请的外部存储空间权限
 
内部存储 
内存存储也就是本app的专享目录,其他app是无法访问的,适合存储敏感文件。 
通过系统api访问到的路径:/data/user/0/app_packageName/... 
对应的真是目录:/data/date/app_packageName/...
分区存储 
分区存储实际上就是外部存储空间中建一个app对应目录,本app无须申请权限就可以访问,如果申请读写权限,意味着申请外部空间所有访问权限,在Android10之前,分区存储目录是有可能被其他app访问到的。从Android11开始,其他app是无法访问的,有人也称之为沙盒模式。  官网 对它的介绍
2.权限变更记录 
Android6.0引入动态权限 
申请读写权限需要在Manifest.xml中申明:
<uses-permission android:name ="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name ="android.permission.READ_EXTERNAL_STORAGE" /> 
 
Android6.0之前只需申明就可以进行文件读写,Android6.0之后需要进行动态权限申请,也就是要用户同意了才行。权限申请回调很烦人,可以去Github上看一下RxPermission,EasyPermission之类的库。
Android7.0权限变更 
自从Android7.0开始之后,禁止使用  file://这类型的URI,尝试直接使用这种URI会触发 FileUriExposedException,建议使用FileProvider 来创建content://这类型URI
Android10尝试引入分区存储 
上面已经讲述了分区存储其实就是外部存储空间的app目录,本app无须权限就可以访问。 
在Android10上可以禁用:android:requestLegacyExternalStorage="true",但是在Android11上就不管用了哦,系统会自动忽略啊哈哈
Google首次尝试引入分区存储,我当时听了人都傻了=.=  真能折腾啊
 
3. 使用FileProvider 
Setup1: 
在res目录下新建xml目录,在xml目录下新建filepaths.xml文件
<?xml version="1.0" encoding="utf-8"?> <paths >          <files-path         name="int_root"         path="/" />          <cache-path         name="app_cache"         path="/" />          <external-path         name="ext_root"         path="/" />          <external-files-path         name="ext_pub"         path="/" />          <external-cache-path         name="ext_cache"         path="/" />          <external-media-path         name="ext_media"         path="/"/> </paths > 
 
Setup2: 
在AndroidManifest.xml中申明:
<application     android:allowBackup="true"      android:icon="@mipmap/ic_launcher"      android:label="@string/app_name"      android:requestLegacyExternalStorage="true"      android:roundIcon="@mipmap/ic_launcher_round"      android:supportsRtl="true"      android:theme="@style/Theme.AndroidStorageDemo" >     <provider         android:name="androidx.core.content.FileProvider"          android:authorities="com.alexlu.androidstorage.fileProvider"          android:enabled="true"          android:exported="false"          android:grantUriPermissions="true" >         <meta-data             android:name="android.support.FILE_PROVIDER_PATHS"              android:resource="@xml/filepaths"  />     </provider> </application>           
 
注意这里的android:authorities="com.alexlu.androidstorage.fileProvider"修改为自己的包名,
Setup3: 
使用的时候注意与配置文件中注册的包名一致:
val  file = File(xxpath ) val  uri = FileProvider . getUriForFile(context , "com.alexlu.androidstorage.fileProvider" , file ) ;
 
为什么要建一个xml文件? 
其实就是将以前的file://解析成自定义的名称,xml中的name参数就是你自定义的路径名称 
path填写文件夹名称,如果为空或者/,代表所有路径
xml中不同方法的意义 
以下是一一对应的:
<files-path/> --> Context . getFilesDir()  <cache-path/> --> Context . getCacheDir()  <external -path/> --> Environment . getExternalStorageDirectory()  <external -files-path/> --> Context . getExternalFilesDir(String)  <external -cache-path/> --> Context . getExternalCacheDir()  <external -media-path/> --> Context . getExternalMediaDirs()  
 
我们看一下不同Api获取的路径是什么样子的:
Log . d(TAG,Environment . getExternalStorageDirectory() .absolutePath)Log . d(TAG,Environment . getRootDirectory() .absolutePath)Log . d(TAG,Environment . getDataDirectory() .absolutePath)Log . d(TAG,Environment . getDownloadCacheDirectory() .absolutePath)if  (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {    Log . d(TAG,"getDownloadCacheDirectory():" +Environment . getStorageDirectory() .absolutePath)      } Log . d(TAG,this.filesDir.absolutePath)Log . d(TAG,this.cacheDir.absolutePath)Log . d(TAG,this.codeCacheDir.absolutePath)Log . d(TAG,"externalCacheDir:" +this.externalCacheDir?.absolutePath)this.externalMediaDirs.forEach {     Log . d(TAG,"externalMediaDirs:" +it.absolutePath)      } Log . d(TAG,"getExternalFilesDir:" +this.getExternalFilesDir(null ) ?.absolutePath)
 
以上都标明了其输出路径,storage/emulated/0代表的就是外部存储空间,/data/user/0代表的就是内存存储空间,包含包名说明就是app私有目录或者分区存储目录。
我们通过以上方法就可以愉快的创建文件啦
使用示例: 
使用内部私有目录 
App内部的私有空间比较小,建议对其谨慎操作,读写之前查询其可用容量。大文件建议保存在外部分区目录下。
 
获取剩余可用空间:  建议查看StorageStatsManager 
获取内存私有目录下的cache文件路径
fun  getAppCachePath(context : Context, subDir :String?=null ) :String{    val  path = StringBuilder(context .cacheDir .absolutePath )      subDir?.let  {         path.append(File . separator).append(it).append(File . separator)     }     val  dir = File(path .toString () )     if  (!dir.exists() ) dir.mkdir()      return path.to String()  } 
 
获取内部私有目录下的files文件路径
fun  getAppFilePath(context : Context, subDir :String?=null ) : String {    val  path = StringBuilder(context .filesDir .absolutePath )      subDir?.let  {         path.append(File . separator).append(it).append(File . separator)     }     val  dir = File(path .toString () )     if  (!dir.exists() ) dir.mkdir()      return path.to String()  } 
 
可以选择创建子目录,其中subDis代表子目录文件夹名称
我们保存两张图片到其目录下:
val  file = File(FileUtil.getAppCachePath (this ) ,"${System.currentTimeMillis()}.jpg" )val  file2 = File(FileUtil.getAppFilePath (this ,"image" ) ,"${System.currentTimeMillis()}.jpg" )OperatePicUtil . saveBitmap2File(this ,bitmap ,file ) OperatePicUtil . saveBitmap2File(this ,bitmap ,file2 ) 
 
(OperatePicUtil工具类可以查看Demo )
我们在Android12虚拟机上可以看到保存没有问题:
使用外部公共目录 
Environment.getExternalStorageDirectory()在Android10之后标识为废弃,意思就是这个API很好用但是我不想让你用,实际测试在Android12上依旧有用。
 
fun  getExternalPicturesPath(subDir :String?=null ) : String{    val  path = StringBuilder(Environment.getExternalStorageDirectory () .absolutePath)         .append(File . separator)         .append(Environment.DIRECTORY_PICTURES)     subDir?.let  {         path.append(File . separator).append(it).append(File . separator)     }     val  dir = File(path .toString () )     if  (!dir.exists() ) dir.mkdir()      return path.to String()  } fun  getExternalDownloadPath(subDir :String?=null ) : String{    val  path = StringBuilder(Environment.getExternalStorageDirectory () .absolutePath)         .append(File . separator)         .append(Environment.DIRECTORY_DOWNLOADS)     subDir?.let  {         path.append(File . separator).append(it).append(File . separator)     }     val  dir = File(path .toString () )     if  (!dir.exists() ) dir.mkdir()      return path.to String()  } 
 
使用分区存储目录 
接口定义:分别获取file,cache,media目录,type代表子目录名称,可以为null
@Override public  File getExternalFilesDir (String  type  )  {    return  mBase.getExternalFilesDir(type ); } @Override public  File[] getExternalFilesDirs (String  type  )  {    return  mBase.getExternalFilesDirs(type ); } @Override public  File getExternalCacheDir ( )  {    return  mBase.getExternalCacheDir(); } @Override public  File[] getExternalCacheDirs ( )  {    return  mBase.getExternalCacheDirs(); } @Override public  File[] getExternalMediaDirs ( )  {    return  mBase.getExternalMediaDirs(); } 
 
获取分区存储目录:
     fun  getExternalAppFilePath (context: Context ,subDir: String ?=null )  :String{    val  path = context.getExternalFilesDir(subDir)?.absolutePath     val  dir = File(path.toString())     if  (!dir.exists()) dir.mkdir()     return  path.toString() } 
 
保存图片到分区存储目录下:
val  file = File(FileUtil.getExternalAppFilePath (this ) ,"${System.currentTimeMillis()}.jpg" )PicturesUtil . saveBitmap2File(context  = this ,bitmap  = bitmap ,file  = file ) 
 
我们可以打印文件的路径为:
/sdcard/ Android/data/ com.alexlu.androidstorage/files
 
App设置界面中的“清除存储空间”和“清除缓存” 
清除存储空间:清除app所有保存的文件和偏好设置,数据库等信息,但是不包括外部公共目录的文件,相当于App卸载重新安装了 
清除缓存:清除外部分区存储和内部私有存储中的 cache 目录 
 
其他文件操作 
比如其他类型的文件写入,保存文档、音频文件,插入图片到系统相册。具体用法请查看Github Demo ,如有错误,请大家指出,欢迎大家点个Star