7.0系统中FileProvider使用和源码解析

Android 7.0强制启用了被称作 StrictMode的策略,带来的影响就是你的App对外无法暴露file://类型的URI了。

如果你使用Intent携带这样的URI去打开外部App(比如:打开系统相机拍照),那么会抛出FileUriExposedException异常

Android7.0系统中添加了一个新的设置,采用新的方式FileProvider访问文件系统。下面结合源码对FileProvider的工作流程和实现做一个简单分析。

FileProvider是一个继承自ContentProvider的类,因此他的使用方式也和ContentProvider比较像。

首先在Application中注册

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

这样我们就声明了我们要使用的FileProvider。其中meta-data中的android:resource="@xml/file_path"是我们自己要创建的一个xml文件。对应在Androidstudio res资源文件夹的xml下。按照规定我们在这个xml文件中要设置一些信息:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-files-path
            name="downloadAPP"
            path="" />
        <external-path
            name="images"
            path="kbb/camera" />
    </paths>
</resources>

跟节点我们自己在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<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();

用来存储获取的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<String, File> mRoots = new HashMap<String, File>();

这个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<String, File> mostSpecific = null;
            // 循环从mRoots这个Map中获取保存的数据
            for (Map.Entry<String, File> 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<String, File> 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,如何解析,对应那个存储路径。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值