Android 7.0强制启用了被称作 StrictMode的策略,带来的影响就是你的App对外无法暴露file://
类型的URI了。
如果你使用Intent携带这样的URI去打开外部App(比如:打开系统相机拍照),那么会抛出FileUriExposedException异常
Android7.0系统中添加了一个新的设置,采用新的方式FileProvider访问文件系统。下面结合源码对FileProvider的工作流程和实现做一个简单分析。
FileProvider是一个继承自ContentProvider的类,因此他的使用方式也和ContentProvider比较像。
首先在Application中注册:
这样我们就声明了我们要使用的FileProvider。其中meta-data中的android:resource="@xml/file_path"是我们自己要创建的一个xml文件。对应在Androidstudio res资源文件夹的xml下。按照规定我们在这个xml文件中要设置一些信息:
跟节点我们自己在paths节点下可以添加固定的路径信息。比如上面添加了两个节点external-files-path和external-path。还有那些我们可以添加的节点呢?从FileProvider的源码中我们可以知道还有
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
如上这些设置。他们分别对应了不同的存储比如上面的external-files-path和external-path分别对应了
context.getExternalFilesDir("downloadAPP")
Environment.getExternalStoragePublicDirectory
下面我们分析一下是怎么来的:
7.0系统以前我们获取要给Uri方式:
Uri uri = Uri.fromFile(apkFile);
使用FileProvider后规定:
Uri uri = FileProvider.getUriForFile(context,
BuildConfig.APPLICATION_ID + ".provider", apkFile);
我们添加了一个ContentProvider系统在启动App引用的时候就会加载它从而调用FileProvider的方法
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
......
mStrategy = getPathStrategy(context, info.authority);
}
这里我们看到在attachInfo方法中调用了getPathStrategy()返回了一个PathStrategy mStrategy。跟踪进入代码:
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
主要就三个地方:
第一句strat = sCache.get(authority);
第二个句strat = parsePathStrategy(context, authority);
第三个句sCache.put(authority, strat);
而sCache是一个
@GuardedBy("sCache")
private static HashMap sCache = new HashMap();
用来存储获取的PathStrategy。我们第一次加载FileProvider的时候这个Map肯定是空所以进入第二句执行解析。然后把结果添加到Map中保存。
查看一下parsePathStrategy()方法
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// 开始解析xml
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
// 遍历 节点
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
// 获取我们定义的xml 中的name和path节点
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();// 对应的getFilesDir
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();// 对应的getExternalStorageDirectory()
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);// 对应的方法
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
我们发现,在这个方法中,解析了我们定义的xml文件,不断循环获取我们添加的节点。这个解析xml的过程是从根节点resource开始的,一层一层,到path,然后解析我们设置的节点。这里也能看出,通过循环会把我们设置的多个节点都解析出来。
最后把找到的节点赋值给target。然后调用了strat.addRoot(name, buildPath(target, path));这句代码设置。我们点击查看对应的TAG_FILES_PATH、TAG_EXTERNAL、TAG_EXTERNAL_FILES等发现就是在前面定义了常量
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
这样我们就知道了这些常量和我们xml中是怎么对应的。同时每个标签对应的java获取路径的方法是什么。
TAG_FILES_PATH = "files-path" 对应context.getFilesDir();
TAG_CACHE_PATH = "cache-path" 对应context.getCacheDir();
TAG_EXTERNAL = "external-path"对应Environment.getExternalStorageDirectory();
TAG_EXTERNAL_FILES = "external-files-path"对应
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
TAG_EXTERNAL_CACHE = "external-cache-path"对应
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
每个不同的方法对应的文件的目录是不同的,我们在xml中设置节点的时候就可以参考这里的配置了。
最后看一下start.addRoot()方法做了什么。
public void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
// 解析出一个File
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
//保存File
mRoots.put(name, root);
}
查看一下mRoots是SimplePathStrategy内部类定义的一个Map
private final HashMap mRoots = new HashMap();
这个Map又保存了解析出来的File。
这样我们的getPathStrategy(Context context, String authority)方法就执行完了,最后保存了一个SimplePathStrategy到Map中sCache.put(authority, strat);
现在我们从获取Uri的那句代码查看一下:
FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".provider", file);
public static Uri getUriForFile(Context context, String authority, File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
这个getUriForFile()方法中刚好调用了刚刚查看的方法getPathStrategy(context, authority);然后调用了返回结果PathStrategy对象的方法strategy.getUriForFile(file);来获取一个Uri。我们知道这个strategy就是前面创建的SimplePathStrategy对象。我们查看这个方法:
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry mostSpecific = null;
// 循环从mRoots这个Map中获取保存的数据
for (Map.Entry root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
// 抛出一个异常
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
关键的地方就是那个for循环了,从mRoots中不断取出root来比较。我们知道这个mRoots就是前面我们保存File的那个Map集合。这里就是取出那个File然后比较。
Debug一下这段代码发现如下
我们还记得前面执行getPathStrategy(Context context, String authority)方法的时候有三居主要的代码,第一句是
strat = sCache.get(authority);
从sCache这个Map中取出一个authority对应的start返回。从前面的代码中我们知道这个authority就是调用
FileProvider.getUriForFile(context,
BuildConfig.APPLICATION_ID + ".provider", apkFile);
这个方法的时候传递的第二个参数,我们的provider这个报名拼接的字符串。
我们重新看一下这段代码
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
//根据authority从Map中获取保存的strat
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
//根据authority保存strat对象
sCache.put(authority, strat);
}
}
return strat;
}
也就是把authority这个字符串作为了Map的Key来保存的,这是唯一的。
看上面的图。我们知道了,也就是在App加载的时候解析我们的Provider根据authority保存下来。当我们执行getUriForFile(File file)获取一个Uri的时候从我们保存的Map中又去查找这个provider。对比我们调用FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);方法时第三个参数的file 的path。这个path就是前面代码getUriForFile(File file)的这句path = file.getCanonicalPath();从前面debug的截图中可以看出这个path就是我们传递的一个文件在磁盘上的路径。循环中的root就是我们在xml中配置的如external-files-path对应的保存数据的目录。
for (Map.Entry root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
//传递的文件是否和我们设置的xml中的存储路径匹配
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
如果传递的文件的路径要是和我们xml中设置的文件的路径匹配就继续,否则就会抛出一个我们刚开始设置的时候常见的一个异常:“Failed to find configured root that contains”!
最后就是通过我们传递的文件获取路径拼接创建Uri。
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
// 获取path
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
// 拼接
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
// 创建Uri返回
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
至此我们就基本搞清楚了FileProvider的工作流程。如何设置xml,如何解析,对应那个存储路径。