目录介绍
-
01. 磁盘沙盒的概述
- 1.1 我的项目背景阐明
- 1.2 沙盒作用
- 1.3 设计指标
-
02.Android 存储概念
- 2.1 存储划分介绍
- 2.2 机身外部存储
- 2.3 机身内部存储
- 2.4 SD 卡内部存储
- 2.5 总结和梳理下
-
03. 计划根底设计
- 3.1 整体架构图
- 3.2 UML 设计图
- 3.3 要害流程图
- 3.4 接口设计图
- 3.5 模块间依赖关系
-
04. 一些技术要点阐明
- 4.1 应用队列治理 Fragment 栈
- 4.2 File 文件列表
- 4.3 不同版本拜访权限
- 4.4 拜访文件操作
- 4.5 10 和 11 权限阐明
- 4.6 分享文件给第三方
- 4.7 关上图片资源
- 4.8 为何须要 FileProvider
- 4.9 跨过程 IPC 通信
-
05. 其余设计实际阐明
- 5.1 性能设计
- 5.2 稳定性设计
- 5.3 debug 依赖设计
01. 磁盘沙盒的概述
1.1 我的项目背景阐明
- app 展现在数据量多且刷新频繁的状况下,为晋升用户体验,通常会对上次已有数据做内存缓存或磁盘缓存,以达到疾速展现数据的目标。缓存的数据变动是否正确、缓存是否起到对应作用是 QA 须要重点测试的对象。
- android 缓存门路查看办法有哪些呢?将手机关上开发者模式并连贯电脑,在 pc 控制台输出 cd /data/data/ 目录,应用 adb 次要是不便测试(删除,查看,导出都比拟麻烦)。
- 如何简略疾速,傻瓜式的查看缓存文件,操作缓存文件,那么该我的项目小工具就十分有必要呢!采纳可视化界面读取缓存数据,不便操作,直观也简略。
1.2 沙盒作用
-
能够通过该工具查看缓存文件
- 疾速查看
data/data/ 包名
目录下的缓存文件。 - 疾速查看
/sdcard/Android/data/ 包名
下存储文件。
- 疾速查看
-
对缓存文件解决
- 反对查看 file 文件列表数据,关上缓存文件查看数据详情。还能够删除缓存对应的文件或者文件夹,并且敌对反对分享到内部。
- 可能查看缓存文件批改的信息,批改的工夫,缓存文件的大小,获取文件的门路等等。都是在可视化界面上解决。
1.3 设计指标
-
可视化界面展现
-
多种解决文件操作
- 针对 file 文件夹,或者 file 文件,长按能够呈现弹窗,让测试抉择是否删除文件。
- 点击 file 文件夹,则拿到对应的文件列表,而后展现。点击 file 直到是具体文件 (文本,图片,db,json 等非 file 文件夹) 跳转详情。
-
一键接入该工具
- FileExplorerActivity.startActivity(MainActivity.this);
- 开源我的项目地址:https://github.com/yangchong2…
02.Android 存储基本概念
2.1 存储划分介绍
-
存储划分介绍
- 手机空间存储划分为两局部:1、机身存储;2、SD 卡内部存储
- 机身存储划分为两局部:1、外部存储;2、内部存储
-
机身外部存储
- 放到 data/data 目录下的缓存文件,个别应用 adb 无奈查看该门路文件,公有的。程序卸载后,该目录也会被删除。
-
机身内部存储
- 放到 /storage/emulated/0/ 目录下的文件,有共享目录,还有 App 内部公有目录,还有其余目录。App 卸载的时候,相应的 app 创立的文件也会被删除。
-
SD 卡内部存储
- 放到 sd 库中目录下文件,内部凋谢的文件,能够查看。
2.2 机身外部存储
-
想一下平时应用的长久化计划:这些文件都是默认放在外部存储里。
- SharedPreferences—-> 实用于存储小文件
- 数据库 —-> 存储构造比较复杂的大文件
-
如果包名为:com.yc.helper,则对应的外部存储目录为:/data/data/com.yc.helper/
- 第一个 ”/” 示意根目录,其后每个 ”/” 示意目录宰割符。外部存储里给每个利用依照其包名各自划分了目录
- 每个 App 的外部存储空间仅容许本人拜访(除非有更高的权限,如 root),程序卸载后,该目录也会被删除。
-
机身外部存储个别存储那些文件呢?大略有以下这些
- cache–> 寄存缓存文件
- code_cache–> 寄存运行时代码优化等产生的缓存
- databases–> 寄存数据库文件
- files–> 寄存个别文件
- lib–> 寄存 App 依赖的 so 库 是软链接,指向 /data/app/ 某个子目录下
- shared_prefs–> 寄存 SharedPreferences 文件
-
那么怎么通过代码拜访到这些门路的文件呢?代码如下所示
context.getCacheDir().getAbsolutePath() context.getCodeCacheDir().getAbsolutePath() //databases 间接通过 getDatabasePath(name)获取 context.getFilesDir().getAbsolutePath() //lib,临时还不晓得怎么获取该门路 //shared_prefs 间接通过 SharedPreferences 获取
2.3 机身内部存储
-
寄存地位,次要有那些?如下所示,根目录下几个须要关注的目录:
- /data/ 这个是后面说的公有文件
- /sdcard/ /sdcard/ 是软链接,指向 /storage/self/primary
- /storage/ /storage/self/primary/ 是软链接,指向 /storage/emulated/0/
-
也就是说 /sdcard/、/storage/self/primary/ 真正指向的是 /storage/emulated/0/
-
上面这个是用 adb 查看 /storage/emulated/0 门路资源
a51x:/storage $ ls emulated self a51x:/storage $ cd emulated/ a51x:/storage/emulated $ ls ls: .: Permission denied 1|a51x:/storage/emulated $ cd 0 a51x:/storage/emulated/0 $ ls // 省略 /storage/emulated/0 下的文件
-
- 而后来看下 /storage/emulated/0/ 存储的资源有哪些?如下,分为三局部:
-
第一种:共享存储空间
- 也就是所有 App 共享的局部,比方相册、音乐、铃声、文档等:
- DCIM/ 和 Pictures/–> 存储图片
- DCIM/、Movies/ 和 Pictures–> 存储视频
- Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/ 和 Ringtones/–> 存储音频文件
- Download/–> 下载的文件
- Documents–> 存储如.pdf 类型等文件
-
第二种:App 内部公有目录
- Android/data/—> 存储各个 App 的内部公有目录。
- 与外部存储相似,命名形式是:Android/data/xx——>xx 指利用的包名。如:/sdcard/Android/data/com.yc.helper
-
第三种:其它目录
- 比方各个 App 在 /sdcard/ 目录下创立的目录,如支付宝创立的目录:alipay/,高德创立的目录:amap/,腾讯创立的目录:com.tencent.xx/ 等。
-
那么怎么通过代码拜访到这些门路的文件呢?代码如下所示
- 第一种:通过 ContentProvider 拜访,共享存储空间中的图片,视频,音频,文档等资源
-
第二种:能够看出再 /sdcard/Android/data/ 目录下生成了 com.yc.helper/ 目录,该目录下有两个子目录别离是:files/、cache/。当然也能够抉择创立其它目录。App 卸载的时候,两者都会被革除。
context.getExternalCacheDir().getAbsolutePath(); context.getExternalFilesDir(null).getAbsolutePath();
- 第三种:只有拿到根目录,就能够遍历寻找其它子目录 / 文件。
2.4 SD 卡内部存储
- 当给设施插入 SD 卡后,查看其目录:/sdcard/ —> 仍然指向 /storage/self/primary,持续来看 /storage/,能够看出,多了 sdcard1,软链接指向了 /storage/77E4-07E7/。
-
拜访形式,跟获取内部存储 -App 公有目录形式一样。
File[] fileList = context.getExternalFilesDirs(null);
- 返回 File 对象数组,当有多个内部存储时候,存储在数组里。返回的数组有两个元素,一个是自带内部存储存储,另一个是插入的 SD 卡。
2.5 总结和梳理下
- Android 存储有三种:手机外部存储、手机自带内部存储、SD 卡扩大内部存储等。
-
外部存储与内部存储里的 App 公有目录
-
相同点:
- 1、属于 App 专属,App 本身拜访两者无需任何权限。
- 2、App 卸载后,两者皆被删除。
- 3、两者目录下减少的文件最终会被统计到 ” 设置 -> 存储和缓存 ” 里。
-
不同点:
- /data/data/com.yc.helper/ 位于外部存储,个别用于存储容量较小的,私密性较强的文件。
- 而 /sdcard/Android/data/com.yc.helper/ 位于内部存储,作为 App 公有目录,个别用于存储容量较大的文件,即便删除了也不影响 App 失常性能。
-
-
在设置里的 ” 存储与缓存 ” 项,有革除数据和革除缓存,两者有何区别?
-
当点击 ” 革除数据 ” 时:
- 外部存储 /data/data/com.yc.helper/cache/、/data/data/com.yc.helper/code_cache/ 目录会被清空
- 内部存储 /sdcard/Android/data/com.yc.helper/cache/ 会被清空
-
当点击 ” 革除缓存 ” 时:
- 外部存储 /data/data/com.yc.helper/ 下除了 lib/,其余子目录皆被删除
- 内部存储 /sdcard/Android/data/com.yc.helper/ 被清空
- 这种状况,相当于删除用户 sp,数据库文件,相当于重置了 app
-
04. 一些技术要点阐明
4.1 应用队列治理 Fragment 栈
-
该磁盘沙盒 file 工具页面的组成部分是这样的
- FileExplorerActivity + FileExplorerFragment(多个,file 列表页面) + TextDetailFragment(一个,file 详情页面)
-
针对磁盘 file 文件列表
FileExplorerFragment
页面,点击 file 文件 item- 如果是文件夹则是持续关上跳转到 file 文件列表
FileExplorerFragment
页面,否则跳转到文件详情页面
- 如果是文件夹则是持续关上跳转到 file 文件列表
-
解决工作栈返回逻辑。举个例子当初列表
FileExplorerFragment
当作 B,文件详情页面当作 C,宿主 Activity 当作 A。也就是说,点击返回键,顺次敞开了 fragment 直到没有,回到宿主 activity 页面。再次点击返回键,则敞开 activity!- 可能存在的工作栈是:关上 A1-> 关上 B1-> 关上 C1
- 那么点击返回键按钮,返回敞开的程序则是:敞开 C1-> 敞开 B1-> 敞开 A1
-
Fragment 回退栈解决形式
- 第一种计划:创立一个栈 (先进后出),关上一个
FileExplorerFragment
列表页面 (push
一个fragment
对象到队列中),敞开一个列表页面 (remove
最下面那个fragment
对象,而后调用FragmentManager
中popBackStack
操作敞开fragment
) - 第二种计划:通过 fragmentManager 获取所有 fragment 对象,返回一个 list,当点击返回的时候,调用 popBackStack 移除最下面一个
- 第一种计划:创立一个栈 (先进后出),关上一个
-
具体解决该场景中回退逻辑
- 首先定义一个双端队列 ArrayDeque,用来存储和移除元素。外部应用数组实现,能够当作栈来应用,性能十分弱小。
-
当开启一个 fragment 页面的时候,调用 push(相当于 addFirst 在栈顶增加元素)来存储 fragment 对象。代码如下所示
public void showContent(Class<? extends Fragment> target, Bundle bundle) { try {Fragment fragment = target.newInstance(); if (bundle != null) {fragment.setArguments(bundle); } FragmentManager fm = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fm.beginTransaction(); fragmentTransaction.add(android.R.id.content, fragment); //push 等同于 addFirst,增加到第一个 mFragments.push(fragment); //add 等同于 addLast,增加到最初 //mFragments.add(fragment); fragmentTransaction.addToBackStack(""); // 将 fragment 提交到工作栈中 fragmentTransaction.commit();} catch (InstantiationException exception) {FileExplorerUtils.logError(TAG + exception.toString()); } catch (IllegalAccessException exception) {FileExplorerUtils.logError(TAG + exception.toString()); } }
-
当敞开一个 fragment 页面的时候,调用 removeFirst(相当于弹出栈顶的元素)移除 fragment 对象。代码如下所示
@Override public void onBackPressed() {if (!mFragments.isEmpty()) {Fragment fragment = mFragments.getFirst(); if (fragment!=null){ // 移除最下面的一个 mFragments.removeFirst();} super.onBackPressed(); // 如果 fragment 栈为空,则间接敞开 activity if (mFragments.isEmpty()) {finish(); } } else {super.onBackPressed(); } } /** * 回退 fragment 工作栈操作 * @param fragment fragment */ public void doBack(Fragment fragment) {if (mFragments.contains(fragment)) {mFragments.remove(fragment); FragmentManager fm = getSupportFragmentManager(); // 回退 fragment 操作 fm.popBackStack(); if (mFragments.isEmpty()) { // 如果 fragment 栈为空,则间接敞开宿主 activity finish();} } }
4.2 File 文件列表
-
获取文件列表,次要包含,
data/data/ 包名
目录下的缓存文件。/sdcard/Android/data/ 包名
下存储文件。/** * 初始化默认文件。留神:加 External 和不加 (默认) 的比拟 * 相同点:1. 都能够做 app 缓存目录。2.app 卸载后,两个目录下的数据都会被清空。* 不同点:1. 目录的门路不同。前者的目录存在内部 SD 卡上的。后者的目录存在 app 的外部存储上。* 2. 前者的门路在手机里能够间接看到。后者的门路须要 root 当前,用 Root Explorer 文件管理器能力看到。* * @param context 上下文 * @return 列表 */ private List<File> initDefaultRootFileInfos(Context context) {List<File> fileInfos = new ArrayList<>(); // 第一个是文件父门路 File parentFile = context.getFilesDir().getParentFile(); if (parentFile != null) {fileInfos.add(parentFile); } // 门路:/data/user/0/com.yc.lifehelper // 第二个是缓存文件门路 File externalCacheDir = context.getExternalCacheDir(); if (externalCacheDir != null) {fileInfos.add(externalCacheDir); } // 门路:/storage/emulated/0/Android/data/com.yc.lifehelper/cache // 第三个是内部 file 门路 File externalFilesDir = context.getExternalFilesDir((String) null); if (externalFilesDir != null) {fileInfos.add(externalFilesDir); } // 门路:/storage/emulated/0/Android/data/com.yc.lifehelper/files return fileInfos; }
4.3 不同版本拜访权限
-
Android 6.0 之前拜访形式
-
Android 6.0 之前是无需申请动静权限的,在 AndroidManifest.xml 里申明存储权限。就能够访问共享存储空间、其它目录下的文件。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
-
Android 6.0 之后的拜访形式
-
Android 6.0 后须要动静申请权限,除了在 AndroidManifest.xml 里申明存储权限外,还须要在代码里动静申请。
// 申请权限 if (ContextCompat.checkSelfPermission(mActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(mActivity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE); }
-
4.4 拜访文件操作
- 权限申请胜利后,即可对自带内部存储之共享存储空间和其它目录进行拜访。别离以共享存储空间和其它目录为例,论述拜访形式:
-
拜访媒体文件(共享存储空间)。目标是拿到媒体文件的门路,有两种形式获取门路:
-
以图片为例,假如图片存储在 /sdcard/Pictures/ 目录下。门路:/storage/emulated/0/Pictures/yc.png,拿到门路后就能够解析并获取 Bitmap。
// 获取目录:/storage/emulated/0/ File rootFile = Environment.getExternalStorageDirectory(); String imagePath = rootFile.getAbsolutePath() + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "yc.png"; Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
-
通过 MediaStore 获取门路
ContentResolver contentResolver = context.getContentResolver(); Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); while(cursor.moveToNext()) {String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); Bitmap bitmap = BitmapFactory.decodeFile(imagePath); break; }
-
还有一种不间接通过门路拜访的办法,通过 MediaStore 获取 Uri。与间接拿到门路不同的是,此处拿到的是 Uri。图片的信息封装在 Uri 里,通过 Uri 结构出 InputStream,再进行图片解码拿到 Bitmap
private void getImagePath(Context context) {ContentResolver contentResolver = context.getContentResolver(); Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); while(cursor.moveToNext()) { // 获取惟一的 id long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); // 通过 id 结构 Uri Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id); openUri(uri); break; } }
-
-
拜访文档和其它文件(共享存储空间)。
- 间接结构门路。与媒体文件一样,能够间接结构门路拜访。
-
拜访其它目录
- 间接结构门路。与媒体文件一样,能够间接结构门路拜访。
-
总结一下共同点
- 拜访目录 / 文件可通过如下两个办法:1、通过门路拜访。门路能够间接结构也能够通过 MediaStore 获取。2、通过 Uri 拜访。Uri 能够通过 MediaStore 或者 SAF(存储拜访框架,通过 intent 调用 startActivity 拜访)获取。
4.5 10 和 11 权限阐明
-
Android10 权限扭转
- 比方可能间接在 /sdcard/ 目录下创立目录 / 文件。能够看出 /sdcard/ 目录下,如淘宝、qq、qq 浏览器、微博、支付宝等都本人建了目录。
- 这么看来,导致目录构造很乱,而且 App 卸载后,对应的目录并没有删除,于是就是遗留了很多 ” 垃圾 ” 文件,长此以往不解决,用户的存储空间越来越小。
-
之前文件创建弊病如下
- 卸载 App 也不能删除该目录下的文件
- 在设置里 ” 革除数据 ” 或者 ” 革除缓存 ” 并不能删除该目录下的文件
- App 能够随便批改其它目录下的文件,如批改别的 App 创立的文件等,不平安
-
为什么要在 /sdcard/ 目录下新建 app 存储的目录
- 此处新建的目录不会被设置里的 App 存储用量统计,让用户 ” 看起来 ” 本人的 App 占用的存储空间很小。还有就是不便操作文件
-
Android 10.0 拜访变更
- Google 在 Android 10.0 上重拳出击了。引入 Scoped Storage。简略来说有好几个版本:作用域存储、分区存储、沙盒存储。分区存储原理:
- 1、App 拜访本身外部存储空间、拜访内部存储空间 -App 公有目录不须要任何权限(这个与 Android 10.0 之前统一)
- 2、内部存储空间 - 共享存储空间、内部存储空间 - 其它目录 App 无奈通过门路间接拜访,不能新建、删除、批改目录 / 文件等
- 3、内部存储空间 - 共享存储空间、内部存储空间 - 其它目录 须要通过 Uri 拜访
4.6 分享文件给第三方
-
这里间接说分享外部文件给第三方,大略的思路如下所示:
- 第一步:先判断是否有读取文件的权限,如果没有则申请;如果有则进行第二步;
- 第二步:先把文件转移到内部存储文件,为何要这样操作,次要是解决 data/data 下目前文件无奈间接分享问题,因而须要将指标文件拷贝到内部门路
- 第三步:通过 intent 发送,FileProvider 拿到对应门路的 uri,最初调用 startActivity 进行分享文件。
-
大略的代码如下所示
if (ContextCompat.checkSelfPermission(mActivity,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(mActivity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE); } else { // 先把文件转移到内部存储文件 File srcFile = new File(mFile.getPath()); String newFilePath = AppFileUtils.getFileSharePath() + "/fileShare.txt"; File destFile = new File(newFilePath); // 拷贝文件,将 data/data 源文件拷贝到新的指标文件门路下 boolean copy = AppFileUtils.copyFile(srcFile, destFile); if (copy) { // 分享 boolean shareFile = FileShareUtils.shareFile(mActivity, destFile); if (shareFile) {Toast.makeText(getContext(), "文件分享胜利", Toast.LENGTH_SHORT).show();} else {Toast.makeText(getContext(), "文件分享失败", Toast.LENGTH_SHORT).show();} } else {Toast.makeText(getContext(), "文件保留失败", Toast.LENGTH_SHORT).show();} }
4.7 关上图片资源
-
首先判断文件,是否是图片资源,如果是图片资源,则跳转到关上图片详情。目前只是依据文件的后缀名来判断 (对文件名称以. 进行裁剪获取后缀名) 是否是图片。
if (FileExplorerUtils.isImage(fileInfo)) {Bundle bundle = new Bundle(); bundle.putSerializable("file_key", fileInfo); showContent(ImageDetailFragment.class, bundle); }
-
关上图片跳转详情,这外面为了防止关上大图 OOM,因而须要对图片进行压缩,目前该工具次要是内存压缩和尺寸缩放形式。大略的原理如下
- 例如,咱们的原图是一张 2700 1900 像素的照片,加载到内存就须要 19.6M 内存空间,然而,咱们须要把它展现在一个列表页中,组件可展现尺寸为 270 190,这时,咱们实际上只须要一张原图的低分辨率的缩略图即可(与图片显示所对应的 UI 控件匹配),那么实际上 270 * 190 像素的图片,只须要 0.2M 的内存即可。这个采纳缩放比压缩。
- 加载图片,先加载到内存,再进行操作吗,能够如果先加载到内存,如同也不太对,这样只接占用了 19.6M + 0.2M 2 份内存了,而咱们想要的是,在原图不加载到内存中,只接将缩放后的图片加载到内存中,能够实现吗?
- 进行内存压缩,要将 BitmapFactory.Options 的 inJustDecodeBounds 属性设置为 true,解析一次图片。留神这个中央是外围,这个解析图片并没有生成 bitmap 对象(也就是说没有为它分配内存控件),而仅仅是拿到它的宽低等属性。
- 而后将 BitmapFactory.Options 连同冀望的宽度和高度一起传递到到 calculateInSampleSize 办法中,就能够失去适合的 inSampleSize 值了。这一步会压缩图片。之后再解析一次图片,应用新获取到的 inSampleSize 值,并把 inJustDecodeBounds 设置为 false,就能够失去压缩后的图片了。
4.8 为何须要 FileProvider
4.8.1 文件共享根底概念
-
理解文件共享的基础知识
- 提到文件共享,首先想到就是在本地磁盘上寄存一个文件,多个利用都能够拜访它,如下:
- 现实状态下只有晓得了文件的寄存门路,那么各个利用都能够读写它。比方相册里的图片或者视频寄存目录:/sdcard/DCIM/、/sdcard/Pictures/、/sdcard/Movies/。
-
文件共享形式是如何了解
- 一个常见的利用场景:利用 A 里检索到一个文件 yc.txt,它无奈关上,于是想借助其它利用关上,这个时候它须要把待关上的文件门路通知其它利用。对应案例就是,把磁盘文件分享到 qq。
- 这就波及到了过程间通信。Android 过程间通信次要伎俩是 Binder,而四大组件的通信也是依附 Binder,因而咱们利用间传递门路能够依附四大组件。
4.8.2 7.0 前后对文件解决形式
-
Android 7.0 之前应用,传递门路能够通过 Uri
Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); // 通过门路,结构 Uri。设置 Intent,附带 Uri,而后通过 intent 跨过程通信 Uri uri = Uri.fromFile(new File(external_filePath)); intent.setData(uri); startActivity(intent);
- 接管方在收到 Intent 后,拿出 Uri,通过:filePath = uri.getEncodedPath() 拿到发送方发送的原始门路后,即可读写文件。然而此种结构 Uri 形式在 Android7.0(含)之后被禁止了,若是应用则抛出异样,异样是 FileUriExposedException。
- 这种形式毛病如下:第一发送方传递的文件门路接管方齐全通晓,高深莫测,没有平安保障;第二发送方传递的文件门路接管方可能没有读取权限,导致接管异样。
-
Android 7.0(含)之后如何解决下面两个毛病问题
- 对第一个问题:能够将具体门路替换为另一个字符串,相似以前密码本的感觉,比方:”/storage/emulated/0/com.yc.app/yc.txt” 替换为 ”file/yc.txt”,这样接管方收到文件门路齐全不晓得原始文件门路是咋样的。那么会导致另一个额定的问题:接管方不晓得实在门路,如何读取文件呢?
- 对第二个问题既然不确定接管方是否有关上文件权限,那么是否由发送方关上,而后将流传递给接管方就能够了呢?
- Android 7.0(含)之后引入了 FileProvider,能够解决上述两个问题。
4.8.3 FileProvider 利用与原理
-
第一步,定义自定义 FileProvider 并且注册清单文件
public class ExplorerProvider extends FileProvider { } <!-- 既然是 ContentProvider,那么须要像 Activity 一样在 AndroidManifest.xml 里申明:--> <!--android:authorities 标识 ContentProvider 的唯一性,能够本人任意定义,最好是全局惟一的。--> <!--android:name 是指之前定义的 FileProvider 子类。--> <!--android:exported="false" 限度其余利用获取 Provider。--> <!--android:grantUriPermissions="true" 授予其它利用拜访 Uri 权限。--> <!--meta-data 囊括了别名利用表。--> <!--android:name 这个值是固定的,示意要解析 file_path--> <!--android:resource 本人定义实现的映射表 --> <provider android:name="com.yc.toolutils.file.ExplorerProvider" android:authorities="${applicationId}.fileExplorerProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_explorer_provider" /> </provider>
-
第二步,增加门路映射表
-
在 /res/ 下建设 xml 文件夹,而后再创立对应的映射表(xml),最终门路如下:/res/xml/file_explorer_provider.xml。
<paths> <!--FileProvider 须要读取映射表。--> <external-cache-path name="external_cache" path="." /> <cache-path name="cache" path="." /> <external-path name="external_path" path="." /> <files-path name="files_path" path="." /> <external-files-path name="external_files_path" path="." /> <root-path name="root_path" path="." /> </paths>
-
-
第三步,应用 ExplorerProvider 来跨过程通信交互
-
如何解决第一个问题,让接管方看不到具体文件的门路?如下所示,上面结构后,第三方利用收到此 Uri 后,并不能从门路看出咱们传递的实在门路,这就解决了第一个问题。
public static boolean shareFile(Context context, File file) { boolean isShareSuccess; try {if (null != file && file.exists()) {Intent share = new Intent(Intent.ACTION_SEND); // 此处可发送多种文件 String absolutePath = file.getAbsolutePath(); // 通过扩展名找到 mimeType String mimeType = getMimeType(absolutePath); share.setType(mimeType); Uri uri; // 判断 7.0 以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 第二个参数示意要用哪个 ContentProvider,这个惟一值在 AndroidManifest.xml 里定义了 // 若是没有定义 MyFileProvider,可间接应用 FileProvider 代替 String authority = context.getPackageName() + ".fileExplorerProvider"; uri = FileProvider.getUriForFile(context,authority, file); } else {uri = Uri.fromFile(file); } //content://com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt //content 作为 scheme;//com.yc.lifehelper.fileExplorerProvider 即为咱们定义的 authorities,作为 host;LogUtils.d("share file uri :" + uri); String encodedPath = uri.getEncodedPath(); //external_path/fileShare.txt // 如此结构后,第三方利用收到此 Uri 后,并不能从门路看出咱们传递的实在门路,这就解决了第一个问题:// 发送方传递的文件门路接管方齐全通晓,高深莫测,没有平安保障。LogUtils.d("share file uri encode path :" + encodedPath); share.putExtra(Intent.EXTRA_STREAM, uri); share.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 赋予读写权限 share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent intent = Intent.createChooser(share, "分享文件"); // 交由零碎解决 context.startActivity(intent); isShareSuccess = true; } else {isShareSuccess = false;} } catch (Exception e) {e.printStackTrace(); isShareSuccess = false; } return isShareSuccess; }
- 如何解决第二个问题,发送方传递的文件门路接管方可能没有读取权限,导致接管异样?通过 FileProvider.getUriForFile 为入口查看源码,利用间通过 IPC 机制,最初调用了 openFile()办法,而 FileProvider 重写了该办法。
-
4.9 跨过程 IPC 通信
-
A 利用 (该 demo) 通过结构 Uri,通过 intent 调用 B(分享到 QQ)
- 利用 A 将 path 结构为 Uri:利用 A 在启动的时候,会扫描 AndroidManifest.xml 里的 FileProvider,并读取映射表结构为一个 Map。
- 还是以 /storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt 为例,当调用 FileProvider.getUriForFile(xx)时,遍历 Map,找到最匹配条目,最匹配的即为 external_file。因而会用 external_file 代替原始门路,最终造成的 Uri 为:content://com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt
-
B 利用 (QQ) 通过 Uri 结构输出流,将 Uri 解析成具体的门路
- 利用 B 通过 Uri(A 传递过去的),解析成具体的 file 文件。先将 Uri 拆散出 external_file/fileShare.txt,而后通过 external_file 从 Map 里找到对应 Value 为:/storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/,最初将 fileShare.txt 拼接,造成的门路为:/storage/emulated/0/com.yc.lifehelper.fileExplorerProvider/external_path/fileShare.txt
-
当初来梳理整个流程:
- 1、利用 A 应用 FileProvider 通过 Map(映射表)将 Path 转为 Uri,通过 IPC 传递给利用 B。
- 2、利用 B 应用 Uri 通过 IPC 获取利用 A 的 FileProvider。
- 3、利用 A 应用 FileProvider 通过映射表将 Uri 转为 Path,并结构出文件描述符。
- 4、利用 A 将文件描述符返回给利用 B,利用 B 就能够读取利用 A 发送的文件了。
-
整个交互流程图如下
05. 其余设计实际阐明
5.1 性能设计
- 这个暂无,因为是小工具,次要是在 debug 环境下依赖应用。代码逻辑并不简单,不会影响 App 的性能。
5.2 稳定性设计
-
批改文件阐明
- 目前,针对文本文件,比方缓存的 json 数据,存储在文本文件中,之前测试说让该工具反对批改属性,思考到批改 json 比较复杂,因而这里只是实现能够删除文本文件,或者批改文件名称的性能。
- 针对图片文件,能够关上且进行了图片压缩,仅仅反对删除图片文件操作。
- 针对 sp 存储的数据,是 xml,这里可视化展现 sp 的数据,目前能够反对批改 sp 数据,测试童鞋这不便操作简略,进步某些场景的测试效率。
-
为何不反对批改 json
- 读取文本文件,是一行行读取,批改数据编辑数据麻烦,而且批改实现后对 json 数据合法性判断也比拟难解决。因而这里临时不提供批改缓存的 json 数据,测试如果要看,能够通过分享到内部 qq 查看文件,或者间接查看,防止脏数据。
5.3 debug 依赖设计
-
倡议在 debug 下应用
-
在小工具放到 debug 包名下,依赖应用。或者在 gradle 依赖的时候辨别也能够。如下所示:
// 在 app 包下依赖 apply from: rootProject.file('buildScript/fileExplorer.gradle') /** * 沙盒 file 工具配置脚本 */ println('gradle file explorer , init start') if (!isNeedUseExplorer()) {println('gradle file explorer , not need file explorer') return } println('gradle file isNeedUseExplorer = ture') dependencies { // 依赖 implementation('com.github.jacoco:runtime:0.0.23-SNAPSHOT') } // 过滤,只在 debug 下应用 def isNeedUseJacoco() {Map<String, String> map = System.getenv() if (map == null) {return false} // 拿到编译后的 BUILD_TYPE 和 CONFIG。具体看 BuildConfig 生成类的代码 boolean hasBuildType = map.containsKey("BUILD_TYPE") boolean hasConfig = map.containsKey("CONFIG") println 'gradle file explorer isNeedUseExplorer hasBuildType =====>' + hasBuildType + ',hasConfig =' + hasConfig String buildType = "debug" String config = "debug" if (hasBuildType) {buildType = map.get("BUILD_TYPE") } if (hasConfig) {config = map.get("CONFIG") } println 'gradle file explorer isNeedUseExplorer buildType =====>' + buildType + ',config =' + config if (buildType.toLowerCase() == "debug" && config.toLowerCase() == "debug" && isNotUserFile()) {println('gradle file explorer debug used') return true } println('gradle file explorer not use') // 如果是正式包,则不应用沙盒 file 工具 return false } static def isNotUserFile() { // 在 debug 下默认沙盒 file 工具,如果你在 debug 下不想应用沙盒 file 工具,则设置成 false return true }
-