ContentProvider 源码笔记

实现原理

初始化

进程启动流程简述

客户端进程启动 -> ActivityThread#main -> ActivityThread#attach -> ActivityManagerNative#attachApplication -> ActivitymanagerService#attachApplication

ActivitymanagerService#attachApplication 方法中会调用到 ActivitymanagerService#generateApplicationProvidersLocked 这个方法,这个方法通过 PKMS 去获取解析后的应用的清单文件中 provider 信息,为每个 provider 新建 ContentProviderRecord 作为 AMS 端的 ContentProvider 表现

ContentProvider安装流程.png
ActivityManagerService.java

private final List<ProviderInfo> generateApplicationProvidersLocked(ProcessRecord app) {
    List<ProviderInfo> providers = null;
    providers = AppGlobals.getPackageManager().queryContentProviders(...);  //PKMS 获取清单中的 <providers/> 节点信息
    int userId = app.userId;
    if (providers != null) {
        int N = providers.size();
        app.pubProviders.ensureCapacity(N + app.pubProviders.size());
        for (int i=0; i<N; i++) {
            ProviderInfo cpi = (ProviderInfo)providers.get(i);
            // 带 flag:FLAG_SINGLE_USER,且需要有 android.Manifest.permission.INTERACT_ACROSS_USERS 权限
            boolean singleton = isSingleton(cpi.processName, cpi.applicationInfo, cpi.name, cpi.flags);
            if (singleton && UserHandle.getUserId(app.uid) != 0) {
                providers.remove(i);
                N--;
                i--;
                continue;
            }
            ComponentName comp = new ComponentName(cpi.packageName, cpi.name);
            ContentProviderRecord cpr = mProviderMap.getProviderByClass(comp, userId);
            if (cpr == null) {
                cpr = new ContentProviderRecord(this, cpi, app.info, comp, singleton);
                mProviderMap.putProviderByClass(comp, cpr);
            }
            app.pubProviders.put(cpi.name, cpr);
            if (!cpi.multiprocess || !"android".equals(cpi.packageName)) {
                app.addPackage(cpi.applicationInfo.packageName, mProcessStats);
            }
            //..
        }
    }
    return providers;
}

通过 PKMS 取到 List<ProviderInfo> 列表,最后会通过 IPC 机制传递回 ActivityThread 并在 ActivityThread#handleBindApplication 方法中进行安装,PKMS 解析出来的 ProviderInfo 信息如下

PKMS解析出描述ContentProvider的数据结构.png

遍历 provider 列表,进行安装并通知 AMS 安装完毕

private void installContentProviders( Context context, List<ProviderInfo> providers) {
    final ArrayList<IActivityManager.ContentProviderHolder> results = new ArrayList<IActivityManager.ContentProviderHolder>();
    for (ProviderInfo cpi : providers) {
        IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi, false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
        if (cph != null) {
            cph.noReleaseNeeded = true;
            results.add(cph);
        }
    }
    try {
        ActivityManagerNative.getDefault().publishContentProviders( getApplicationThread(), results);
    } catch (RemoteException ex) {
    }
}

具体安装在 installProvider 方法处理,安装过程会通过反射的方式来新建 ContentProvider,同时新建了一个实现 IContentProvider 接口和继承 BinderTransport 类,这个对象很重要,作为 binder 本地对象提供服务,记录清单记录的权限信息接着调用 ContentProvider#onCreate 方法,后还需要在 ActivityThread 中以 ProviderClientRecord 对象作为表示并记录在 ActivityThread,建立了 ContentProviderProviderClientRecord 的联系


