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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容