ContentProvider启动过程分析

前面阅读了BroadcastReceiver的源码
这篇文章也应该是继续看VirtualApk中关于插件ContentProvider的处理的。不过由于处理逻辑类似于ActivityService,所以到最后再看。本文的目的是了解系统对于ContentProvider的整个处理的过程。

ContentProvider是一个可以跨进程的组件,比如我们可以使用通讯录的ContentProvider来获取手机中的通信录信息。ContentResolver封装了ContentProvider跨进程通信的逻辑,使我们在使用ContentProvider时不需要关心这些细节。

那我们在使用context.getContentResolver().query(uri)时发生了什么呢?我们的进程是如何使用其他进程的ContentProvider的呢?
接下来我们就来分析Android系统源码对于ContentProvider的处理,来弄明白这些问题。

ContentProvider的实例化过程

我们从ContextImp.getContentResolver().query()开始看:

public final Cursor query(...) {
    IContentProvider unstableProvider = acquireUnstableProvider(uri); 
}

即首先要获得一个IContentProvider。它是ContentProvider可以跨进交互的一个aidl接口。其实这里拿到的就是一个Binder。所以接下来就看这个IContentProvider(Binder)是如果获取的。

我们在调用ContextImp.getContentResolver()获得的其实是ApplicationContentResolver。因此来看一下它的acquireUnstableProvider():

protected IContentProvider acquireProvider(Context context, String auth) {
    return mMainThread.acquireProvider(context,
                ContentProvider.getAuthorityWithoutUserId(auth),
                resolveUserIdFromAuthority(auth), true);
    }

继续看源码, 切换到主线程ActivityThread.java:

public final IContentProvider acquireProvider(Context c, String auth, int userId, boolean stable) {
    // 如果已经缓存过这个 auth对应的IContentProvider,则直接返回
    final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
    if (provider != null) return provider;

    ContentProviderHolder holder = null;
    
    holder = ActivityManager.getService().getContentProvider(
                    getApplicationThread(), auth, userId, stable);

    //获取失败
    if (holder == null) return null;

    //在向服务端获取holder,服务端如果发现ContentProvider的进程和当前客户端进程是同一个进程就会让客户端进程来实例化ContentProvider,具体细节可以在下面分析中看到
    holder = installProvider(c, holder, holder.info, true /*noisy*/, holder.noReleaseNeeded, stable);

    return holder.provider;
}

即在本地没有获得过IContentProvider时,直接向ActivityManagerService发起getContentProvider的请求,最终调用ActivityManagerService.getContentProviderImpl(), 这个方法就是ContentProvider实例化逻辑的核心了:

首先来看一下这个方法的声明:

ContentProviderHolder getContentProviderImpl(IApplicationThread caller,String name, IBinder token, boolean stable, int userId)

即最终是返回一个ContentProviderHolder,它是什么呢?它其实是一个可以在进程间传递的数据对象(aidl),看一下它的定义:

public class ContentProviderHolder implements Parcelable {
    public final ProviderInfo info;
    public IContentProvider provider;
    public IBinder connection;
    ...

继续看getContentProviderImpl(),这个方法比较长,所以接下来我们分段来看这个方法, 顺序是(1)、(2)、(3)... 这种 :

ActivityManagerService.getContentProviderImpl()(1)

    //三个关键对象
    ContentProviderRecord cpr;
    ContentProviderConnection conn = null;
    ProviderInfo cpi = null;
    ...
    cpr = mProviderMap.getProviderByName(name, userId); // 看看系统是否已经缓存了这个ContentProvider

先来解释一下ContentProviderRecordContentProviderConnectionProviderInfomProviderMap它们大概是什么:

ContentProviderRecord: 它是系统(ActivityManagerService)用来记录一个ContentProvider相关信息的对象。

ContentProviderConnection: 它是一个Binder。连接服务端(ActivityManagerService)和客户端(我们的app)。里面记录着一个ContentProvider的状态,比如是否已经死掉了等。

ProviderInfo: 用来保存一个ContentProvider的信息(manifest中的<provider>), 比如authorityreadPermission等。

mProviderMap: 它的类型是ProviderMap。它里面存在几个map,这些map都是保存ContentProvider的信息的。

ok我们继续来看源码:

ActivityManagerService.getContentProviderImpl()(2)

    cpr = mProviderMap.getProviderByName(name, userId); // 看看系统是否已经缓存了这个ContentProvider
    boolean providerRunning = cpr != null && cpr.proc != null && !cpr.proc.killed;
    if (providerRunning) { 
        ...
    }

    if (!providerRunning) {
        ...
    }

即根据ContentProvider所在的进程是否是活跃这个ContentProvider是否被启动过(缓存下来)两个状态来进行不同的处理 :

ContentProvider已被加载并且所在的进程正在运行

即: if(providerRunning){ ... }中的代码

ActivityManagerService.getContentProviderImpl()(3)

