安卓存储目录分为 内部存储 和 外部存储。 内部存储的目录为 /data/ 目录, 其中 内部存储 在未root的手机上是无法查看的。
要了解APP的存储目录结构,我们先从 app开始安装时谈起。
在系统开始安装一个apk时,系统会先将apk文件复制到 data/app/ 目录下,再解析apk信息,然后dexopt优化操作。
内部存储在未root的手机上虽然无法查看的,但在Android Studio 3.1.4 中可以借助 Device File Explorer 工具来查看 内部存储空间。 这个工具在 AS 的右下角, 如果不小心移除了,可以来下面 右边 这张图里找到。
打开这个工具,我们可以看到 data/app/ 目录下有许多包名,这些都是 已经安装到手机的 app的 包名。如下图所示:
以MIUI 8.2为例, 这些不同包名的内部存储了两大块内容,一个so文件;另一个是oat文件。
so和oat 这两部分文件是系统能运行此app的基础,app的机器代码都保存在这里。 为安全着想,没有root的手机即使借助 Device File Explorer 也不能查看 oat里存储的 odex文件。
ART和Dalvik都算是一种Android运行时环境,或者叫做虚拟机,用来解释dex类型文件。但是ART是安装时解释,Dalvik是运行时解释。 4.4 以前的版本使用的是 dalvik 虚拟机,在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。
dalvik: 在5.0 以前 的 Dalvik时代,app每次启动时,系统都需要通过 即时编译器jit(Just-In-Time实时编译) 将dex文件或odex翻译成能被虚拟机加载的native code , 最终产物是相同名称的 dey文件(表示这是一个优化过的dex),这样使得 dalvik 虚拟机的 app启动速度很慢。
ART: 5.0 及之后的 ART虚拟机中,完全抛弃了dalvik的JIT, 使用了AOT直接在安装时将其完全翻译成native code.这一技术的引入,使得虚拟机执行指令的速度又一重大提升。
oat: 是 AOT 在安装apk时 生成的 native code,对应的文件后缀为 *.oat(实际上是一个自定义的elf文件,里面包含的都是本地机器指令)), o是optimize(优化)的缩写,a是android的缩写,t是runTime的意思,oat 是在apk安装时通过dexopt工具将dex文件优化成二进制格式的文件,然后再通过AOT(Ahead-Of-Time 预先编译)生成 能被art虚拟机执行的机器 吗,从而加快app的启动速度。
在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。
上面我们介绍了 安装apk 后的相关目录,在我们运行一个app后,代码里所涉及到的文件 如 数据库、sp、webview缓存等都保存到了 data/data/包名/ 目录下。
"data/app/包名" 和 "data/data/包名" 都是应用私有目录,只允许应用内部访问, 其它应用的进程是无法访问的。
注意:当用户卸载 App 时,系统自动删除 data/data 目录下对应包名的文件夹及其内容。
具体目录 如下图:
Android SDK 提供有如下方法可以获取并操作 内部存储 空间下应用 私有目录文件 的方法,都位于 Application Context 中,供开发者直接调用:getFilesDir()、getCacheDir()、deleteFile()、fileList()、Environment.getDataDirectory();
外部存储目录的路径为 "Android/data/包名" ,主要是考虑到内部存储空间容量有限,普通用户不能直接直观地查看目录文件等其他原因,Android 在外部存储空间中也提供有特殊目录供应用存放私有文件。
一般设备都有内置 SD 卡,同时也提供外部 SD 卡拓展,可能对应路径的目录名有所差异。
值得注意的是,与内部存储空间的应用私有目录不同的是:
第一,默认情况下,系统并不会自动创建外部存储空间的应用私有目录。只有在应用需要的时候,开发人员通过 SDK 提供的 API 创建该目录文件夹和操作文件夹内容。
第二,自 Android 7.0 开始,系统对外部存储目录中 应用私有目录的访问权限进一步限制。其他 App 无法通过 file:// 这种形式的 Uri 直接读写 非自己app外部私有目录下的文件内容,而是需要通过 FileProvider 访问。(关于这个内容,接下来再写一篇文章专门说说 7.0 的适配问题,欢迎关注我的微信公众号:安卓笔记侠。)
第三,宿主 App 可以直接读写内部存储空间中的应用私有目录;而在 4.4 版本开始,宿主 App 才可以直接读写外部存储空间中的应用私有目录,使开发人员无需在 Manifest 文件中或者动态申请外部存储空间的文件读写权限。
而相同点在于:同属于应用私有目录,当用户卸载 App 时,系统也会自动删除外部存储空间下的对应 App 私有目录文件夹及其内容。
同样,Android SDK 中也提供有便捷的 API 供开发人员直接操作外部存储空间下的应用私有目录:getExternalFilesDir()、getExternalCacheDir()、Environment.getExternalStorageDirectory();
区别是,在4.4之后通过 Environment 访问外部 存储空间时需要读写存储卡权限。
注意:对于外部存储空间下的应用私有目录文件,由于普通用户可以自由修改和删除,开发人员在使用时,一定要做好判空处理和异常捕获,防止应用崩溃退出!
注意:访问外部存储空间 的 非应用私有目录 时记得申请读写权限!
外部存储空间已经为用户默认分类出一些公共目录。开发人员可以通过 Environment 类提供的方法直接获取相应目录的绝对路径,Environment.getExternalStoragePublicDirectory(String type);
传递不同的 type 参数类型即可:Envinonment 类提供诸多 type 参数的常量,比如:
DIRECTORY_MUSIC:Music
DIRECTORY_MOVIES:Movies
DIRECTORY_PICTURES:Pictures
DIRECTORY_DOWNLOADS:Download
以第一个常量为例,音乐类别的公共目录绝对路径为:/storage/emulated/0/Music。如果你使用文件管理器打开设备的外部存储空间的话,均可以看到这些公共目录文件夹。
一般来说,利用两种应用私有目录和公共目录便能够存储应用中需要保存的数据和文件。如果这些还不够的话,那一定是你的开发姿势不对。在 Code Review 的前提下,如果还是不够的话,还可以在外部存储空间自由创建其他目录,通过这个方式获取外部存储空间的绝对路径,然后操作文件:Environment.getExternalStorageDirectory();
使用应用私有目录保存应用相关数据,使用公共目录保存应用无关数据(共享数据)。无论哪种情况,都需要做好数据分类保存,便于清除等统一管理。随便打开手机上的几个应用,不难发现,很多应用都包含一个清理缓存的功能。事实上,开发人员清理的就是应用相关数据,也就是应用私有目录下的文件。
考虑到外部存储空间上的内容可能被用户手动删除,或者卸载拓展 SD 卡等不可控因素,操作前记得使用 Environment 类提供的 API 方法判断容量是否充足、文件是否存在等情况,做好异常捕获,减少应用崩溃率。相信这一定是一个良好的习惯。
在Android 7.0以前, 可以使用file://uri的方式访问外部存储中的 其它应用 的私有目录的文件,但是这有个问题,就是即使不是你自身应用产生的文件,只要知道对方的uri则就可以调用到,这样在安全性上就产生了风险。
所以Android 7.0后新增了对文件跨进程访问的限制,这个限制会造成,如果使用file://uri的方式访问,则会出现android.os.FileUriExposedException的异常。
FileProvider 的注册 有两大步骤
1、 在 manifest.xml 中注册
...
...
...
2、在 res/xml 目录下 添加 共享目录标识 文件
...
其中 paths 的 标签 可以配置多组,每一组也有多种选择,具体规则如下:
:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir()
所获取的目录路径;
:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir()
所获取的目录路径;
:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory()
所获取的目录路径;
:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null)
所获取的目录路径;
:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();
可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 name 和 path 两个属性。
其中,path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。
注意:path 属性值不能使用具体的独立文件名,只能是目录名。
而 name 属性用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。
如果我们需要分享的文件位于同级别目录下不同的子目录中,就需要添加多个子元素逐一指定要分享的文件目录,或者共享他们通用的父目录也行。
添加完共享目录后,再在
元素中使用
元素将 res/xml 中的 path 文件与注册的 FileProvider 链接起来:
经过这两大步,我们已经完成了 FileProvider 注册。 这样其它 app 就能 使用 我们app FileProvider 所代表的目录下的文件。 具体使用的步骤大概如下 :
1、FileProvider.getUriForFile 构造 contentUri
2、申请 uri 访问权限
3、startActivity 来启动此 contentUri;
举几个例子,7.0 及以后的 跨进程访问文件都需要通过 FileProvider来实现,
a、7.0的 apk 更新功能: apk安装进程需要访问你的app外部存储中私有目录下的apk文件
File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");
Uri apkUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID+".myprovider", apkFile);
Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(installIntent);
b、调用系统拍照,并保存到外部存储目录中: 相机进程 需要将图片数据写入到 你的app外部存储中私有目录下 的 文件中
String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".myprovider", outputFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);