1. 为什么你的Android应用突然崩溃了那天我正在调试一个视频编辑功能点击分享按钮时应用突然闪退日志里赫然出现一行刺眼的红色错误android.os.FileUriExposedException。相信不少Android开发者都遇到过这个经典异常特别是在处理文件共享时。这个错误的本质是Android 7.0API 24引入的严格模式文件URI暴露安全机制导致的。简单来说当你尝试通过file://URI将应用私有文件共享给其他应用时系统会直接抛出这个异常。这就像你家的门牌号突然被公开在社交媒体上任何人都能找上门来——显然存在安全隐患。Android系统为了防止这种不安全的数据共享方式强制要求使用更安全的FileProvider机制。我查了下数据在Stack Overflow上关于这个异常的提问超过1.2万条在Crashlytics统计的崩溃排行榜上常年位居前20。最容易触发这个异常的场景包括调用系统相机拍照后保存图片视频编辑后分享到社交媒体通过Intent打开第三方应用查看文档应用间文件共享功能2. FileUriExposedException的底层原理2.1 从报错堆栈看问题根源先来看一个典型的错误堆栈android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/Camera/VIDEO.mp4 exposed beyond app through Intent.getData() at android.os.StrictMode.onFileUriExposed(StrictMode.java:2141) at android.net.Uri.checkFileUriExposed(Uri.java:2391) at android.content.Intent.prepareToLeaveProcess(Intent.java:11165)关键点在于prepareToLeaveProcess方法当Intent要启动其他应用的Activity时系统会检查数据URI是否合规。如果发现是file://格式且目标应用与当前应用不在同一个Linux用户ID下就会触发这个保护机制。2.2 Android权限模型的演变在Android 7.0之前应用可以通过file://URI自由共享文件。但这样会带来严重的安全问题任何应用都可以读取这些文件无法精确控制访问权限文件路径可能包含敏感信息FileProvider的引入解决了这些问题通过content://URI替代file://支持临时权限授予隐藏真实文件路径支持路径访问限制3. 四步解决FileUriExposedException3.1 第一步声明FileProvider在AndroidManifest.xml中添加provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.files android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider关键参数说明authorities建议使用应用包名作为前缀exported设为false确保只有你的应用能访问grantUriPermissions允许临时授权3.2 第二步配置共享路径创建res/xml/file_paths.xml?xml version1.0 encodingutf-8? paths external-path nameexternal_files path. / external-files-path nameexternal_files_path path. / cache-path namecache_path path. / /paths路径类型说明external-path对应Environment.getExternalStorageDirectory()external-files-pathgetExternalFilesDir()cache-pathgetCacheDir()3.3 第三步改造代码逻辑修改前的危险代码Uri uri Uri.fromFile(file); // 会产生file://URI intent.setDataAndType(uri, video/*);安全改造后的代码Uri uri FileProvider.getUriForFile( context, context.getPackageName() .files, // 与manifest中一致 file); intent.setDataAndType(uri, video/*); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);3.4 第四步处理接收方兼容性有些老版本应用可能不支持content://URI需要特殊处理if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { uri FileProvider.getUriForFile(...); } else { uri Uri.fromFile(file); }4. 实际开发中的五个坑点4.1 路径配置错误最常见的错误是file_paths.xml中路径配置不匹配。比如!-- 错误示例 -- external-path pathPictures /当尝试访问/storage/emulated/0/DCIM/file时就会失败因为路径不匹配。正确做法是使用.表示根目录或者完整路径external-path pathDCIM/ /4.2 权限未正确授予忘记添加FLAG_GRANT_READ_URI_PERMISSION会导致接收方无权限访问。更稳妥的做法是intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);4.3 多模块冲突当应用有多个模块时可能会出现FileProvider冲突。解决方法是在每个模块使用不同的authorities!-- 主模块 -- android:authorities${applicationId}.main_files !-- 功能模块 -- android:authorities${applicationId}.feature_files4.4 文件路径变化有些手机厂商会修改存储路径比如华为的内部存储/外部存储逻辑不同。建议使用Context获取路径File file new File(context.getExternalFilesDir(null), video.mp4);4.5 第三方应用兼容测试发现微信7.0以下版本对content://URI支持不完善需要特殊处理if (isWeChat() !isWeChatVersionSupported()) { uri getUriThroughPublicDirectory(file); }5. 高级应用场景5.1 分享多个文件通过Intent.createChooser分享多个文件时需要为每个URI单独授权ArrayListUri uris new ArrayList(); Intent intent new Intent(Intent.ACTION_SEND_MULTIPLE); for (File file : files) { Uri uri FileProvider.getUriForFile(...); uris.add(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);5.2 自定义FileProvider当需要特殊处理时可以继承FileProviderpublic class CustomFileProvider extends FileProvider { Override public boolean onCreate() { // 自定义初始化逻辑 return super.onCreate(); } }然后在manifest中使用自定义类android:name.CustomFileProvider5.3 动态路径配置通过覆写getFileForUri实现动态路径Override public File getFileForUri(Uri uri) { if (uri.getPath().contains(/special/)) { return handleSpecialCase(uri); } return super.getFileForUri(uri); }6. 测试与验证技巧6.1 单元测试方案使用AndroidX Test测试FileProviderRunWith(AndroidJUnit4.class) public class FileProviderTest { Test public void testFileUriConversion() { Context context ApplicationProvider.getApplicationContext(); File file new File(context.getFilesDir(), test.txt); Uri uri FileProvider.getUriForFile( context, context.getPackageName() .files, file); assertThat(uri.getScheme()).isEqualTo(content); } }6.2 真机调试技巧在开发者选项中开启严格模式可以提前发现URI暴露问题adb shell settings put global strict_mode_visible 16.3 常见错误码监控这些关键日志FileUriExposedExceptionURI暴露SecurityException权限不足IllegalArgumentException路径配置错误7. 性能优化建议7.1 URI缓存机制频繁获取URI会有性能开销可以建立缓存private static LruCacheString, Uri uriCache new LruCache(100); public static Uri getCachedUri(Context context, File file) { String key file.getAbsolutePath(); Uri uri uriCache.get(key); if (uri null) { uri FileProvider.getUriForFile(...); uriCache.put(key, uri); } return uri; }7.2 批量授权优化当需要授权给多个应用时使用PackageManager批量检查ListResolveInfo apps pm.queryIntentActivities(intent, 0); for (ResolveInfo app : apps) { context.grantUriPermission( app.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); }8. 替代方案对比8.1 使用MediaStore对于媒体文件MediaStore是更现代的方案ContentValues values new ContentValues(); values.put(MediaStore.Video.Media.DATA, file.getAbsolutePath()); Uri uri context.getContentResolver().insert( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);8.2 使用Storage Access Framework通过Intent.ACTION_OPEN_DOCUMENT获取长期访问权限Intent intent new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(video/*); startActivityForResult(intent, REQUEST_CODE);在开发视频编辑类应用时我花了整整两天时间排查各种文件共享问题。最深刻的教训是不要假设所有设备的行为都一致华为、小米等厂商的定制ROM可能会带来意想不到的问题。现在我的调试清单上永远有一条检查FileProvider配置是否完整。