Android技术知识Android知识Android开发经验谈

ContentProvider 源码笔记

2017-07-09  本文已影响195人  HelloCsl

实现原理

初始化

进程启动流程简述

客户端进程启动 -> 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 的判断决定了是否单实例的情况


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 可以通过以下标志进行权限限制

如若需要提供给外部应用访问,那么 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
上一篇 下一篇

猜你喜欢

热点阅读