private IActivityManager.ContentProviderHolder installProvider(Context context, IActivityManager.ContentProviderHolder holder, ProviderInfo info,
        boolean noisy, boolean noReleaseNeeded, boolean stable) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    if (holder == null || holder.provider == null) {
        Context c = null;
        ApplicationInfo ai = info.applicationInfo;
        if (context.getPackageName().equals(ai.packageName)) {
            c = context;
        }
        //...
        try {
            final java.lang.ClassLoader cl = c.getClassLoader();
            localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
            provider = localProvider.getIContentProvider(); //Transport , Binder 本地对象
            //...
            localProvider.attachInfo(c, info);  //
        } catch (java.lang.Exception e) {
            //...
        }
    }
    //...
    IActivityManager.ContentProviderHolder retHolder;

    synchronized (mProviderMap) {
        //...
        IBinder jBinder = provider.asBinder();  //Transport , Binder 本地对象
        if (localProvider != null) {
            ComponentName cname = new ComponentName(info.packageName, info.name);
            ProviderClientRecord pr = mLocalProvidersByName.get(cname);
            if (pr != null) {
                //...
            } else {
                holder = new IActivityManager.ContentProviderHolder(info);  //ContentProviderHolder 对象会反馈到 AMS,记录了一个 ContentProvider 的 binder 对象,所以叫 holder 嘛
                holder.provider = provider;
                holder.noReleaseNeeded = true;
                pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
                mLocalProviders.put(jBinder, pr);
                mLocalProvidersByName.put(cname, pr);
            }
            retHolder = pr.mHolder;
        } else {
            //...
        }
    }
    return retHolder;  //ContentProviderHolder 对象会反馈到 AMS
}

正式注册到 AMS

之后还需要通知 AMS 服务端的 ContentProvider 已经安装完了,IActivityManager.ContentProviderHolder 作为和 AMS 联系的信息载体,IActivityManager.ContentProviderHolder 的新建的时候记录了 ContentProvider#mTransport 这个实现了 IContentProvider 接口的 binder 本地对象,最后调用了 publishContentProviders 方法发送到 AMS

前面已经说到 AMS 已经为 ContentProvider 建立了 ContentProviderRecord 对象并记录在其进程对象中,当客户端进程处理完 ContentProvider 的实例化等操作后,将接受到 ContentProviderProxy 这个代理对象,并记录在 ContentProviderRecord#provider 上,以便和目标进程的 ContentProvider 进行通信


public final void publishContentProviders(IApplicationThread caller, List<ContentProviderHolder> providers) {

    synchronized (this) {
        final ProcessRecord r = getRecordForAppLocked(caller);
        //...
        final long origId = Binder.clearCallingIdentity();

        final int N = providers.size();
        for (int i=0; i<N; i++) {
            ContentProviderHolder src = providers.get(i);
            //..
            ContentProviderRecord dst = r.pubProviders.get(src.info.name);  //通过名字可以找到对应要绑定的 ContentProviderRecord
            //..
            if (dst != null) {
                ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);
                mProviderMap.putProviderByClass(comp, dst); //表明注册完毕,记录以供其他用户查询
                String names[] = dst.info.authority.split(";");
                for (int j = 0; j < names.length; j++) {
                    mProviderMap.putProviderByName(names[j], dst);  //表明注册完毕,记录以供其他用户查询
                }
                //...
                synchronized (dst) {
                    dst.provider = src.provider;  //这步很重要,建立了和具体 ContentProvider 联系
                    dst.proc = r;
                    dst.notifyAll();
                }
                //...
            }
        }
        //...
    }
}
ContentProvider注册到AMS.png

这里出现了不少 ContentProvider 在不同场景的表现,其中最重要的是 ContentProviderProviderClientRecordContentProviderRecord,分别代表了 ContentProvider 的真实实例,ContentProvider 记录在 ActivityThread 缓存中的记录和在 AMS 中的记录,另外的 ContentProviderHolder 将作为服务/客户进程和 AMS 间的信息载体,详细描述和关系如下

ContentProvider对象关系.png

数据处理操作

同一进程访问

