源码茶舍之如何由Uri找寻ContentProvider

引子

我们都知道四大组件之一ContentProvider的用处,它给大家提供一种统一的数据访问格式。调用者无需关心数据源于何处(如DB、XML文件和网络等),只需获取到对应的ContentResolver来进行增删查改即可。
自己实现一个Provider的时候,也会在配置文件中声明如下:

<provider
    android:name=".provider.TestProvider"
    android:authorities="com.xxx.yyy.provider"
    android:exported="true"
    android:readPermission="com.xxx.yyy.permission.READ_PROVIDER" />

其中 authorities 是该Provider的唯一标识,所以一般都写成包名与其他字符串的组合形式,若需提供数据给其他应用,则 exported 要设为true,同时比较规范的做法还需要加上读写权限。
然后,我们再从常见的查询操作说起:

ContentResolver r = getContentResolver();
Uri uri = Uri.parse("content://com.xxx.yyy.provider/test_path/1");
Cursor c = r.query(uri, null, null, null, null);
// ...

如同访问某个网站,我们访问ContentProvider也需要一个URI,其数据格式:

  • scheme前缀是固定的: content://
  • 授权host:此例中为 com.xxx.yyy.provider
  • 路径与参数:此例中为 test_path/1

那么,系统是如何通过这样一个URI来锁定对应的ContentProvider呢?

找寻

主要涉及源码(基于Android 10):

frameworks/base/core/java/android/content/ContentResolver.java
frameworks/base/core/java/android/app/ContextImpl.java
frameworks/base/core/java/android/app/ActivityThread.java

大致思路,便是追踪上述 query 方法中的参数uri,看看它的流向。根据源码设计的套路,起初几层调用都是看不到要害之处的,所以我们无需细读。来来来,先看ContentResolver的 query 方法:

    @Override
    public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable Bundle queryArgs,
            @Nullable CancellationSignal cancellationSignal) {
        // ...
        // 获取“不稳定”的Provider
        IContentProvider unstableProvider = acquireUnstableProvider(uri);
        if (unstableProvider == null) {
            return null;
        }
        IContentProvider stableProvider = null;
        Cursor qCursor = null;
        try {
            // ...
            try {
                // 尝试查询操作
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        queryArgs, remoteCancellationSignal);
            } catch (DeadObjectException e) {
                // The remote process has died...  but we only hold an unstable
                // reference though, so we might recover!!!  Let's try!!!!
                // This is exciting!!1!!1!!!!1
                // 这段注释我特意没删,感觉特别皮。大意:远程进程已死亡,但我们还持有unstableProvider的引用,快试试回收它的资源!这真是一颗赛艇!(虽然我不知道到底这哪儿exciting了)
                unstableProviderDied(unstableProvider);
                // “不稳定”的Provider操作失败,获取“稳定”的Provider
                stableProvider = acquireProvider(uri);
                if (stableProvider == null) {
                    return null;
                }
                // 再次尝试查询操作
                qCursor = stableProvider.query(
                        mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
                }
            if (qCursor == null) {
                return null;
            }
            // ...
        } catch (RemoteException e) {
            // ...
            return null;
        } finally {
            // 释放资源
        }
    }

从上述源码可得知,有两处代码在根据uri获取ContentProvider,即ContentResolver的 acquireUnstableProvideracquireProvider 方法。先看看前者(后者最终殊途同归,本文不额外分析):

    public final IContentProvider acquireUnstableProvider(Uri uri) {
        if (!SCHEME_CONTENT.equals(uri.getScheme())) {
            // 这里硬核匹配字符串,凡是scheme不是content://的直接再见,所以它是固定的
            return null;
        }
        String auth = uri.getAuthority(); // 按例,此处获取到的字符串便包含"com.xxx.yyy.provider"
        if (auth != null) {
            // 此为ContentResolver中的抽象方法,由子Resolver各自具体实现
            return acquireUnstableProvider(mContext, uri.getAuthority());
        }
        return null;
    }

于是我们追踪到ContextImpl的静态内部类ApplicationContentResolver:

    private static final class ApplicationContentResolver extends ContentResolver {
        @UnsupportedAppUsage
        private final ActivityThread mMainThread;
        // ...
        @Override
        protected IContentProvider acquireUnstableProvider(Context c, String auth) {
            return mMainThread.acquireProvider(c,
                    ContentProvider.getAuthorityWithoutUserId(auth),
                    resolveUserIdFromAuthority(auth), false);
        }
    }

