Android 7.0 Nougat 不引入Support库如何实现FileProvider功能
零依赖实现Android 7.0+ 等效FileProvider文件共享方案
Support库提供的FileProvider本质是对系统原生ContentProvider的轻量封装,没有特殊私有API依赖,完全可以自行实现同等逻辑,无需引入Support v4或AndroidX组件,配合系统版本分支判断即可覆盖所有场景。
版本分支规则
- Android 6.0(API 23)及以下:直接使用原有
file://格式Uri传递文件,无需额外处理 - Android 7.0(API 24)及以上:使用自定义原生ContentProvider生成
content://格式Uri,为接收方授予临时文件访问权限,完全规避FileUriExposedException
具体实现
1. 实现原生文件提供者
直接继承系统框架层的android.content.ContentProvider,实现文件访问、查询的核心逻辑:
import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; public class NativeFileProvider extends ContentProvider { private static final String[] DEFAULT_COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}; private File sharedRootDir; @Override public boolean onCreate() { // 可根据业务需求替换为需要共享的根目录,如外部缓存目录getExternalCacheDir() sharedRootDir = getContext().getFilesDir(); return true; } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { File targetFile = getFileFromUri(uri); int accessMode = ParcelFileDescriptor.MODE_READ_ONLY; if (mode.contains("w")) accessMode |= ParcelFileDescriptor.MODE_WRITE_ONLY; return ParcelFileDescriptor.open(targetFile, accessMode); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { File targetFile = getFileFromUri(uri); if (projection == null) projection = DEFAULT_COLUMNS; MatrixCursor cursor = new MatrixCursor(projection, 1); Object[] rowValues = new Object[projection.length]; for (int i = 0; i < projection.length; i++) { String column = projection[i]; if (OpenableColumns.DISPLAY_NAME.equals(column)) { rowValues[i] = targetFile.getName(); } else if (OpenableColumns.SIZE.equals(column)) { rowValues[i] = targetFile.length(); } } cursor.addRow(rowValues); return cursor; } @Override public String getType(Uri uri) { File targetFile = getFileFromUri(uri); return getContext().getContentResolver().getType(Uri.fromFile(targetFile)); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException("Operation not supported"); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Operation not supported"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException("Operation not supported"); } public static Uri getUriFromFile(Context context, File file) { String targetPath; try { targetPath = file.getCanonicalPath(); } catch (IOException e) { throw new IllegalArgumentException("Invalid file path"); } File rootPath; try { rootPath = context.getFilesDir().getCanonicalFile(); } catch (IOException e) { throw new RuntimeException(e); } if (!targetPath.startsWith(rootPath.getPath())) { throw new IllegalArgumentException("File is outside shared directory"); } String relativePath = targetPath.substring(rootPath.getPath().length() + 1); return new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(context.getPackageName() + ".nativefileprovider") .encodedPath(Uri.encode(relativePath, "/")) .build(); } private File getFileFromUri(Uri uri) { String encodedPath = uri.getEncodedPath(); if (encodedPath == null) throw new IllegalArgumentException("Invalid uri"); String relativePath = Uri.decode(encodedPath.substring(1)); File targetFile = new File(sharedRootDir, relativePath); try { if (!targetFile.getCanonicalPath().startsWith(sharedRootDir.getCanonicalPath())) { throw new SecurityException("Illegal path access"); } } catch (IOException e) { throw new IllegalArgumentException("Invalid file"); } return targetFile; } }
2. 清单文件注册组件
在AndroidManifest.xml中注册自定义Provider,关闭公开访问权限、开启临时Uri授权:
<provider android:name=".NativeFileProvider" android:authorities="${applicationId}.nativefileprovider" android:exported="false" android:grantUriPermissions="true" />
3. 跨应用文件传递逻辑
在需要向外部应用传递文件的位置,按系统版本走分支逻辑:
public void dispatchFileIntent(Context context, Intent targetIntent, File shareFile) { Uri fileUri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { fileUri = NativeFileProvider.getUriFromFile(context, shareFile); // 授予接收方临时读权限,需要写权限则追加FLAG_GRANT_WRITE_URI_PERMISSION targetIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { fileUri = Uri.fromFile(shareFile); } // 根据业务场景替换Extra字段,如拍照场景用MediaStore.EXTRA_OUTPUT、分享场景用Intent.EXTRA_STREAM targetIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); context.startActivity(targetIntent); }
关键注意事项
- 必须保留路径合法性校验逻辑,拦截
../这类路径遍历请求,避免应用私有目录被恶意越权访问 - 若需要共享多个目录(外部缓存、公共存储目录等),可在Provider内增加路径映射表,逻辑与Support库FileProvider的xml路径配置规则完全一致
- 临时访问权限会跟随接收方组件生命周期自动回收,无需手动释放;如果使用选择器分享文件,建议提前遍历所有匹配Intent的应用包名,主动调用
grantUriPermission授权,避免部分定制系统上Intent flag授权失效 - 该实现所有API均为系统原生公开API,不存在版本兼容问题,在Android 7.0及以上系统的行为与官方Support库FileProvider完全一致
内容的提问来源于stack exchange,提问作者htfree
相关产品推荐
相关产品推荐