    ProcessRecord r = getRecordForAppLocked(caller); //获取客户端(获得content provider的发起者)的进程信息
    if (r != null && cpr.canRunHere(r)) { //如果请求的ContentProvider和客户端位于同一个进程
        ContentProviderHolder holder = cpr.newHolder(null); //ContentProviderConnection参数传null
        holder.provider = null; //注意,这里置空是让客户端自己去实例化!!
        return holder;
    }

    //客户端进程正在运行,但是和ContentProvider并不在同一个进程
    conn = incProviderCountLocked(r, cpr, token, stable); // 直接根据 ContentProviderRecord和ProcessRecord 构造一个 ContentProviderConnection

    ...

即如果请求的是同进程的ContentProvider则直接回到进程的主线程去实例化ContentProvider。否则使用ContentProviderRecordProcessRecord构造一个ContentProviderConnection

ContentProvider所在的进程没有运行并且服务端(ActivityManagerService)没有加载过它

即: if(!providerRunning){ ... }中的代码

ActivityManagerService.getContentProviderImpl()(4)

    //先解析出来一个ProviderInfo
    cpi = AppGlobals.getPackageManager().resolveContentProvider(name, STOCK_PM_FLAGS | PackageManager.GET_URI_PERMISSION_PATTERNS, userId);
    ...
    ComponentName comp = new ComponentName(cpi.packageName, cpi.name);
    cpr = mProviderMap.getProviderByClass(comp, userId); //这个content provider 没有被加载过

    final boolean firstClass = cpr == null;
    if (firstClass) {
        ...
        cpr = new ContentProviderRecord(this, cpi, ai, comp, singleton); // 构造一个 ContentProviderRecord
    }

    ...

    final int N = mLaunchingProviders.size(); //  mLaunchingProviders它是用来缓存正在启动的 ContentProvider的集合的
    int i;
    for (i = 0; i < N; i++) {
        if (mLaunchingProviders.get(i) == cpr) {  // 已经请求过一次了,provider正在启动,不重复走下面的逻辑
            break;
        }
    }

    //这个 ContentProvider 不是在启动状态,也就是还没启动
    if (i >= N) {
        ProcessRecord proc = getProcessRecordLocked(cpi.processName, cpr.appInfo.uid, false);
        ...
        
         if (proc != null && proc.thread != null && !proc.killed) { //content provider所在的进程已经启动
            proc.thread.scheduleInstallProvider(cpi); //安装这个 Provider , 即客户端实例化它
          } else {
            //启动content provider 所在的进程, 并且唤起 content provider
            proc = startProcessLocked(cpi.processName,cpr.appInfo, false, 0, "content provider",new ComponentName(cpi.applicationInfo.packageName,cpi.name)...);
         }

        cpr.launchingApp = proc;
        mLaunchingProviders.add(cpr); //添加到正在启动的队列
    }

    //缓存 ContentProvider信息
    if (firstClass) {
        mProviderMap.putProviderByClass(comp, cpr);
    }
    mProviderMap.putProviderByName(name, cpr);

    //构造一个 ContentProviderConnection
    conn = incProviderCountLocked(r, cpr, token, stable);
    if (conn != null) {
        conn.waiting = true; //设置这个connection
    }

等待客户端实例化 ContentProvider

ActivityManagerService.getContentProviderImpl()(5)

    // Wait for the provider to be published...
    synchronized (cpr) {
        while (cpr.provider == null) {
            ....
            if (conn != null) {
                conn.waiting = true;
            }
            cpr.wait();
        }
    }

    return cpr != null ? cpr.newHolder(conn) : null; //返回给请求这个客户端的进程

根据前面的分析,ContentProvider所在的进程没有运行或者不是和获取者同一个进程,就创建了一个ContentProviderConnection,那么服务端就会挂起,启动ContentProvider所在的进程,并等待它实例化ContentProvider :

在继续看客户端实例化ContentProvider之前,我们先用一张图来总结一下客户端进程请求服务端(ActivityManagerService)启动一个ContentProvider的逻辑 :

ActivityManagerService对于ContentProvider启动请求的处理.png

客户端实例化ContentProvider

ok,通过前面的分析我们知道ContentProvider最终是在它所在的进程实例化的。接下来就看一下客户端相关代码,

同一个进程中的ContentProvider实例化过程

前面分析我们知道,如果客户端进程请求的ContentProvider位于同一个进程,则ActivityManager.getService().getContentProvider(...);,会返回一个内容为空的ContentProviderHolder,
我们再拿刚开始客户端向服务端请求ContentProvider的代码看一下:

    holder = ActivityManager.getService().getContentProvider( getApplicationThread(), auth, userId, stable);