对于同一进程对 ContentProvider 的访问比较简单,因为进程启动的时候已经对 ContentProvider 进行了安装并初始化,所以可以在 ActivityThread 中直接找到缓存的 IContentProvider,直接操作

ContentProvider同一进程数据操作流程.png

同一应用非同一进程访问

ContentProvider非同一进程数据操作.png

单实例还是多实例?

对于同一应用的非同一进程访问需要通过 AMS 来找到目标的 IContentProvider 对象并进行通讯,如果目标的进程还未启动,那么还需要先把目标进程启动,过程就有点繁琐了,所以这里直接假设目标进程已经启动的情况,所以会找到目标的 ContentProviderRecord 对象,其中 cpr.canRunHere 的判断决定了是否单实例的情况

  • 1、检测是否能在同一应用内的进程启动,如果同一进程或者 multiprocess 标志为 true,那么就在目标进程安装 ContentProvider 的实例,这就是多实例的情况,这时候只需要向目标进程发送目标 ContentProviderProviderInfo 信息即可,剩下就是目标进程的安装过程

  • 2、单实例的情况,带上目标 ContentProviderContentProviderRecord 对象


private final ContentProviderHolder getContentProviderImpl(IApplicationThread caller,String name, IBinder token, boolean stable, int userId) {
    ContentProviderRecord cpr;
    ContentProviderConnection conn = null;
    ProviderInfo cpi = null;

    synchronized(this) {
        ProcessRecord r = null;
        if (caller != null) {
            r = getRecordForAppLocked(caller);  //目标进程
            //...
        }

        // 检测目标 ContentProvider 是否存在
        cpr = mProviderMap.getProviderByName(name, userId);
        boolean providerRunning = cpr != null;
        if (providerRunning) {
            cpi = cpr.info;
            String msg;
            //权限检测
            if ((msg=checkContentProviderPermissionLocked(cpi, r)) != null) {
                throw new SecurityException(msg);
            }

            if (r != null && cpr.canRunHere(r)) {
                // multiprocess 为 ture,支持应用内多进程,所以 ContentProvider 需要在目标进程实例化
                ContentProviderHolder holder = cpr.newHolder(null);
                holder.provider = null;
                return holder;
            }

            final long origId = Binder.clearCallingIdentity();

            // 单实例的情况
            conn = incProviderCountLocked(r, cpr, token, stable);
            //...
        }
        //...
    }//synchronized
    // Wait for the provider to be published...
    synchronized (cpr) {
        while (cpr.provider == null) {
          //...
        }
    }
    return cpr != null ? cpr.newHolder(conn) : null;
}

客户端的安装

主要是根据 AMS 发来的 ContentProviderHolder 是否为 null 作响应,如果为 null ,那么和进程启动并安装 ContentProvider 的流程一样,所以直接看非空的情况,客户端收到的将是一个 ContentProviderProxy 代理对象,将用来和目标 ContentProvider 进行通信


private IActivityManager.ContentProviderHolder installProvider(Context context, IActivityManager.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; //AMS 发来的,这里会是一个 ContentProviderProxy 对象
    }

    IActivityManager.ContentProviderHolder retHolder;

    synchronized (mProviderMap) {
        //...
        IBinder jBinder = provider.asBinder();
        if (localProvider != null) {
          //..
        } else {
            ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
            if (prc != null) {
                //..
                // We need to transfer our new reference to the existing
                // ref count, releasing the old one...  but only if
                // release is needed (that is, it is not running in the
                // system process).
                if (!noReleaseNeeded) {
                    incProviderRefLocked(prc, stable);
                    try {
                        ActivityManagerNative.getDefault().removeContentProvider( holder.connection, stable);
                    } catch (RemoteException e) {
                        //do nothing content provider object is dead any way
                    }
                }
            } else {
                //本地进程缓存记录
                ProviderClientRecord client = installProviderAuthoritiesLocked( provider, localProvider, holder);
                if (noReleaseNeeded) {
                    prc = new ProviderRefCount(holder, client, 1000, 1000);
                } else {
                    prc = stable ? new ProviderRefCount(holder, client, 1, 0) : new ProviderRefCount(holder, client, 0, 1);
                }
                mProviderRefCountMap.put(jBinder, prc);
            }
            retHolder = prc.holder;
        }
    }

    return retHolder;
}

