一篇文章搞懂android存储目录结构(维护更新)

写在前面:

在半个月前使用livery从服务器下载hong.apk(即命名为小红.apk),使用代码:

InternetClient.getInstance().downloadApkInService(url,"hong.apk","download/apk");

做升级功能时,下载的apk文件放在了外部存储目录

/storage/emulated/0/Android/data/packagename/files/download/apk/hong.apk

下载的时候却一直提示下载出错,查看日志找到关键的log:

Android:open failed: ENOENT (No such file or directory)

经过步步排除错误,step1:检查了AndroidManifest.xml中的权限配置,是否配置了读写文件权限(权限有了)
step2:考虑到我使用android系统6.0以上(好吧其实是android10 pixel),是否需要动态申请权限呢(那就参考之前文章动态拿一下权限,为了保证确实不是权限问题:索性给应用一个最高权限)
step3:不是权限问题,只能跟踪源码了(当然这部分代码其实是我自己写的),排查了创建文件以及目录,最后发现,在下载文件的时候调用:

File downloadFile = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

我看到这个'''getExternalStoragePublicDirectory```被标记为过时deprecated,查了一下,这就是关键点:

本身这个方法没有问题,但是在android Q版本就有问题了,在新的Android SDK 29编译的时候,Studio会提示Environment.getExternalStorageDirectory()过时,
要用Context#getExternalFilesDir代替,Android Q以后Environment.getExternalStorageDirectory()返回的路径无法直接访问,这叫android Q的独立存储,它对应用存储空间访问进行了限制,
目的为了让用户更好地控制文件并限制文件混乱,Android Q 改变了应用程序访问设备外部存储上文件的方式

知道了这一点,结合手机系统版本,那么这个问题解决,并且也一并解决了livery本身下载文件存在的混乱问题(更正的版本为1.1.22),

同时也总结一下android存储目录,在android专栏目录下竟然没有该系列的文章,这次算是补上了,以后再变化,是系列问题那就维护更新


一:存储分类

对于Android存储目录,我总结成一张思维导图,图中展示了Android存储的目录,接下来我们详细分析每一个目录

一篇文章搞懂android存储目录结构(维护更新)_第1张图片

图1:Android存储目录结构Bgwan

1.内部存储

内部存储位于系统中很特殊的一个位置,对于设备中每一个安装的 App,系统都会在 data/data/packagename/xxx 自动创建与之对应的文件夹。如果你想将文件存储于内部存储中,那么文件默认只能被你的应用访问到,且一个应用所创建的所有文件都在和应用包名相同的目录下。也就是说应用创建于内部存储的文件,与这个应用是关联起来的。当一个应用卸载之后,内部存储中的这些文件也被删除。对于这个内部目录,用户是无法访问的,除非获取root权限。

String fileDir = this.getFilesDir().getAbsolutePath();  
String cacheDir = this.getCacheDir().getAbsolutePath();

版权声明CopyRight:

本内容作者:sunst,转载或引用请 标明出处 ,违者追究法律责任!!!

一般情况下,我们获取到的路径为data/data/packagename/xxx,小米手机下面打印出来的结果如下:

fileDir:/data/user/0/com.sunsta.hong/files
cacheDir:/data/user/0/com.sunsta.hong/cache

对于内部存储路径,我们一般通过以下两种方式获取,内部存储空间的获取都需要使用Context:

context.getFileDir()

对应内部存储的路径为: data/data/packagename/files,但是对于有的手机如:华为,小米等获取到的路径为:data/user/0/packagename/files

context.getCacheDir()

对应内部存储的路径为: data/data/packagename/cache,但是对于有的手机如:华为,小米等获取到的路径为:data/user/0/packagename/cache应用程序的缓存目录,该目录内的文件在设备内存不足时会优先被删除掉,所以存放在这里的文件是没有任何保障的,可能会随时丢掉。

context.openFileOutput(String fileName, int mode);

这个方法打开的就是data/data/packagename/files/目录下的文件 ,刚开始看到这个方法的时候还在好奇“我就传了个文件名,它是怎么找到这个文件的(/hong.png)” ,再这个目录下面如果没有fileName这儿名儿的文件 那么系统就会创建一个

2.外部存储

针对于外部存储比较容易混淆,因为在Android4.4以前,手机机身存储就叫内部存储,插入的SD卡就是外部存储,但是在Android4.4以后的话,就目前而言,现在的手机自带的存储就很大,现在Android10.0的话,有的手机能达到256G的存储,针对于这种情况,手机机身自带的存储也是外部存储,如果再插入SD卡的话也叫外部存储,因此对于外部存储分为两部分:SD卡和扩展卡内存
我们通过一段代码来获取手机的外部存储目录,我们用的测试手机是三星G4,带有插入SD卡的:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  
File[] files = getExternalFilesDirs(Environment.MEDIA_MOUNTED);  
for (File file : files) {  
Log.e("file_dir", file.getAbsolutePath());  
}  
}

对于以上代码,打印的结果如下:

/storage/emulated/0/Android/data/com.sunsta.hong/files/mounted  
/storage/extSdCard/Android/data/com.sunsta.hong/file/mounted

打印出两行目录,第一行目录是机身自带的外部存储目录,目录结构为:/storage/emulated/0/Android/data/packagename/files
第二行是存储卡的目录结构,路径为:/storage/extSdCard/Android/data/packagename/files

(1):扩展外部存储

此目录路径需要通过context来获取,同时在app卸载之后,这些文件也会被删除。类似于内部存储。

getExternalCacheDir()

对应外部存储路径:/storage/emulated/0/Android/data/packagename/cache

getExternalFilesDir(String type)

对应外部存储路径:/storage/emulated/0/Android/data/packagename/files

getExternalFilesDir的参数可以传以下几种:String?: The type of files directory to return. May be null for the root of the files directory or one of the following constants for a subdirectory:

android.os.Environment#DIRECTORY_MUSIC,
android.os.Environment#DIRECTORY_PODCASTS,
android.os.Environment#DIRECTORY_RINGTONES,
android.os.Environment#DIRECTORY_ALARMS,
android.os.Environment#DIRECTORY_NOTIFICATIONS,
android.os.Environment#DIRECTORY_PICTURES,
android.os.Environment#DIRECTORY_MOVIES.

This value may be null.下面# getExternalStorageDirectory(过时中会有介绍)

例如我们传一个

getExternalFilesDir(Environment.DIRECTORY_PICTURES);

得到的路径如下:

/storage/emulated/0/Android/data/yourPackageName/files/Pictures

(2):SD卡存储

SD卡里面的文件是可以被自由访问,即文件的数据对其他应用或者用户来说都是可以访问的,当应用被卸载之后,其卸载前创建的文件仍然保留。
对于SD卡上面的文件路径需要通过Environment获取,同时在获取前需要判断SD的状态:

MEDIA_UNKNOWN SD卡未知
MEDIA_REMOVED SD卡移除
MEDIA_UNMOUNTED SD卡未安装
MEDIA_CHECKING SD卡检查中,刚装上SD卡时
MEDIA_NOFS SD卡为空白或正在使用不受支持的文件系统
MEDIA_MOUNTED SD卡安装
MEDIA_MOUNTED_READ_ONLY SD卡安装但是只读
MEDIA_SHARED SD卡共享
MEDIA_BAD_REMOVAL SD卡移除错误
MEDIA_UNMOUNTABLE 存在SD卡但是不能挂载,例如发生在介质损坏
String externalStorageState = Environment.getExternalStorageState();  
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)){  
//sd卡已经安装,可以进行相关文件操作  
}

(Android10过时或不建议使用)getExternalStorageDirectory()

对应外部存储路径:/storage/emulated/0

(Android10过时或不建议使用)getExternalStoragePublicDirectory(String type)

获取外部存储的共享文件夹路径,参数可以传以下几种如:

DIRECTORY_ALARMS 闹钟铃声文件类型
DIRECTORY_MUSIC 音乐目录
DIRECTORY_PICTURES 图片目录
DIRECTORY_NOTIFICATIONS 音频文件通知铃声
DIRECTORY_PODCASTS 播客音频
DIRECTORY_MOVIES 电影目录
DIRECTORY_RINGTONES 手机铃声音频
DIRECTORY_DOWNLOADS 下载目录
DIRECTORY_DCIM 相机拍照或录像文件的存储目录
DIRECTORY_DOCUMENTS 文件文档目录
String externalStoragePublicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath();

前面提到的app下载失败,该方法过时,所以我们在android 10以上只需要替换为 getExternalFilesDir(null);
得到的路径如下

/storage/emulated/0/Android/data/packagename/files

以上便是获取相机DCIM目录,对应获取的路径为:/storage/emulated/0/DCIM。

对于公共存储目录: 我们可以在外部存储上新建任意文件夹(应该都知道,啰嗦一句,6.0及之后的系统需要动态申请权限),这些目录的内容不会随着应用的卸载而消失。如:

Environment.getExternalStorageDirectory(): /storage/emulated/0  
Environment.getExternalStoragePublicDirectory(""): /storage/emulated/0  
Environment.getExternalStoragePublicDirectory("test"): /storage/emulated/0/test  
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES): /storage/emulated/0/Pictures

3.系统存储目录

getRootDirectory()

对应获取系统分区根路径:/system

getDataDirectory()

对应获取用户数据目录路径:/data

getDownloadCacheDirectory()

对应获取用户缓存目录路径:/cache

二:相关概念区别

1.getFileDir()和getCacheDir()区别

这两个都位于内部存储目录/data/data/packagename/下面,位于同一级别,前者是file目录下面,后面是cache目录下。

2.getFileDir()和getExternalFilesDir(String type)区别

前者位于内部存储目录/data/data/packagename/file下面,后者位于外部存储目录/storage/emulated/0/Android/data/packagename/files下面,它们都存在于应用包名下面,也就是说属于app应用的,所以当app卸载后,它们也会被删除的。
前面提到的apk下载升级功能,我们从服务器端下载的app需要放到外部存储目录下面,而不是内部存储目录,因为内部存储目录的空间很小。另外我也做了相关测试,如果将apk放到内部存储目录file下面的话,安装时会出现问题,提示解析包出错。

3.清除数据和清除缓存的区别

在app中有清除数据和清除缓存这两个概念,那么这两者分别清除的是什么目录下面的数据呢?

4.清除数据

清除数据清除的是保存在app中所有数据,就是上面提到的位于packagename下面的所有文件,包含内部存储(/data/data/packagename/)和外部存储(/storage/emulated/0/Android/data/packagename/)。当然除了SD卡上面的数据,SD卡上面的数据当app卸载之后还会存在的。

5.清除缓存

缓存是程序运行时的临时存储空间,它可以存放从网络下载的临时图片,从用户的角度出发清除缓存对用户并没有太大的影响,但是清除缓存后用户再次使用该APP时,由于本地缓存已经被清理,所有的数据需要重新从网络上获取。为了在清除缓存的时候能够正常清除与应用相关的缓存,请将缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下。

三:关于Fileprovider StrictMode禁(2020年10月17补充维护内容)

android存储目录结构了解了,可能有人对于Fileprovider不太理解,其实本系列内容我在17年Android7.0(Android N)适配教程,拍照-选择系统相册已经中总结过了,这里再重新提一下:

在Android7.0系统上,android框架强制执行了StrictMode API政策禁止向你的应用外公开file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。

其实说白了,就是高版本对权限要求更高了,当你想要共享一个创建的文件,如果是7.0以上版本,解决方案也如:

应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider

这里我来进行一个简单的步骤配置,再一边讲解:

1.Fileprovider配置于讲解

(1)在AndroidManifest.xml配置


    
authorities:一个标识,在当前系统内必须是唯一值,一般用包名。
exported:表示该 FileProvider 是否需要公开出去。
granUriPermissions:是否允许授权文件的临时访问权限。这里需要,所以是 true。

(2)编写base_fileprovider_takephoto.xml参考



    
        

    

path代表要共享的目录。。。name是一个标示,随便,自己看的懂就行,这是一个简单的配置内容,当然还有其它也介绍出来

root-path 对应DEVICE_ROOT,也就是File DEVICE_ROOT = new File("/"),即根目录,一般不需要配置。
files-path对应 content.getFileDir() 获取到的目录。
cache-path对应 content.getCacheDir() 获取到的目录
external-path对应 Environment.getExternalStorageDirectory() 指向的目录。
external-files-path对应 ContextCompat.getExternalFilesDirs() 获取到的目录。
external-cache-path对应 ContextCompat.getExternalCacheDirs() 获取到的目录。

这里是一个对应关系:(也对应文中图1内容

一篇文章搞懂android存储目录结构(维护更新)_第2张图片

图2:Android存储目录结构对应Fileprovider

我们还是以base_fileprovider_takephoto.xml的内容来解释

使用了files-path标签,创建了images为name, picture/为我需要共享的路径

File dirFile = new File(Context.getFilesDir(), "picture");//创建picture目录
File imageFile = new File(dirFile, "default_image.jpg");//新文件
Uri contentUri = getUriForFile(getContext(), "pakagename.fileprovider",imageFile);//安全的Uri

我们创建了一个picture目录,以此中方式共享我们的文件default_image.jpg,则打印后的Uri一定是这样的:

content://pakagename.fileprovider/images/default_image.jpg

我们拿到这个Uri就可以共享我们的内容了(说的优点啰嗦),其它的是一样的,下面举几个实用案例就清楚了

2.使用案例参考

FileProvider 可以申请临时读写文件权限,以增强安全性,所以 Content URI 生成完成后,还需要申请临时访问权限。

通常直接通过 intent.setFlags 即可完成,具体的权限名称为:Intent.FLAG_GRANT_READ_URI_PERMISSION 和 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。

(1):微信朋友圈多图分享

微信官方不支持朋友圈直接多图分享,Android N之前的版本由于没有强制限制 file:// 的使用

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI"));
intent.setAction("android.intent.action.SEND_MULTIPLE");

// List存储多张图片地址
ArrayList localArrayList = new ArrayList<>();
for (int i = 0, size = localPicsList.size(); i < size; i++) {
    localArrayList.add(Uri.parse("file:///" + localPicsList.get(i)));
}

intent.putParcelableArrayListExtra("android.intent.extra.STREAM", localArrayList);
intent.setType("image/*");
intent.putExtra("Kdescription", desc);
context.startActivity(intent);

这种方式可以直接绕过微信官方 SDK 实现多图分享,无需手动选择图片,唯一的问题就是没有分享结果的回调,也就是说无法判断是否分享成功,这在大部分情况下依然是一种可以接受的方案,但是Android N 之后这种方式就不行了,原因就是 Android N 不允许 file://Uri 的方式在不同的 App 间共享文件,但是如果换成 FileProvider 的方式,经试验发现依然是无效的,所以在 Android N 上无法实现朋友圈直接多图分享,虽然这是一个失败的案例情景,但并不是说我们方法不对,只是可能腾讯有自己的机制,就限制我们用SDK

那么举个有用的列子:使用,以安装apk为例

(2):关于应用内apk自动安装说明

参考:Livery使用《关于应用内apk自动安装说明》

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
Uri uri;
File file = new File(saveFolder, updateSaveName);
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
    uri = FileProvider.getUriForFile(activity, pakagename+".provider"), file);
} else {
    uri = Uri.fromFile(file);
}
String type = "application/vnd.android.package-archive";
intent.setDataAndType(uri, type);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) {
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
activity.startActivityForResult(intent, 69);

如果:Android 10.0的手机会抛出异常:java.io.FileNotFoundException: open failed: ENOENT (No such file or directory),则在AndroidManifest.xml中 配置(一般来说都有配置)

android:requestLegacyExternalStorage="true"

四:存储解决方案

1.Environment.getExternalStorageDirectory() is deprecated过时的替代方案

Environment.getExternalStorageDirectory()可以改成:

getExternalFilesDir(null);

得到的路径如下:

/storage/emulated/0/Android/data/packagename/files

这个目录会在应用被卸载的时候删除,而且访问这个目录不需要动态申请STORAGE权限。

https://user-gold-cdn.xitu.io/2019/12/4/16ed0297bcab87f3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

2.targetsdkversion大于等于23,

按照以往在外部存储上创建目录的方法肯定一直返回false。这种情况在Android6.0之前都是不存在的,6.0在权限管理方面更加全面,所以注意:在读写外置存储的时候不仅要在manifest中静态授权,还需要在代码中动态授权。

动态配置权限:

  
  

3.文件的读写可以借助Livery 中FileUtils来操作,这里不做过多介绍

4.android 10 沙箱机制可以参考:

对于Android Q的沙箱机制,可以参考这一篇文章,介绍的非常详细和清楚(maybe 翻qiang) android 10沙箱机制的介绍

致谢与参考:

https://juejin.cn/post/6844904013515718664

感谢评论区阿斯同学指出,文章参考以上链接作者内容

你可能感兴趣的:(android,android)