實現原理
初始化
進程啟動流程簡述
客戶端進程啟動 -> ActivityThread#main
-> ActivityThread#attach
-> ActivityManagerNative#attachApplication
-> ActivitymanagerService#attachApplication
在 ActivitymanagerService#attachApplication
方法中會調用到 ActivitymanagerService#generateApplicationProvidersLocked
這個方法,這個方法通過 PKMS
去獲取解析后的應用的清單文件中 provider
信息,為每個 provider
新建 ContentProviderRecord
作為 AMS
端的 ContentProvider
表現
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
信息如下
遍歷 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
接口和繼承 Binder
的 Transport
類,這個對象很重要,作為 binder
本地對象提供服務,記錄清單記錄的權限信息接著調用 ContentProvider#onCreate
方法,后還需要在 ActivityThread
中以 ProviderClientRecord
對象作為表示并記錄在 ActivityThread
,建立了 ContentProvider
和 ProviderClientRecord
的聯系
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
在不同場景的表現,其中最重要的是 ContentProvider
、ProviderClientRecord
、ContentProviderRecord
,分別代表了 ContentProvider
的真實實例,ContentProvider
記錄在 ActivityThread
緩存中的記錄和在 AMS
中的記錄,另外的 ContentProviderHolder
將作為服務/客戶進程和 AMS
間的信息載體,詳細描述和關系如下
數據處理操作
同一進程訪問
對于同一進程對 ContentProvider
的訪問比較簡單,因為進程啟動的時候已經對 ContentProvider
進行了安裝并初始化,所以可以在 ActivityThread
中直接找到緩存的 IContentProvider
,直接操作
同一應用非同一進程訪問
單實例還是多實例?
對于同一應用的非同一進程訪問需要通過 AMS
來找到目標的 IContentProvider
對象并進行通訊,如果目標的進程還未啟動,那么還需要先把目標進程啟動,過程就有點繁瑣了,所以這里直接假設目標進程已經啟動的情況,所以會找到目標的 ContentProviderRecord
對象,其中 cpr.canRunHere
的判斷決定了是否單實例的情況
1、檢測是否能在同一應用內的進程啟動,如果同一進程或者
multiprocess
標志為true
,那么就在目標進程安裝ContentProvider
的實例,這就是多實例的情況,這時候只需要向目標進程發送目標ContentProvider
的ProviderInfo
信息即可,剩下就是目標進程的安裝過程2、單實例的情況,帶上目標
ContentProvider
的ContentProviderRecord
對象
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
: 是否允許外部應用訪問
如若需要提供給外部應用訪問,那么 exported
為 true
,此時需要我們定義外部應用需要的訪問權限,定義使用 <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
實例并注冊到 AMS
,AMS
將保留了提供服務的 ContentProvider
的代理對象 ContentProviderProxy
,當其他進程需要獲取目標 ContentProvider
服務的時候,AMS
進行權限的檢測并在內部找到該代理對象并發送到 Client 進程,所以 Client 也獲得了提供服務的 ContentProvider
的代理對象,然后用這個代理對象就可以和服務進程的 ContentProvider
進行通信,之后不再需要經過 AMS
這一層,所以服務進程的 ContentProvider
也需要在每次數據操作之前進行權限檢測