权限检测

ContentProvider 可以通过以下标志进行权限限制

  • android:permission : 外部应用访问所需要的读取/写入权限
  • android:readPermission : 外部应用访问需要的读取权限
  • android:writePermission : 外部应用访问需要的写入权限
  • android:exported : 是否允许外部应用访问

如若需要提供给外部应用访问,那么 exportedtrue,此时需要我们定义外部应用需要的访问权限,定义使用 <permission/> 来定义,此时需要认真的考虑 protectionLevel,一般的话一个公司打包签名 APP 的签名证书都应该是一致的,这种情况下,Provider 的 android:protectionLevel 应为设为 signature 比较合适

AMS 端的检测

AMS 对应用请求的 ContentProvider 使用前会先进行权限检测,包括 writePermission(同 appId 不检测)、readPermission(同 appId 不检测)、pathPermissions(同 appId 不检测)、exported(是否可供外部应用访问)、必要的时候还要用 PKMS#checkUidPermission 方法进行权限检测

private final String checkContentProviderPermissionLocked(ProviderInfo cpi, ProcessRecord r) {
    final int callingPid = (r != null) ? r.pid : Binder.getCallingPid();  //同一应用,可以多个 PID
    final int callingUid = (r != null) ? r.uid : Binder.getCallingUid();  //同一应用,只有一个 uid
    if (checkComponentPermission(cpi.readPermission, callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
            == PackageManager.PERMISSION_GRANTED) {
        return null;
    }
    if (checkComponentPermission(cpi.writePermission, callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
            == PackageManager.PERMISSION_GRANTED) {
        return null;
    }

    PathPermission[] pps = cpi.pathPermissions;
    if (pps != null) {
        int i = pps.length;
        while (i > 0) {
            i--;
            PathPermission pp = pps[i];
            if (checkComponentPermission(pp.getReadPermission(), callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
                    == PackageManager.PERMISSION_GRANTED) {
                return null;
            }
            if (checkComponentPermission(pp.getWritePermission(), callingPid, callingUid, cpi.applicationInfo.uid, cpi.exported)
                    == PackageManager.PERMISSION_GRANTED) {
                return null;
            }
        }
    }

    ArrayMap<Uri, UriPermission> perms = mGrantedUriPermissions.get(callingUid);
    if (perms != null) {
        for (Map.Entry<Uri, UriPermission> uri : perms.entrySet()) {
            if (uri.getKey().getAuthority().equals(cpi.authority)) {
                return null;
            }
        }
    }
    String msg;
    if (!cpi.exported) {
        msg = "Permission Denial: opening provider "...
    } else {
        msg = "Permission Denial: opening provider " ...
    }
    Slog.w(TAG, msg);
    return msg;
}
ActivityManager.java

//没个应用分配一个 uid,一个应用可能有多个进程,就拥有不同的 pid,但是其应用内进程共享同一个 uid
public static int checkComponentPermission(String permission, int uid, int owningUid, boolean exported) {
    // 系统服务和 root 用户,直接授权
    if (uid == 0 || uid == Process.SYSTEM_UID) {
        return PackageManager.PERMISSION_GRANTED;
    }
    // Isolated processes don't get any permissions.
    if (UserHandle.isIsolated(uid)) {
        return PackageManager.PERMISSION_DENIED;
    }
    // 同一个应用,不需要检测
    if (owningUid >= 0 && UserHandle.isSameApp(uid, owningUid)) {
        return PackageManager.PERMISSION_GRANTED;
    }
    // If the target is not exported, then nobody else can get to it.
    if (!exported) {
        return PackageManager.PERMISSION_DENIED;
    }
    if (permission == null) {
        return PackageManager.PERMISSION_GRANTED;
    }
    try {
        return AppGlobals.getPackageManager().checkUidPermission(permission, uid);
    } catch (RemoteException e) {
        Slog.e(TAG, "PackageManager is dead?!?", e);
    }
    return PackageManager.PERMISSION_DENIED;
}

ContentProvider 端的检测

目标 ContentProvider 被访问的时候每次也需要再进行权限检测,主要在 Transport 作为 ContentProvider 的代理类,其 enforceXXXPermission 等方法用来进行权限检测,这里的检测内容其实和 AMS 类似的

检测的内容:

ContentProvider$Transport.java

private int enforceReadPermission(String callingPkg, Uri uri) throws SecurityException {
    enforceReadPermissionInner(uri);  //在 ContentProvider 中实现
    if (mReadOp != AppOpsManager.OP_NONE) { //默认不走 if 的逻辑
        return mAppOpsManager.noteOp(mReadOp, Binder.getCallingUid(), callingPkg);
    }
    return AppOpsManager.MODE_ALLOWED;
}
ContentProvider.java

protected void enforceReadPermissionInner(Uri uri) throws SecurityException {
     final Context context = getContext();
     final int pid = Binder.getCallingPid();
     final int uid = Binder.getCallingUid();
     String missingPerm = null;

     if (UserHandle.isSameApp(uid, mMyUid)) { //是否同一应用
         return;
     }

     if (mExported) { //是否提供给其他进程使用
         final String componentPerm = getReadPermission();  
         if (componentPerm != null) {
             if (context.checkPermission(componentPerm, pid, uid) == PERMISSION_GRANTED) {  // 向 ams 查询目标应用是否具有权限,还是会走到 `ActivityManager.checkComponentPermission` 这个方法
                 return;
             } else {
                 missingPerm = componentPerm;
             }
         }
         // track if unprotected read is allowed; any denied
         // <path-permission> below removes this ability
         boolean allowDefaultRead = (componentPerm == null);

         final PathPermission[] pps = getPathPermissions();
         if (pps != null) {
             final String path = uri.getPath();
             for (PathPermission pp : pps) {
                 final String pathPerm = pp.getReadPermission();
                 if (pathPerm != null && pp.match(path)) {
                     if (context.checkPermission(pathPerm, pid, uid) == PERMISSION_GRANTED) {
                         return;
                     } else {
                         // any denied <path-permission> means we lose
                         // default <provider> access.
                         allowDefaultRead = false;
                         missingPerm = pathPerm;
                     }
                 }
             }
         }

         // if we passed <path-permission> checks above, and no default
         // <provider> permission, then allow access.
         if (allowDefaultRead) return;
     }

     // last chance, check against any uri grants
     if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) {
         return;
     }

     final String failReason = mExported
             ? " requires " + missingPerm + ", or grantUriPermission()"
             : " requires the provider be exported, or grantUriPermission()";
             //...
 }

关于 ContentProvider 的安全问题,请阅读 Android安全开发之Provider组件安全-阿里聚安全创建 ContentProvider

小结

上面的过程可以简述为目标进程启动的时候新建 ContentProvider 实例并注册到 AMSAMS 将保留了提供服务的 ContentProvider 的代理对象 ContentProviderProxy,当其他进程需要获取目标 ContentProvider 服务的时候,AMS 进行权限的检测并在内部找到该代理对象并发送到 Client 进程,所以 Client 也获得了提供服务的 ContentProvider 的代理对象,然后用这个代理对象就可以和服务进程的 ContentProvider 进行通信,之后不再需要经过 AMS 这一层,所以服务进程的 ContentProvider 也需要在每次数据操作之前进行权限检测

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

推荐阅读更多精彩内容