源碼茶舍之如何由Uri找尋ContentProvider

引子

咱們都知道四大組件之一ContentProvider的用處,它給你們提供一種統一的數據訪問格式。調用者無需關心數據源於何處(如DB、XML文件和網絡等),只需獲取到對應的ContentResolver來進行增刪查改便可。 本身實現一個Provider的時候,也會在配置文件中聲明以下:java

<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,同時比較規範的作法還須要加上讀寫權限。 而後,咱們再從常見的查詢操做提及:android

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呢?bash

找尋

主要涉及源碼(基於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 方法:app

@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的 acquireUnstableProvideracquireProvider 方法。先看看前者(後者最終異曲同工,本文不額外分析):ide

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了:ui

@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 方法:this

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也就是根據 authorityuserId 來惟一肯定的,對應了文章開頭的介紹。 此外,因爲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 方法層層調用到 ActivityThreadacquireExistingProvider 方法,根據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分別給內部和外部開發者使用,便於區分和數據統計。
相關文章
相關標籤/搜索