实际调用到ActivityThread当中去了,注意此时传递的关键参数已经是 auth 而不是uri了:

    @UnsupportedAppUsage
    public final IContentProvider acquireProvider(
            Context c, String auth, int userId, boolean stable) {
        // 获取已存在的Provider    
        final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
        if (provider != null) {
        	return provider;
        }
        // ...
        // 没获取到再尝试安装,这里来个插眼,等会有大用
        holder = installProvider(c, holder, holder.info,
                true /*noisy*/, holder.noReleaseNeeded, stable);
        return holder.provider;
    }

看源码一般来说最好先深后广,且优先搞清热点代码。接下来我们看 acquireExistingProvider 方法:

    public final IContentProvider acquireExistingProvider( Context c, String auth, int userId, boolean stable) {
        synchronized (mProviderMap) {
            final ProviderKey key = new ProviderKey(auth, userId);
            // 关注这个存储Provider记录的的map,其实这里就是本文重点
            final ProviderClientRecord pr = mProviderMap.get(key);
            if (pr == null) {
                return null;
            }

            IContentProvider provider = pr.mProvider; // 最终获取Provider实例
            IBinder jBinder = provider.asBinder();
            if (!jBinder.isBinderAlive()) {
                // Provider所在进程已死,直接返回null
                handleUnstableProviderDiedLocked(jBinder, true);
                return null;
            }
            // ...
            return provider;
        }
    }

分析到这里,就自然而然有几个问题了, ProviderKey 是什么,怎么构造的? mProviderMap 又是什么时候填充的?
带着问题,先看前者:

    private static final class ProviderKey {
        final String authority;
        final int userId;

        public ProviderKey(String authority, int userId) {
            this.authority = authority;
            this.userId = userId;
        }

        @Override
        public boolean equals(Object o) {
            // ...
        }

        @Override
        public int hashCode() {
            // ...
        }
    }

可见, ProviderKey 是ActivityThread当中的一个内部POJO,非常普通,没有对入参做任何特殊处理。那么ContentProvider也就是根据 authorityuserId 来唯一确定的,对应了文章开头的介绍。
此外,由于Android目前是多用户操作系统(国产ROM淡化了此概念,但应用双开、系统分身等功能实现均与多用户有关),所以这里用户id是必要的。

接下来看后一个问题, mProviderMap 从哪儿来?什么时候添加的Provider记录?很简单了,还是在ActivityThread当中,实例化如下:

    @UnsupportedAppUsage
    final ArrayMap<ProviderKey, ProviderClientRecord> mProviderMap
        = new ArrayMap<ProviderKey, ProviderClientRecord>();

且仅有一处在进行 put 操作:

    private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
            ContentProvider localProvider, ContentProviderHolder holder) {
        final String auths[] = holder.info.authority.split(";");
        final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);

        if (provider != null) {
            // ...
        }

        final ProviderClientRecord pcr = new ProviderClientRecord(
                auths, provider, localProvider, holder);
        for (String auth : auths) {
            final ProviderKey key = new ProviderKey(auth, userId);
            final ProviderClientRecord existing = mProviderMap.get(key);
            if (existing != null) {
                // ...
            } else {
                mProviderMap.put(key, pcr); // 在此处添加的
            }
        }
        return pcr;
    }

可见,ProviderClientRecord实例的构造是在这个 installProviderAuthoritiesLocked 私有方法中完成并添加到map中的。
这里有个小插曲特别注意:方法的第一行代码,对 authority 字符串进行了分割(分隔符为;),最终ProviderClientRecord的数量也取决于分割出来的数组。所以在Manifest配置文件中声明 android:authorities 属性时,可以填入多个授权host(就好比多个域名可以同时指向一个网站),以分号分割,难怪属性名要用复数呢。

接下来看看 installProviderAuthoritiesLocked 方法的调用处:

    @UnsupportedAppUsage
    private ContentProviderHolder installProvider(Context context,
            ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
        ContentProvider localProvider = null;
        IContentProvider provider;
        if (holder == null || holder.provider == null) {
            // ...
        } else {
            provider = holder.provider;
            // ...
        }

        ContentProviderHolder retHolder;

        synchronized (mProviderMap) {
            // ...
            IBinder jBinder = provider.asBinder();
            if (localProvider != null) {
                ComponentName cname = new ComponentName(info.packageName, info.name);
                ProviderClientRecord pr = mLocalProvidersByName.get(cname);
                if (pr != null) {
                    // ...
                } else {
                    // ...
                    // 第一处调用
                    pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
                    // ...
                }
                retHolder = pr.mHolder;
            } else {
                ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
                if (prc != null) {
                    // ...
                } else {
                    // 第二处调用
                    ProviderClientRecord client = installProviderAuthoritiesLocked(
                            provider, localProvider, holder);
                    // ...
                }
                retHolder = prc.holder;
            }
        }
        return retHolder;
    }

由上, installProviderAuthoritiesLocked 方法的调用均在 installProvider 方法中。还记得上文的“插眼”吗?呼应上了。

