12、文件 Provider:FileProvider 使用、文件 URI 生成、临时权限授予

聊到 FileProvider,我得先坦白一件事。刚做 Android 那会儿,我直接拿 file:// 路径传给别的 App,结果在 Android 7.0 上直接崩了。嗯,就是那个著名的 FileUriExposedException。当时我还在想,Google 怎么这么不近人情?后来才明白,这是为了安全。

说白了,FileProvider 就是 Android 官方给咱们准备的一个「安全文件快递员」。它能把 App 内部的文件,包装成一个 content:// 的 URI 发给别人。对方拿着这个 URI,就能在临时权限范围内读写文件。

核心思想: 用 content URI 代替 file URI,配合临时权限,实现跨进程安全文件共享。

12.1 为什么需要 FileProvider?

你想想看,如果直接把 file:///storage/emulated/0/... 发给第三方 App,对方就能拿到文件的绝对路径。这太危险了。恶意 App 完全可以顺着路径去偷你别的文件。

FileProvider 做了两件事:

  • 隐藏真实路径——把 /data/data/你包名/files/xxx.jpg 映射成 content://你包名.fileprovider/files/xxx.jpg
  • 控制访问权限——只有你授权的 App,在授权时间内才能读写

我在项目中遇到过一件事:有个第三方 SDK 要读取我们 App 的日志文件。如果直接给 file URI,那 SDK 就能遍历整个 data 目录。用了 FileProvider 之后,它只能看到我授权的那一个文件。安全多了。

12.2 配置 FileProvider

配置其实不复杂,分三步走。

12.2.1 在 AndroidManifest 中声明

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

这里有个细节要注意:android:exported="false" 是必须的。因为 FileProvider 不需要被外部直接调用,它只通过 Intent 来工作。我见过有人手抖写成 true,虽然也能用,但安全审计会报警。

12.2.2 编写 file_paths.xml

res/xml/ 目录下创建这个文件。它定义了哪些路径可以被映射。

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="my_files" path="." />
    <cache-path name="my_cache" path="." />
    <external-path name="external_files" path="." />
    <external-files-path name="external_app_files" path="." />
    <external-cache-path name="external_cache" path="." />
</paths>

每个标签对应一种存储区域:

标签 对应路径 适用场景
<files-path> Context.getFilesDir() 内部存储私有文件
<cache-path> Context.getCacheDir() 缓存文件
<external-path> Environment.getExternalStorageDirectory() 外部存储根目录
<external-files-path> Context.getExternalFilesDir() 外部存储私有目录
<external-cache-path> Context.getExternalCacheDir() 外部缓存目录
我的习惯: 我一般只配 <files-path><cache-path>。外部存储的路径太宽泛,容易暴露不该暴露的东西。除非业务确实需要,否则别偷懒全写上。

12.3 生成文件 URI

配置好了之后,生成 URI 就一行代码的事:

File file = new File(context.getFilesDir(), "report.pdf");
Uri contentUri = FileProvider.getUriForFile(context,
        context.getPackageName() + ".fileprovider",
        file);

返回的 URI 长这样:

content://com.example.myapp.fileprovider/my_files/report.pdf

注意看,my_files 就是你在 file_paths.xml 里定义的 name 属性。它把真实路径给隐藏了。

我曾经踩过一个坑:getUriForFile 的第二个参数必须和 Manifest 里的 android:authorities 完全一致。少写个点都不行。而且如果你用了 applicationId 占位符,那这个参数最好动态拼接,别写死。

12.4 临时权限授予

光生成 URI 还不够。你得告诉系统:「我允许哪个 App 来读/写这个文件」。这就是临时权限的作用。

12.4.1 通过 Intent 授权

最常见的做法,是在发送 Intent 时加上 flag:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(contentUri, "application/pdf");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 如果需要写权限,再加这个
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivity(intent);

这里有两个 flag:

  • FLAG_GRANT_READ_URI_PERMISSION——授予读权限
  • FLAG_GRANT_WRITE_URI_PERMISSION——授予写权限

权限是「临时」的,什么意思呢?就是目标 App 在它的 Activity 存活期间可以访问。一旦进程被杀,权限就没了。你想想看,这比直接给文件路径安全多了吧?

注意: 如果你同时需要读写权限,两个 flag 都要加。只加读权限的话,对方想修改文件就会报错。我之前给一个图片编辑 App 传图片,忘了加写权限,结果对方保存不了编辑结果。排查了半天才发现是权限没给够。

12.4.2 通过 setClipData 授权

如果你要发送多个文件,可以用 ClipData

Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("image/*");

ArrayList<Uri> uris = new ArrayList<>();
uris.add(FileProvider.getUriForFile(...));
uris.add(FileProvider.getUriForFile(...));

intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
intent.setClipData(ClipData.newUri(getContentResolver(), "images", uris.get(0)));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

这里有个关键点:setClipData 会把所有 URI 都带上,系统会自动给这些 URI 都授予权限。如果你只调 addFlags 但不设 setClipData,那只有第一个 URI 有权限。嗯,这个细节我当年也是 debug 了好久才发现的。

12.5 知识体系图

下面这张图总结了 FileProvider 的完整工作流程:

FileProvider 工作流程 1. 配置 Provider AndroidManifest 声明 2. 定义映射路径 file_paths.xml 3. 生成 Content URI getUriForFile() 4. 临时权限授予 FLAG_GRANT_READ_URI_PERMISSION FLAG_GRANT_WRITE_URI_PERMISSION 5. 发送给目标 App Intent.setDataAndType() 或 setClipData() 目标 App 接收 Content URI 通过 ContentResolver 读写文件 权限生命周期:目标 App 存活期间有效,进程被杀后自动回收

12.6 避坑指南

最后分享几个我实战中遇到的坑:

  • Android 7.0 以上必须用——低于 7.0 的设备虽然不会崩溃,但为了统一,我建议全版本都用 FileProvider。别搞版本判断。
  • 路径别写太宽——path="." 表示整个目录都暴露。如果你只想暴露某个子目录,写成 path="downloads/" 更安全。
  • 临时权限不是永久的——如果目标 App 想长期持有这个文件,它应该自己复制一份。别指望 URI 权限能跨进程持久化。
  • 多文件发送记得用 ClipData——我前面说了,不加 ClipData 的话,只有第一个 URI 有权限。这个坑我至少见过三次。
我的建议: 如果你在做一个文件分享功能,最好封装一个工具类。把 FileProvider 的 URI 生成、权限添加、Intent 构建都包在一起。这样团队里其他人用起来不会出错。我自己的项目里就是这么干的,省心。

好了,FileProvider 的核心内容就这些。说白了就是三步:配置、生成 URI、授权。但每一步都有细节,不注意就会踩坑。希望你能在实际项目中用起来,别再被 FileUriExposedException 折磨了。


公众号:蓝海资料掘金营,微信deep3321