    //在向服务端获取holder,服务端如果发现ContentProvider的进程和当前客户端进程是同一个进程就会让客户端进程来实例化ContentProvider,具体细节可以在下面分析中看到
    holder = installProvider(c, holder, holder.info, true /*noisy*/, holder.noReleaseNeeded, stable);

我们继续看ActivityThread.installProvider, 这个方法其实有两个逻辑, 下面我只截取一些关键的逻辑,我们现在只看同一个进程中的ContentProvider实例化过程, 即会初始化localProvider的逻辑:

private ContentProviderHolder installProvider(...) {
    ContentProvider localProvider = null;
    IContentProvider provider;

    if (holder == null || holder.provider == null) { //服务端没有缓存过这个provider,客户端需要初始化
        final java.lang.ClassLoader cl = c.getClassLoader();
        LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);

        localProvider = packageInfo.getAppFactory().instantiateProvider(cl, info.name);//实例化ContentProvider
        provider = localProvider.getIContentProvider(); 
        ...
        localProvider.attachInfo(c, info);
    }else {
        provider = holder.provider;
    }

    ContentProviderHolder retHolder;
    IBinder jBinder = provider.asBinder();

    if (localProvider != null) { //同一个进程的ContentProvider
         ProviderClientRecord pr = mLocalProvidersByName.get(cname);
         pr = installProviderAuthoritiesLocked(provider, localProvider, holder); //把Provider缓存起来
        retHolder = pr.mHolder;
    }else{
        ProviderRefCount prc = mProviderRefCountMap.get(jBinder); //其他进程的ContentProvider
        ...
        if(prc == null){
            prc = new ProviderRefCount(holder, client, 1000, 1000);
            //也缓存起来
            ProviderClientRecord client = installProviderAuthoritiesLocked(provider, localProvider, holder);
            mProviderRefCountMap.put(jBinder, prc);
            ...
        }
        retHolder = prc.holder;
    }
    return retHolder;
}

对于这个方法我们暂且知道在客户端进程请求的ContentProvider位于同一个进程时它会: 实例化ContentProvider,缓存起来

不在同一个进程中的ContentProvider实例化过程

如果客户端进程请求的ContentProvider不在同一个进程,根据前面我们分析ActivityManagerService的逻辑可以知道, ActivityManagerService会调用ContentProvider所在进程的proc.thread.scheduleInstallProvider(cpi),
其实最终调用到installContentProviders()

 private void installContentProviders(Context context, List<ProviderInfo> providers) {
        final ArrayList<ContentProviderHolder> results = new ArrayList<>();

        //ActivityManagerService 让客户端启动的是一个ContentProvider列表
        for (ProviderInfo cpi : providers) {
            ContentProviderHolder cph = installProvider(context, null, cpi,false, true ,true);
            if (cph != null) {
                cph.noReleaseNeeded = true;
                results.add(cph);
            }
        }

        ActivityManager.getService().publishContentProviders(getApplicationThread(), results); //通知服务端,content provider ok啦
    }

即它会调用installProvider来实例化ContentProvider,并通知服务端ContentProviderok了,可以给其他进程使用了。

installProvider具体做了什么呢? 前面已经分析过了,其实就是 缓存下其他进程的IContentProvider(Binder)。你可以去看一下上面
installProvider方法的localProvider == null的那个逻辑。

到这里,客户端其实就拿到了IContentProvider(Binder)。 即ContextImp.getContentResolver().query()拿到了IContentProvider。可执行query了。

是不是有点云里雾里的,我们看一下下面这张图,来理一下思路吧:

客户端安装ContentProvider.png

到这里我们算理解了ContentProvider的工作原理, 我们以一个ContentProvider第一次启动为例来总结一下:

  1. 进程在启动ContentProvider时会向ActivityManagerService要,ActivityManagerService如果没有就会让客户端启动这个ContentProvider
  2. 客户端进程启动ContentProvider后就会缓存起来, 方便后续获取
  3. ActivityManagerService只会缓存那些可能跨进程访问的ContentProvider
  4. 和不同进程的ContentProvider通信是通过Binder实现的

VirtualApk关于ContentProvider的处理

VirtualApk它是一个插件化框架,它所需要支持的特性是: 插件中的ContentProvider如何跑起来? 它又没有在manifest中注册。

其实很简单,类似于它对插件Service的支持:

  1. 定义一个占坑的ContentProvider(运行在一个独立的进程)
  2. hook掉插件Activity的Context,并返回自定义的PluginContentResolver
  3. PluginContentResolver在获取ContentProvider时,先把个占坑的ContentProvider唤醒。即让它在ActivityManagerService中跑起来
  4. 返回给插件一个IContentProvider的动态代理。
  5. 插件通过这个IContentProvider动态代理来对ContentProvider做增删改查
  6. 在动态代理中把插件的增删改查的Uri,重新拼接定位到占坑的ContentProvider
  7. 占坑的ContentProvider实例化插件请求的ContentProvider,并做对应的增删该查。

所以:

  1. 插件的ContentProvider是运行在占坑的ContentProvider进程中的。
  2. 插件的ContentProvider是不会运行在自己自定的进程中的,即没有多进程ContentProvider的概念。

欢迎Star我的Android进阶计划,看更多干货

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267