目录介绍

  • 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/yangchong211/YCAndroidTool

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页面,否则跳转到文件详情页面
  • 处理任务栈返回逻辑。举个例子现在列表FileExplorerFragment当作B,文件详情页面当作C,宿主Activity当作A。也就是说,点击返回键,依次关闭了fragment直到没有,回到宿主activity页面。再次点击返回键,则关闭activity!
    • 可能存在的任务栈是:打开A1->打开B1->打开C1
    • 那么点击返回键按钮,返回关闭的顺序则是:关闭C1->关闭B1->关闭A1
  • Fragment回退栈处理方式
    • 第一种方案:创建一个栈(先进后出),打开一个FileExplorerFragment列表页面(push一个fragment对象到队列中),关闭一个列表页面(remove最上面那个fragment对象,然后调用FragmentManagerpopBackStack操作关闭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栈为空,则直接关闭activityif (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栈为空,则直接关闭宿主activityfinish();}}
      }
      

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/filesreturn 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()) {//获取唯一的idlong id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));//通过id构造UriUri 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 自己定义实现的映射表-->
    <providerandroid:name="com.yc.toolutils.file.ExplorerProvider"android:authorities="${applicationId}.fileExplorerProvider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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();//通过扩展名找到mimeTypeString 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 = ' + hasConfigString 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 = ' + configif (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工具,则设置成falsereturn true
    }
    

App磁盘沙盒工具实践相关推荐

  1. Mac App Store沙盒政策气走开发者 苹果“死不悔改”

    专注,条理,牛逼的邮件客户端Postbox退出Mac App Store的消息你一定听说了,7月1号的时候我们知道:"Postbox他们忍受了App Store升级不付费的机制, 但是&qu ...

  2. iOS 冰与火之歌 - 利用 XPC 过 App 沙盒

    蒸米是阿里巴巴的移动安全工程师,香港中文大学博士,也是发现并命名了XcodeGhost的人.这次他所在的iOS安全小组发现了影响最新版iOS 9.3的0day漏洞.此漏洞杀伤力巨大,在非越狱手机上一个 ...

  3. ios沙箱软件_iOS沙盒管理工具FHHSandBoxViewer

    KeyWord:沙盒文件递归遍历.Swift里的闭包多个weak修饰.iOS使用AirDrop 操作演示.gif 平时真机开发调试App的时候想查看沙盒具体内容一般做法是连接Xcode下载整个App的 ...

  4. 【iOS 开发】使用 iMazing 进行沙盒调试

    当开发项目有可能进行图片的缓存.清理操作时,我们可能会需要手动去 app 的沙盒里面删除.替换一些内容来进行测试,但是通过代码去目录下查找文件可能不是很方便,也不够直观,这里提供一个方法来方便这个调试 ...

  5. 布谷鸟沙盒分析静态文件_“案例沙盒方法”喜提国际商学院协会(AACSB)2019年启发式创新奖...

    国际商学院协会(AACSB)于2019年4月14日至16日在英国爱丁堡召开年会,会上公布了2019年Innovation that Inspire的(启发式创新奖)获奖学校及项目.大会共收到800余个 ...

  6. IOS文件管理系统:沙盒、NSFileManager、NSFileHandle、NSCoding、NSKeyedArchiver解析

    什么是沙盒? 苹果为我们每个app都分配了一个固定的文件夹,我们在开发过程中可以向文件夹存入我们想要存储的数据,同时也能从这些文件夹取这些数据,但是我们不能取到其他app所存储的数据.简单说,沙盒就是 ...

  7. 清空沙盒路径下的缓存数据

    http://www.jianshu.com/p/7f1ea17995ef 引言: 移动应用在处理网络资源时,一般都会做离线缓存处理,其中以图片缓存最为典型,其中很流行的离线缓存框架为 SDWebIm ...

  8. iOS开发 清空沙盒路径下的缓存数据

    引言: 移动应用在处理网络资源时,一般都会做离线缓存处理,其中以图片缓存最为典型,其中很流行的离线缓存框架为 SDWebImage . 但是,离线缓存会占用手机存储空间,所以缓存清理功能基本成为资讯. ...

  9. iOS 开发之沙盒机制 文件操作 (NSFielManager)

    原文链接:http://www.jianshu.com/p/349855b5a8ae iOS APP 可以在自己的沙盒里读写文件,但是,不可以访问其他 APP 的沙盒.每一个 APP 都是一个信息孤岛 ...

最新文章

  1. Redis数据库搭建集群(集群概念、redis集群、搭建集群(配置机器1、2、创建集群、数据操作验证)、Python与redis集群交互)
  2. Android硬件抽象层(HAL)深入剖析(一)
  3. boost库之tcp client 回调不正常
  4. uploadify多文件上传插件
  5. 执行应用程序出现: No such file or directory
  6. zabbix3.0.4通过jmx监控tomcat
  7. EntityFramwork常见问题
  8. 使用ABP打造SAAS系统(2)——前端框架选择
  9. 运维-替换-修改kibana徽标
  10. 一篇文章讲清python开发必懂的8种数据结构
  11. kvaser在linux中的应用
  12. Ubuntu14.04 ec20 4G模块 ppp拨号--rt3070 wifi模块释放sofAP
  13. c语言由天数求日期,C语言-由日期求天数由天数求日期
  14. mysql 错误问题_Mysql常见的几个错误问题及解决方法
  15. Ubuntu 设置屏幕字体大小
  16. circos个人使用教程
  17. 思科下一代模拟器EVE-NG安装
  18. 我的世界java无法连接服务器_java - 无法连接到Minecraft服务器 - SO中文参考 - www.soinside.com...
  19. mongodb数据迁移设置方法
  20. 项目管理软件用哪个好?推荐这6款项目管理工具

热门文章

  1. studio one机架效果包
  2. 调用iframe子页面中的函数
  3. 状态空间平均建模——Flyback
  4. matlab 与cla的区别clf,何时使用cla(),clf()或close()清除matplotlib中的图?...
  5. DSP28035 CLA配置与调试步骤
  6. EMNIST数据集 —— 百度云链接
  7. Git 入门到精通,大厂程序员呕心沥血强力推荐
  8. 1月16日服务器维护,【影之诗】1月16日服务器维护时卡片调整公告
  9. ROS1学习笔记--xacro
  10. 小程序--语音合成tts 对接多平台(讯飞,思必驰,百度)