关于android:App磁盘沙盒工具实践

目录介绍

  • 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页面,否则跳转到文件详情页面
  • 解决工作栈返回逻辑。举个例子当初列表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栈为空,则间接敞开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
      }

demo地址:https://github.com/yangchong2…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理