ContentProvider 源码笔记

作者: HelloCsl | 来源:发表于2017-07-09 11:23 被阅读195次

    实现原理

    初始化

    进程启动流程简述

    客户端进程启动 -> 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

    相关文章

      网友评论

        本文标题:ContentProvider 源码笔记

        本文链接:https://www.haomeiwen.com/subject/xzsshxtx.html