在Android开发中处理文件共享时,经常会遇到一个经典错误:"FileUriExposedException"。这个异常背后隐藏着Android系统的一个重要安全机制演变过程。
早期Android版本中,应用间文件共享可以直接使用file://URI。但这种方式存在严重安全隐患:
从Android 7.0(API 24)开始,系统强制要求使用ContentProvider来共享文件。这就是FileProvider诞生的背景——它是Android系统提供的一个特殊ContentProvider子类,专门用于安全地共享应用文件。
关键提示:如果你的应用targetSdkVersion≥24,就必须使用FileProvider替代传统的file://URI方式,否则在Android 7.0及以上设备会直接崩溃。
FileProvider的核心工作流程可以分解为三个关键步骤:
这种机制带来了多重优势:
| 方案 | 最低API | 安全性 | 适用场景 |
|---|---|---|---|
| FileProvider | API 24 | 高 | 应用间共享私有文件 |
| MediaStore | API 1 | 中 | 共享媒体文件到系统相册 |
| Storage Access Framework | API 19 | 高 | 用户主动选择文件 |
1. 清单文件声明
在AndroidManifest.xml中添加:
xml复制<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>
2. 创建路径配置文件
res/xml/file_paths.xml:
xml复制<paths>
<!-- 对应Context.getFilesDir() -->
<files-path name="internal_files" path="." />
<!-- 对应Context.getCacheDir() -->
<cache-path name="internal_cache" path="." />
<!-- 对应Environment.getExternalStorageDirectory() -->
<external-path name="external_storage" path="." />
<!-- 对应Context.getExternalFilesDir(null) -->
<external-files-path name="external_app_files" path="." />
<!-- 对应Context.getExternalCacheDir() -->
<external-cache-path name="external_app_cache" path="." />
</paths>
场景:分享应用内的图片文件
kotlin复制fun shareImage(file: File) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
putExtra(Intent.EXTRA_STREAM, uri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(Intent.createChooser(intent, "分享图片"))
}
关键参数说明:
对于复杂项目,建议按功能模块划分路径:
xml复制<paths>
<!-- 用户头像 -->
<files-path name="user_avatars" path="avatars/" />
<!-- 应用日志 -->
<files-path name="app_logs" path="logs/" />
<!-- 临时下载 -->
<external-cache-path name="temp_downloads" path="downloads/" />
</paths>
问题1:FileProvider$IllegalArgumentException: Failed to find configured root
原因:尝试分享未配置路径下的文件
解决方案:
问题2:接收方应用无法打开文件
解决方案:
URI缓存:对频繁共享的文件URI进行缓存
kotlin复制private val uriCache = LruCache<String, Uri>(10)
fun getCachedUri(file: File): Uri {
return uriCache.get(file.path) ?:
FileProvider.getUriForFile(...).also {
uriCache.put(file.path, it)
}
}
批量授权:使用Intent.setClipData()实现多文件共享
kotlin复制val uris = arrayListOf<Uri>().apply {
add(getUriForFile(file1))
add(getUriForFile(file2))
}
Intent().apply {
action = Intent.ACTION_SEND_MULTIPLE
type = "image/*"
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
最小权限原则:
输入验证:
kotlin复制fun getValidatedUri(file: File): Uri {
// 检查文件是否在允许目录中
val allowedPaths = setOf("/data/data/...", "/storage/...")
if (!file.canonicalPath.startsWith(allowedPaths)) {
throw SecurityException("非法文件路径")
}
return FileProvider.getUriForFile(...)
}
临时权限回收:
kotlin复制// 在不再需要共享时调用
context.revokeUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION)
日志监控:
xml复制<provider
...
android:writePermission="android.permission.DUMP"
tools:ignore="ProtectedPermissions">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
kotlin复制@Test
fun testFileProviderUriGeneration() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val testFile = File(context.filesDir, "test.txt").apply {
writeText("test content")
}
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
testFile
)
assertThat(uri.authority).isEqualTo("${context.packageName}.fileprovider")
assertThat(uri.path).contains("test.txt")
}
kotlin复制class FileShareWorker(context: Context, params: WorkerParameters)
: Worker(context, params) {
override fun doWork(): Result {
val file = File(applicationContext.filesDir, "report.pdf")
generateReport(file)
val uri = FileProvider.getUriForFile(
applicationContext,
"${applicationContext.packageName}.fileprovider",
file
)
NotificationCompat.Builder(applicationContext, "channel_id")
.setContentTitle("报告已生成")
.setContentText("点击分享")
.setSmallIcon(R.drawable.ic_share)
.setContentIntent(createSharePendingIntent(uri))
.build()
.let { notification ->
NotificationManagerCompat.from(applicationContext)
.notify(1, notification)
}
return Result.success()
}
private fun createSharePendingIntent(uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
putExtra(Intent.EXTRA_STREAM, uri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(
applicationContext,
0,
Intent.createChooser(intent, null),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}
方案设计:
定义自定义FileProvider子类
kotlin复制class CustomFileProvider : FileProvider() {
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
// 添加额外的权限检查
checkPermission(uri)
return super.openFile(uri, mode)
}
}
配置独立的authority
xml复制<provider
android:name=".CustomFileProvider"
android:authorities="${applicationId}.crossapp.fileprovider"
android:exported="true"
android:grantUriPermissions="true">
<meta-data ... />
</provider>
实现安全的URI验证
kotlin复制fun verifyUriSignature(uri: Uri): Boolean {
val expected = buildSignature(uri.path)
return uri.getQueryParameter("sig") == expected
}
kotlin复制fun getShareableUri(file: File): Uri {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
file
)
} else {
Uri.fromFile(file)
}
}
| 版本范围 | 处理要点 | 注意事项 |
|---|---|---|
| API <19 | 直接使用file:// | 无特殊处理 |
| API 19-23 | 可选择性使用FileProvider | 建议提前适配 |
| API 24+ | 必须使用FileProvider | 严格权限控制 |
当多个库都声明FileProvider时,会出现合并冲突:
解决方案:
在applicationId后添加库标识符
xml复制<provider
android:authorities="${applicationId}.libraryname.fileprovider"
... />
使用tools:replace合并属性
xml复制<provider
tools:replace="android:authorities"
... />
APK文件共享:
kotlin复制fun installApk(apkFile: File) {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(...)
} else {
Uri.fromFile(apkFile)
}
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(intent)
}
注意事项:
kotlin复制class LoggingFileProvider : FileProvider() {
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
logAccess(uri)
return super.query(uri, projection, selection, selectionArgs, sortOrder)
}
private fun logAccess(uri: Uri) {
FirebaseAnalytics.getInstance(context)
.logEvent("file_provider_access", bundleOf(
"uri" to uri.toString(),
"time" to System.currentTimeMillis()
))
}
}
| 指标 | 采集方式 | 告警阈值 |
|---|---|---|
| 失败请求数 | ContentProvider调用统计 | 每分钟>5次 |
| 非法路径访问 | URI路径分析 | 任何非法访问 |
| 权限授予次数 | PackageManager日志 | 异常增长时 |
在实际项目中,FileProvider的正确使用往往能避免80%以上的文件共享相关问题。我曾在一次版本更新中,因为没有正确处理FileProvider导致应用在Android 10设备上全面崩溃,这个教训让我深刻理解到掌握FileProvider的重要性。特别要注意的是,不同厂商ROM可能会对FileProvider有定制行为,因此真机测试环节绝对不能省略。