引子
我們都知道四大組件之一ContentProvider的用處,它給大家提供一種統一的數據訪問格式。調用者無需關心數據源於何處(如DB、XML文件和網絡等),只需獲取到對應的ContentResolver來進行增刪查改即可。
自己實現一個Provider的時候,也會在配置文件中聲明如下:
<provider
android:name=".provider.TestProvider"
android:authorities="com.xxx.yyy.provider"
android:exported="true"
android:readPermission="com.xxx.yyy.permission.READ_PROVIDER" />
其中 authorities
是該Provider的唯一標識,所以一般都寫成包名與其他字符串的組合形式,若需提供數據給其他應用,則 exported
要設爲true,同時比較規範的做法還需要加上讀寫權限。
然後,我們再從常見的查詢操作說起:
ContentResolver r = getContentResolver();
Uri uri = Uri.parse("content://com.xxx.yyy.provider/test_path/1");
Cursor c = r.query(uri, null, null, null, null);
// ...
如同訪問某個網站,我們訪問ContentProvider也需要一個URI,其數據格式:
- scheme前綴是固定的: content://
- 授權host:此例中爲 com.xxx.yyy.provider
- 路徑與參數:此例中爲 test_path/1
那麼,系統是如何通過這樣一個URI來鎖定對應的ContentProvider呢?
找尋
主要涉及源碼(基於Android 10):
frameworks/base/core/java/android/content/ContentResolver.java
frameworks/base/core/java/android/app/ContextImpl.java
frameworks/base/core/java/android/app/ActivityThread.java
大致思路,便是追蹤上述 query
方法中的參數uri,看看它的流向。根據源碼設計的套路,起初幾層調用都是看不到要害之處的,所以我們無需細讀。來來來,先看ContentResolver的 query
方法:
@Override
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal) {
// ...
// 獲取“不穩定”的Provider
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
// ...
try {
// 嘗試查詢操作
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
// 這段註釋我特意沒刪,感覺特別皮。大意:遠程進程已死亡,但我們還持有unstableProvider的引用,快試試回收它的資源!這真是一顆賽艇!(雖然我不知道到底這哪兒exciting了)
unstableProviderDied(unstableProvider);
// “不穩定”的Provider操作失敗,獲取“穩定”的Provider
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
// 再次嘗試查詢操作
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}
// ...
} catch (RemoteException e) {
// ...
return null;
} finally {
// 釋放資源
}
}
從上述源碼可得知,有兩處代碼在根據uri獲取ContentProvider,即ContentResolver的 acquireUnstableProvider
和 acquireProvider
方法。先看看前者(後者最終殊途同歸,本文不額外分析):
public final IContentProvider acquireUnstableProvider(Uri uri) {
if (!SCHEME_CONTENT.equals(uri.getScheme())) {
// 這裏硬核匹配字符串,凡是scheme不是content://的直接再見,所以它是固定的
return null;
}
String auth = uri.getAuthority(); // 按例,此處獲取到的字符串便包含"com.xxx.yyy.provider"
if (auth != null) {
// 此爲ContentResolver中的抽象方法,由子Resolver各自具體實現
return acquireUnstableProvider(mContext, uri.getAuthority());
}
return null;
}
於是我們追蹤到ContextImpl的靜態內部類ApplicationContentResolver:
private static final class ApplicationContentResolver extends ContentResolver {
@UnsupportedAppUsage
private final ActivityThread mMainThread;
// ...
@Override
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
return mMainThread.acquireProvider(c,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), false);
}
}
實際調用到ActivityThread當中去了,注意此時傳遞的關鍵參數已經是 auth 而不是uri了:
@UnsupportedAppUsage
public final IContentProvider acquireProvider(
Context c, String auth, int userId, boolean stable) {
// 獲取已存在的Provider
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
if (provider != null) {
return provider;
}
// ...
// 沒獲取到再嘗試安裝,這裏來個插眼,等會有大用
holder = installProvider(c, holder, holder.info,
true /*noisy*/, holder.noReleaseNeeded, stable);
return holder.provider;
}
看源碼一般來說最好先深後廣,且優先搞清熱點代碼。接下來我們看 acquireExistingProvider
方法:
public final IContentProvider acquireExistingProvider( Context c, String auth, int userId, boolean stable) {
synchronized (mProviderMap) {
final ProviderKey key = new ProviderKey(auth, userId);
// 關注這個存儲Provider記錄的的map,其實這裏就是本文重點
final ProviderClientRecord pr = mProviderMap.get(key);
if (pr == null) {
return null;
}
IContentProvider provider = pr.mProvider; // 最終獲取Provider實例
IBinder jBinder = provider.asBinder();
if (!jBinder.isBinderAlive()) {
// Provider所在進程已死,直接返回null
handleUnstableProviderDiedLocked(jBinder, true);
return null;
}
// ...
return provider;
}
}
分析到這裏,就自然而然有幾個問題了, ProviderKey 是什麼,怎麼構造的? mProviderMap 又是什麼時候填充的?
帶着問題,先看前者:
private static final class ProviderKey {
final String authority;
final int userId;
public ProviderKey(String authority, int userId) {
this.authority = authority;
this.userId = userId;
}
@Override
public boolean equals(Object o) {
// ...
}
@Override
public int hashCode() {
// ...
}
}
可見, ProviderKey 是ActivityThread當中的一個內部POJO,非常普通,沒有對入參做任何特殊處理。那麼ContentProvider也就是根據 authority 和 userId 來唯一確定的,對應了文章開頭的介紹。
此外,由於Android目前是多用戶操作系統(國產ROM淡化了此概念,但應用雙開、系統分身等功能實現均與多用戶有關),所以這裏用戶id是必要的。
接下來看後一個問題, mProviderMap 從哪兒來?什麼時候添加的Provider記錄?很簡單了,還是在ActivityThread當中,實例化如下:
@UnsupportedAppUsage
final ArrayMap<ProviderKey, ProviderClientRecord> mProviderMap
= new ArrayMap<ProviderKey, ProviderClientRecord>();
且僅有一處在進行 put
操作:
private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
ContentProvider localProvider, ContentProviderHolder holder) {
final String auths[] = holder.info.authority.split(";");
final int userId = UserHandle.getUserId(holder.info.applicationInfo.uid);
if (provider != null) {
// ...
}
final ProviderClientRecord pcr = new ProviderClientRecord(
auths, provider, localProvider, holder);
for (String auth : auths) {
final ProviderKey key = new ProviderKey(auth, userId);
final ProviderClientRecord existing = mProviderMap.get(key);
if (existing != null) {
// ...
} else {
mProviderMap.put(key, pcr); // 在此處添加的
}
}
return pcr;
}
可見,ProviderClientRecord實例的構造是在這個 installProviderAuthoritiesLocked
私有方法中完成並添加到map中的。
這裏有個小插曲特別注意:方法的第一行代碼,對 authority 字符串進行了分割(分隔符爲;),最終ProviderClientRecord的數量也取決於分割出來的數組。所以在Manifest配置文件中聲明 android:authorities
屬性時,可以填入多個授權host(就好比多個域名可以同時指向一個網站),以分號分割,難怪屬性名要用複數呢。
接下來看看 installProviderAuthoritiesLocked
方法的調用處:
@UnsupportedAppUsage
private ContentProviderHolder installProvider(Context context,
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;
// ...
}
ContentProviderHolder retHolder;
synchronized (mProviderMap) {
// ...
IBinder jBinder = provider.asBinder();
if (localProvider != null) {
ComponentName cname = new ComponentName(info.packageName, info.name);
ProviderClientRecord pr = mLocalProvidersByName.get(cname);
if (pr != null) {
// ...
} else {
// ...
// 第一處調用
pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
// ...
}
retHolder = pr.mHolder;
} else {
ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
if (prc != null) {
// ...
} else {
// 第二處調用
ProviderClientRecord client = installProviderAuthoritiesLocked(
provider, localProvider, holder);
// ...
}
retHolder = prc.holder;
}
}
return retHolder;
}
由上, installProviderAuthoritiesLocked
方法的調用均在 installProvider
方法中。還記得上文的“插眼”嗎?呼應上了。
總結
-
在我們使用ContentResolver來進行查詢操作時,
query
方法層層調用到 ActivityThread 的acquireExistingProvider
方法,根據URI字符串當中的授權host(即 authority )和當前所在用戶的 userId 來獲取對應的Provider實例。 -
當
acquireExistingProvider
獲取不到時,則通過installProvider
方法來安裝Provider並把其載體 ProviderClientRecord 添加到 mProviderMap 中。 -
AndroidManifest中聲明Provider時,
android:authorities
屬性可以填多個字符串,以分號分割:<provider android:name=".provider.TestProvider" android:authorities="com.xxx.yyy.provider;cn.xxx.yyy.provider;net.xxx.yyy.provider" ... />
如此可以寫成多種不同host的URI,映射的卻還是同一個ContentProvider。具體的好處我能想到的有幾點:
- 與同IP多域名的網站一樣,域名多樣化,提前搶佔一些host,避免三方假冒。
- 提供不同的URI分別給內部和外部開發者使用,便於區分和數據統計。