总结

  • 在我们使用ContentResolver来进行查询操作时,query 方法层层调用到 ActivityThreadacquireExistingProvider 方法,根据URI字符串当中的授权host(即 authority )和当前所在用户的 userId 来获取对应的Provider实例。

  • acquireExistingProvider 获取不到时,则通过 installProvider 方法来安装Provider并把其载体 ProviderClientRecord 添加到 mProviderMap 中。

  • AndroidManifest中声明Provider时, android:authorities 属性可以填多个字符串,以分号分割:

    <provider
        android:name=".provider.TestProvider"
        android:authorities="com.xxx.yyy.provider;cn.xxx.yyy.provider;net.xxx.yyy.provider"
        ... />
    

    如此可以写成多种不同host的URI,映射的却还是同一个ContentProvider。具体的好处我能想到的有几点:

    • 与同IP多域名的网站一样,域名多样化,提前抢占一些host,避免三方假冒。
    • 提供不同的URI分别给内部和外部开发者使用,便于区分和数据统计。
已标记关键词 清除标记
相关推荐
天涯论坛的EXCEL阅读器(用宏写的,适合上班族用) V1.3C是最新更新的! 以下是原作者V1.3b的介绍: 1、可浏览更多内容,V1.3b可支持以下各版面(共20个版面)的内容:    国际观察、舞文弄墨、娱乐八卦、情感天地    开心乐园、天涯杂谈、时尚资讯、煮酒论史    关天、影视评论、音乐天地、闲闲书话    旅游休闲、仗剑天涯、经济论坛、莲蓬鬼话    天涯时空、房产观澜、股市论谈、球迷一家,可以说内容更丰富了,不过占用内存也大了,呵呵   2、增加了跳转至某页的功能,无论多高的楼,都可以跳转到任何一页   3、由于版面较多,版面与帖子之间切换不太方便,因此增加了“显示/隐藏”功能,可以根据各自需要,把不常用的版面隐藏起来,这样看帖子会比较方便,具体方法:按“选项”按钮,在弹出的页面,根据里面提示进行操作就行了,非常方便   4、至多可记录100个你曾经浏览过的帖子,可以随时翻看“历史”记录,具体方法:按“历史”按钮,在弹出窗口中查找,双击打开即可   5、增加了楼层显示,浏览更方便,分页沙发非你莫属   6、可以自动清除程序运行时产生的临时文件,具体方法:按“Clear”按钮         其他注意事项及使用方法,请参照以下相关说明:      关于 Tianya Browser V1.0   1、 本浏览器根据天涯网站的架构设计,专用于浏览天涯论坛,不支持其他网站和论坛。   2、 不能“发帖”和“回帖”,目前版本只能潜水,主要是为了不让网友担心ID和密码被窃。   3、 免费使用,但禁止各网友之间相互传播,以防止“别有用心”之人添加恶意代码,使用时请注意保管好您个人重要信息。   4、本人不是专业程序员,水平很低,Bug在所难免,欢迎善意的交流。   5、目前仅在WindowsXP SP3下测试通过,但只在几台机器上使用过,不能保证在所有机器上都能运行,如有问题,请回帖说明,本人将进行改进。   6、本程序仅供闲暇之余使用,切不可耽误工作,如果网友使用本程序,有违反其工作单位相关规定的,由此造成的减薪、被炒等后果,本人概不负责。   7、本程序为开放式,不涉及任何版权,但请不要恶意使用,有好的想法请不吝赐教。         使用方法:   1、该程序采用vba宏设计,所以EXCEL必须开启宏,相关操作:菜单项 Tools(工具) ->Macro(宏) -> Security(安全),Security level (安全等级)设置成 Low(低),确定后关闭EXCEL窗口,再重新启动EXCEL。   2、进入相应版面点击“刷新”按钮,程序将自动更新该版面内容(耗时取决于你的网速),如果发现内容不全,请再次点击“刷新”按钮。   3、在相应版面上,任意选择想要浏览的帖子的行(任意位置,只要行对应即可),点击“Load Posts”按钮,将自动弹出新页面,并显示该帖子内容(耗时取决于你的网速),如果发现内容不全,请再次点击“刷新”按钮。   4、该程序不能显示图片,如想浏览真实网页和图片,在首页请点击“(首页)" 和 "(窗口)" 按钮,在帖子页请点击"(Browser)"按钮,如果要关闭弹出窗口,请点击”Hide“按钮隐藏窗口,(不要点击右上角的小叉叉,在程序”刷新“的过程中点击小叉叉,会有错误产生)。   5、程序运行后会生成一些临时的文件,临时文件存放在Cache文件夹下,文件可以手动删除,但文件夹必须保留,否则会发生路径错误。
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页