一、前言
从 Android N(7.0) 开始,将严格执行 StrictMode 模式,也就是说,将对安全做更严格的校验。而从 Android N 开始,将不允许在 App 间,使用 file:// 的方式,传递一个 File ,否者会抛出 FileUriExposedException 的错误,会直接引发 Crash。
但是,既然官方对文件的分享做了一个这么强硬的修改(直接抛出异常),实际上也提供了解决方案,那就是 FileProvider,通过 comtent:// 的模式替换掉 file:// ,同时,需要开发者主动升级 targetSdkVersion 到 24 才会执行此策略,也留给了开发者升级的时间。
本文就 FileProvider 需要了解的所有细节,进行一个详尽的说明。
二、如何使用 FileProvider
1、什么是 FileProvider
FileProvider 是 Android support v4 包下,提供的一个 ContentProvider 的子类,用于向其他 App 分享文件,并且是在 v4 包下,所以只要引入了 v4 包,就可以做到全版本兼容。
既然 FileProvider 本质上就是一个 ContentProvider ,它其实也继承了 ContentProvider 的特性。ContentProvider 其实就是在可控的范围内,向外部其他的 App 分享数据。而 FileProvider 将这样的数据变成了一个 File 文件而已。
2、在什么场景下需要使用 FileProvider
在 App 间对 file:// 的分享做了严格的校验之后,其实也是出于安全考虑,这就导致了,所有包含 file:// 的URI 的 Intent 离开你的 App ,都受此限制。所以说,只要你的 App 内,通过一个 Intent 传递了一个 file:// 的 Uri ,就需要小心使用了。
在实际开发过程中,使用最多的场景有一下几个:
- 调用相机拍照。
- 剪裁图片。
- 调用系统安装器去安装 Apk。
3、如何使用 FileProvider
1)在 AndroidManifest 中配置
前面提到,FileProvider 实际上是一个 ContentProvider ,所以如果需要使用它,就需要在 AndroidManifest.xml 中声明它。
可以看到,provider 标签下,配置了几个属性:
- name :配置当前 FileProvider 的实现类。
- authorities:配置一个 FileProvider 的名字,它在当前系统内需要是唯一值。
- exported:表示该 FileProvider 是否需要公开出去,这里不需要,所以是 false。
- granUriPermissions:是否允许授权文件的临时访问权限。这里需要,所以是 true。
可以看到 name 属性就是标记当前 FileProvider 的实现类,对于一个 App Module 而言,如果只是自己使用,可以直接使用 v4 包下的 FileProvider ,但是如果是作为一个 Lib Module 来供其他项目使用,最好还是重新空继承一个 FileProvider ,这里填写我们的继承类即可。
2) 指定可分享的文件路径
在配置 Provider 的时候,还需要额外配置一个 <meta-data/> 标签,它用于配置 FileProvider 支持分享出去的目录。这个 <meta-data/> 标签的 name 值是固定的,resource 需要指向一个 根节点为 paths 的 xml 资源文件。
然后就可以对 provider_paths.xml 进行配置。
paths 标签内,必须配置最少一个 xxx-path 标签,上图给出的例子,配置的是 files-path 这些配置的信息,都是可以在官方文档中找到答案的,这里直接以查阅源码的方式来查看他们分别代表的意思。
这些配置,在 FileProvider 的源码内,都是以一个个 TAG_Xxx 标记的。
而他们分别代表的目录,也可以在源码内找到答案。
可以看到,不同的标签,代表不同的目录。
- root-path:表示根目录,『/』。
- files-path:表示 content.getFileDir() 获取到的目录。
- cache-path:表示 content.getCacheDir() 获取到的目录
- external-path:表示Environment.getExternalStorageDirectory() 指向的目录。
- external-files-path:表示 ContextCompat.getExternalFilesDirs() 获取到的目录。
- external-cache-path:表示 ContextCompat.getExternalCacheDirs() 获取到的目录。
注意,这里 ContextCompat 只是对 Context 做了一个兼容处理,其实就是对 Api level 19 做了一个分解,分别代表不同的获取方式,以 getExternalFilesDirs() 为例。
3) 使用 content://
配置工作已经全部完成,后面就需要将之前传递的 file:// 替换成 FileProvider 需要的 content:// ,这就需要用到 FileProvider.getUriForFile() 方法了,以下是它的完整签名。
getUriForFile() 方法,需要一个 authority 的参数,这正是前面在 AndroidManifest.xml 中 配置的 android:authorities 。
调用此方法,会自动得到一个 file:// 转换成 content:// 的 一个 Uri 对象,可以供我们直接使用。
4) 授予临时的读写权限
在配置 provider 标签的时候,有一个属性 android:grantUriPermissions="true" ,它表示允许它授予 Uri 临时的权限。
当我们生成出一个 content:// 的 Uri 对象之后,其实也无法对其直接使用,还需要对这个 Uri 接收的 App 赋予对应的权限才可以。
授权类型的常量,被定义在 Intent 类中。
可以看到,直接就是读和写的权限授予。
而这个授权的动作,提供了两种方式来授权:
1、使用 Context.grantUriPermission() 为其他 App 授予 Uri 对象的访问权限。
它的完整签名如下:
grantUriPermission() 方法包含三个参数,这三个参数都非常的好理解。
- toPackage :表示授予权限的 App 的包名。
- uri:授予权限的 content:// 的 Uri。
- modeFlags:前面提到的读写权限。
这种情况下,授权的有效期限,从授权一刻开始,截止于设备重启或者手动调用 Context.revokeUriPermission() 方法,才会收回对此 Uri 的授权。
2、配合 Intent.addFlags() 授权。
既然这是一个 Intent 的 Flag,Intent 也提供了另外一种比较方便的授权方式,那就是使用 Intent.setFlags() 或者 Intent.addFlag 的方式。
这种方式相信大家都比较熟悉,就不细说了。而使用这种形式的授权,权限截止于该 App 所处的堆栈被销毁。也就是说,一旦授权,直到该 App 被完全退出,这段时间内,该 App 享有对此 Uri 指向的文件的对应权限,我们无法再主动收回此权限了。
虽然使用 Intent.addFlags() 的方式,一旦授权将无法主动回收,但是大多数情况下,也是会使用此种方式进行授权,除了操作起来方便之外,既然授权了也无需太担心对方会有破坏的行为。有点切合 用人不疑,疑人不用 的道理。
拥有了授权权限的 content:// 的 Uri 之后,就可以通过 startXxx 或者 setResult() 的方式,将 Uri 传递给其他的 App。
5)举个例子
到这里,基本上关于 FileProvider 的使用,都做了一个详尽的说明,接下来举个简单的例子来看看如何使用它。
调起系统安装器来安装一个 Apk 。
三、FileProvider 的注意事项
1、authorities 的唯一性
在 AndroidManifest.xml 中配置 provider 的时候,需要保证 android:authorities 的值,在整个系统中的唯一性。其实这也很好理解,看了 FileProvider.getUriForFile() 之后,发现它是通过 android:authorities 属性配置的值,来唯一确定由谁来响应这个 provider 的,所以它需要保证在系统内唯一,否者安装的时候会抛出异常。
而在常规开发过程中,如果是一个 App Module 在使用 FileProvider 的话,那么只需要我们自己规范不要写同一个 authorities 即可。但是如果是作为一个 Lib Module 发布出去的话,是需要考虑使用者的如何使用的,所以为了友好起见,最好使用 applicationId 来配置 provider 标签。
这样配置之后,就会使用 Gradle 中配置的 applicationId 的值替换这里,而使用 FileProvider.getUriForFile() 的时候,只需要根据 applicationId 拼接一个 authorities 值即可,简单修改一下上面调用系统去安装 APK 的例子。
2、Lib 下的 targetSdkVersion
前面提到,如果不将 targatSdkVersion 升级到 24 的话,之前的方式依然是可用的,不会有 FileUriExposedException 的隐患。但是如果你的项目是作为一个 Lib Module 这种 SDK 的形式发布出去,供其他人使用的话,这里的 targetSdkVersion 就不受 Lib 的 targetSdkVersion 控制,而是主项目的 targetSdkVersion。
所以如果是以 SDK 的形式集成到别的 App 内使用的话,如果需要用发送一个 File 给其他 App,一定要适配 FileProvider 。
3、不使用 v4 包
FIleProvider 是存在于的 Support v4 包下,所以想要使用 FileProvider 就必须集成 v4 包。但是对于一个本身无需使用 v4 包的项目来说,为了 FileProvider 来集成 v4 包,无形中就增加了安装包的体积。
但是仔细看 FileProvider ,其实并没有引用到什么更多的 package ,而 FileProvider 本质上也只是一个 ContentProvider ,所以我们只需要将它的代码复制出来,简单修改一下保证可以正确运行,就可以使用,而不是必须继承 v4 包。
四、小结
FileProvider 的核心就是提高安全性,让开发者来限制自己本 App 的文件对外的访问权限,以提高安全性。
所以在开发过程中,只需要配合 FileProvider 将我们可能需要第三方 App 用到的文件目录加入到可授权的范围,然后在发送 Intent 的时候,对其进行授权即可,其他的操作和之前并无变化,这里就不一一列举了。
【本文为51CTO专栏作者“张旸”的原创稿件,转载请通过微信公众号联系作者获取授权】