Android插件化原理解析——ContentProvider的插件化

wenzp521 8年前發布 | 19K 次閱讀 安卓開發 Android開發 移動開發

目前為止我們已經完成了Android四大組件中Activity,Service以及BroadcastReceiver的插件化,這幾個組件各不相同,我們根據它們的特點定制了不同的插件化方案;那么對于ContentProvider,它又有什么特點?應該如何實現它的插件化?

與Activity,BroadcastReceiver等頻繁被使用的組件不同,我們接觸和使用ContentProvider的機會要少得多;但是,ContentProvider這個組件對于Android系統有著特別重要的作用——作為一種極其方便的 數據共享 的手段,ContentProvider使得廣大第三方App能夠在壁壘森嚴的系統中自由呼吸。

在Android系統中,每一個應用程序都有自己的用戶ID,而每一個應用程序所創建的文件的讀寫權限都是只賦予給自己所屬的用戶,這就限制了應用程序之間相互讀寫數據的操作。應用程序之間如果希望能夠進行交互,只能采取跨進程通信的方式;Binder機制能夠滿足一般的IPC需求,但是如果應用程序之間需要共享大量數據,單純使用Binder是很難辦到的——我相信大家對于Binder 1M緩沖區以及TransactionTooLargeException一定不陌生;ContentProvider使用了匿名共享內存(Ashmem)機制完成數據共享,因此它可以很方便地完成大量數據的傳輸。Android系統的短信,聯系人,相冊,媒體庫等等一系列的基礎功能都依賴與ContentProvider,它的重要性可見一斑。

既然ContentProvider的核心特性是數據共享,那么要實現它的插件化,必須能讓插件能夠把它的ContentProvider共享給系統——如果不能「 provide content 」那還叫什么ContentProvider?

但是,如果回想一下Activity等組件的插件化方式,在涉及到「共享」這個問題上,一直沒有較好的解決方案:

  1. 系統中的第三方App無法啟動插件中帶有特定IntentFilter的Activity,因為系統壓根兒感受不到插件中這個真正的Activity的存在。
  2. 插件中的靜態注冊的廣播并不真正是靜態的,而是使用動態注冊廣播模擬實現的;這就導致如果宿主程序進程死亡,這個靜態廣播不會起作用;這個問題的根本原因在由于BroadcastReceiver的IntentFilter的不可預知性,使得我們沒有辦法把靜態廣播真正“共享”給系統。
  3. 我們沒有辦法在第三方App中啟動或者綁定插件中的Service組件;因為插件的Service并不是真正的Service組件,系統能感知到的只是那個代理Service;因此如果插件如果帶有遠程Service組件,它根本不能給第三方App提供遠程服務。

雖然在插件系統中一派生機勃勃的景象,Activity,Service等插件組件百花齊放,插件與宿主、插件與插件在一起愉快滴玩耍;但是一旦脫離了插件系統的溫室,這一片和諧景象不復存在:插件組件不過是傀儡而已,活著的,只有宿主——整個插件系統就是一座鬼城,各個插件組件借尸還魂般地依附在宿主身上,了無生機。

既然希望把插件的ContentProvider共享給整個系統,讓第三方的App都能獲取到我們插件共享的數據,我們必須解決這個問題;下文將會圍繞這個目標展開,完成ContentProvider的插件化,并且順帶給出上述問題的解決方案。閱讀本文之前,可以先clone一份 understand-plugin-framework ,參考此項目的 contentprovider-management 模塊。另外,插件框架原理解析系列文章見索引。

ContentProvider工作原理

首先我們還是得分析一下ContentProvider的工作原理,很多插件化的思路,以及一些Hook點的發現都嚴重依賴于對于系統工作原理的理解;對于ContentProvider的插件化,這一點特別重要。

鋪墊工作

如同我們通過 startActivity 來啟動Activity一樣,與ContentProvider打交道的過程也是從Context類的一個方法開始的,這個方法叫做 getContentResolver ,使用ContentProvider的典型代碼如下:

ContentResolver resolver = content.getContentResolver();
resolver.query(Uri.parse("content://authority/test"), null, null, null, null);

直接去ContextImpl類里面查找的 getContentResolver 實現,發現這個方法返回的類型是android.app.ContextImpl.ApplicationContentResolver,這個類是抽象類android.content.ContentResolver的子類, resolver.query 實際上是調用父類ContentResolver的 query 實現:

public final @Nullable Cursor query(final @NonNull Uri uri, @Nullable String[] projection,
 @Nullable String selection, @Nullable String[] selectionArgs,
 @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
    Preconditions.checkNotNull(uri, "uri");
    IContentProvider unstableProvider = acquireUnstableProvider(uri);
    if (unstableProvider == null) {
        return null;
    }
    IContentProvider stableProvider = null;
    Cursor qCursor = null;
    try {
        long startTime = SystemClock.uptimeMillis();

        ICancellationSignal remoteCancellationSignal = null;
        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
            remoteCancellationSignal = unstableProvider.createCancellationSignal();
            cancellationSignal.setRemote(remoteCancellationSignal);
        }
        try {
            qCursor = unstableProvider.query(mPackageName, uri, projection,
                    selection, selectionArgs, sortOrder, 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
            unstableProviderDied(unstableProvider);
            stableProvider = acquireProvider(uri);
            if (stableProvider == null) {
                return null;
            }
            qCursor = stableProvider.query(mPackageName, uri, projection,
                    selection, selectionArgs, sortOrder, remoteCancellationSignal);
        }
        // 略...
}

注意這里面的那個 try..catch 語句, query 方法首先嘗試調用抽象方法acquireUnstableProvider拿到一個IContentProvider對象,并嘗試調用這個”unstable”對象的 query 方法,萬一調用失敗(拋出DeadObjectExceptopn,熟悉Binder的應該了解這個異常)說明ContentProvider所在的進程已經死亡,這時候會嘗試調用 acquireProvider 這個抽象方法來獲取一個可用的IContentProvider(代碼里面那個萌萌的注釋說明了一切^_^);由于這兩個 acquire* 都是抽象方法,我們可以直接看子類 ApplicationContentResolver 的實現:

@Override
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
    return mMainThread.acquireProvider(c,
            ContentProvider.getAuthorityWithoutUserId(auth),
            resolveUserIdFromAuthority(auth), false);
}
@Override
protected IContentProvider acquireProvider(Context context, String auth) {
    return mMainThread.acquireProvider(context,
            ContentProvider.getAuthorityWithoutUserId(auth),
            resolveUserIdFromAuthority(auth), true);
}

可以看到這兩個抽象方法最終都通過調用 ActivityThread 類的 acquireProvider 獲取到IContentProvider,接下來我們看看到底是如何獲取到ContentProvider的。

ContentProvider獲取過程

ActivityThread類的 acquireProvider 方法如下,我們需要知道的是,方法的最后一個參數 stable 代表著ContentProvider所在的進程是否存活,如果進程已死,可能需要在必要的時候喚起這個進程;

public final IContentProvider acquireProvider(
 Context c, String auth, int userId, boolean stable) {
    final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
    if (provider != null) {
        return provider;
    }

    IActivityManager.ContentProviderHolder holder = null;
    try {
        holder = ActivityManagerNative.getDefault().getContentProvider(
                getApplicationThread(), auth, userId, stable);
    } catch (RemoteException ex) {
    }
    if (holder == null) {
        Slog.e(TAG, "Failed to find provider info for " + auth);
        return null;
    }

    holder = installProvider(c, holder, holder.info,
            true /*noisy*/, holder.noReleaseNeeded, stable);
    return holder.provider;
}

這個方法首先通過 acquireExistingProvider 嘗試從本進程中獲取ContentProvider,如果獲取不到,那么再請求 AMS 獲取對應ContentProvider;想象一下,如果你查詢的是自己App內部的ContentProvider組件,干嘛要勞煩AMS呢?不論是從哪里獲取到的ContentProvider,獲取完畢之后會調用 installProvider 來安裝ContentProvider。

OK打住,我們思考一下,如果要實現ContentProvider的插件化,我們需要完成一些什么工作?開篇的時候我提到了數據共享,那么具體來說,實現插件的數據共享,需要完成什么?ContentProvider是一個數據共享組件,也就是說它不過是 一個攜帶數據的載體而已 。為了支持跨進程共享,這個載體是 Binder調用 ,為了共享大量數據,使用了匿名共享內存;這么說還是有點抽象,那么想一下,給出一個ContentProvider,你能對它做一些什么操作?如果能讓插件支持這些操作,不就支持了插件化么?這就是典型的duck type思想——如果一個東西看起來像ContentProvider,用起來也像ContentProvider,那么它就是ContentProvider。

ContentProvider主要支持 query, insert, update, delete 操作,由于這個組件一般工作在別的進程,因此這些調用都是Binder調用。從上面的代碼可以看到,這些調用最終都是委托給一個IContentProvider的Binder對象完成的,如果我們Hook掉這個對象,那么對于ContentProvider的所有操作都會被我們攔截掉,這時候我們可以做進一步的操作來完成對于插件ContentProvider組件的支持。要攔截這個過程,我們可以 假裝插件的ContentProvider是自己App的ContentProvider ,也就是說,讓 acquireExistingProvider 方法可以直接獲取到插件的ContentProvider,這樣我們就不需要欺騙AMS就能完成插件化了。當然,你也可以選擇Hook掉AMS,讓AMS的 getContentProvider 方法返回被我們處理過的對象,這也是可行的;但是,為什么要舍近求遠呢?

從上文的分析暫時得出結論:我們可以把插件的ContentProvider信息預先放在App進程內部,使得對于ContentProvider執行CURD操作的時候,可以獲取到插件的組件,這樣或許就可以實現插件化了。具體來說,我們要做的事情就是讓 ActivityThread 的 acquireExistingProvider 方法能夠返回插件的ContentProvider信息,我們看看這個方法的實現:

public final IContentProvider acquireExistingProvider(
 Context c, String auth, int userId, boolean stable) {
    synchronized (mProviderMap) {
        final ProviderKey key = new ProviderKey(auth, userId);
        final ProviderClientRecord pr = mProviderMap.get(key);
        if (pr == null) {
            return null;
        }

        // 略。。
    }
}

可以看出,App內部自己的ContentProvider信息保存在ActivityThread類的 mProviderMap 中,這個map的類型是ArrayMap ;我們當然可以通過反射修改這個成員變量,直接把插件的ContentProvider信息填進去,但是這個ProviderClientRecord對象如何構造?我們姑且看看系統自己是如果填充這個字段的。在ActivityThread類中搜索一遍,發現調用mProviderMap對象的 put 方法的之后 installProviderAuthoritiesLocked ,而這個方法最終被 installProvider 方法調用。在分析ContentProvider的獲取過程中我們已經知道,不論是通過本進程的 acquireExistingProvider 還是借助AMS的 getContentProvider 得到ContentProvider,最終都會對這個對象執行 installProvider 操作,也就是「安裝」在本進程內部。那么,我們接著看這個 installProvider 做了什么,它是如何「安裝」ContentProvider的。

進程內部ContentProvider安裝過程

首先,如果之前沒有“安裝”過,那么holder為null,下面的代碼會被執行,

final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.
    loadClass(info.name).newInstance();
provider = localProvider.getIContentProvider();
if (provider == null) {
    Slog.e(TAG, "Failed to instantiate class " +
          info.name + " from sourceDir " +
          info.applicationInfo.sourceDir);
    return null;
}
if (DEBUG_PROVIDER) Slog.v(
    TAG, "Instantiating local provider " + info.name);
// XXX Need to create the correct context for this provider.
localProvider.attachInfo(c, info);

比較直觀,直接load這個ContentProvider所在的類,然后用反射創建出這個ContentProvider對象;但是由于查詢是需要進行跨進程通信的,在本進程創建出這個對象意義不大,所以我們需要取出ContentProvider承載跨進程通信的Binder對象IContentProvider;創建出對象之后,接下來就是構建合適的信息,保存在ActivityThread內部,也就是 mProviderMap :

if (localProvider != null) {
    ComponentName cname = new ComponentName(info.packageName, info.name);
    ProviderClientRecord pr = mLocalProvidersByName.get(cname);
    if (pr != null) {
        if (DEBUG_PROVIDER) {
            Slog.v(TAG, "installProvider: lost the race, "
                    + "using existing local provider");
        }
        provider = pr.mProvider;
    } else {
        holder = new IActivityManager.ContentProviderHolder(info);
        holder.provider = provider;
        holder.noReleaseNeeded = true;
        pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
        mLocalProviders.put(jBinder, pr);
        mLocalProvidersByName.put(cname, pr);
    }
    retHolder = pr.mHolder;
} else {

以上就是安裝代碼,不難理解。

思路嘗試——本地安裝

那么,了解了「安裝」過程再結合上文的分析,我們似乎可以完成ContentProvider的插件化了——直接把插件的ContentProvider安裝在進程內部就行了。如果插件系統有多個進程,那么必須在每個進程都「安裝」一遍,如果你熟悉Android進程的啟動流程那么就會知道,這個安裝ContentProvider的過程適合放在Application類中,因為每個Android進程啟動的時候,App的Application類是會被啟動的。

看起來實現ContentProvider的思路有了,但是這里實際上有一個嚴重的缺陷!

我們依然沒有解決「共享」的問題。我們只是在插件系統啟動的進程里面的ActivityThread的 mProviderMap 給修改了,這使得只有通過插件系統啟動的進程,才能感知到插件中的ContentProvider(因為我們手動把插件中的信息install到這個進程中去了);如果第三方的App想要使用插件的ContentProvider,那系統只會告訴它查無此人。

那么,我們應該如何解決共享這個問題呢?看來還是逃不過AMS的魔掌,我們繼續跟蹤源碼,看看如果在本進程查詢不到ContentProvider,AMS是如何完成這個過程的。在ActivityThread的 acquireProvider 方法中我們提到,如果 acquireExistingProvider 方法返回null,會調用ActivityManagerNative的 getContentProvider 方法通過AMS查詢整個系統中是否存在需要的這個ContentProvider。如果第三方App查詢插件系統的ContentProvider必然走的是這個流程,我們仔細分析一下這個過程;

AMS中的ContentProvider

首先我們查閱ActivityManagerService的 getContentProvider 方法,這個方法間接調用了 getContentProviderImpl 方法; getContentProviderImpl 方法體相當的長,但是實際上只做了兩件事件事(我這就不貼代碼了,讀者可以對著源碼看一遍):

  1. 使用PackageManagerService的resolveContentProvider根據Uri中提供的auth信息查閱對應的ContentProivoder的信息ProviderInfo。
  2. 根據查詢到的ContentProvider信息,嘗試將這個ContentPRovider組件安裝到系統上。

查詢ContentProvider組件的過程

查詢ContentProvider組件的過程看起來很簡單,直接調用PackageManager的 resolveContentProvider 就能從URI中獲取到對應的 ProviderInfo 信息:

@Override
public ProviderInfo resolveContentProvider(String name, int flags, int userId) {
    if (!sUserManager.exists(userId)) return null;
    // reader
    synchronized (mPackages) {
        final PackageParser.Provider provider = mProvidersByAuthority.get(name);
        PackageSetting ps = provider != null
                ? mSettings.mPackages.get(provider.owner.packageName)
                : null;
        return ps != null
                && mSettings.isEnabledLPr(provider.info, flags, userId)
                && (!mSafeMode || (provider.info.applicationInfo.flags
                        &ApplicationInfo.FLAG_SYSTEM) != 0)
                ? PackageParser.generateProviderInfo(provider, flags,
                        ps.readUserState(userId), userId)
                : null;
    }
}

但是實際上我們關心的是,這個 mProvidersByAuthority 里面的信息是如何添加進PackageManagerService的,會在什么時候更新?在PackageManagerService這個類中搜索mProvidersByAuthority.put這個調用,會發現在 scanPackageDirtyLI 會更新 mProvidersByAuthority 這個map的信息,接著往前追蹤會發現: 這些信息是在Android系統啟動的時候收集的 。也就是說,Android系統在啟動的時候會掃描一些App的安裝目錄,典型的比如/data/app/*,獲取這個目錄里面的apk文件,讀取其AndroidManifest.xml中的信息,然后把這些信息保存在PackageManagerService中。合理猜測,在系統啟動之后,安裝新的App也會觸發對新App中AndroidManifest.xml的操作,感興趣的讀者可以自行翻閱源碼。

現在我們知道,查詢ContentProvider的信息來源在Android系統啟動的時候已經初始化好了,這個過程對于我們第三方app來說是鞭長莫及;想要通過像在進程內部Hack這個查找過程是不可能的。

安裝ContentProvider組件的過程

獲取到URI對應的ContentPRovider的信息之后,接下來就是把它安裝到系統上了,這樣以后有別的查詢操作就可以直接拿來使用;但是這個安裝過程AMS是沒有辦法以一己之力完成的。想象一下App DemoA 查詢App DemoB 的某個ContentProviderAppB,那么這個ContentProviderAppB必然存在于DemoB這個App中,AMS所在的進程(system_server)連這個ContentProviderAppB的類都沒有,因此,AMS必須委托DemoB完成它的ContentProviderAppB的安裝;這里就分兩種情況:其一,DemoB這個App已經在運行了,那么AMS直接通知DemoB安裝ContentProviderAppB(如果B已經安裝了那就更好了);其二,DemoB這個app沒在運行,那么必須把B進程喚醒,讓它干活;這個過程也就是ActivityManagerService的 getContentProviderImpl 方法所做的,如下代碼:

if (proc != null && proc.thread != null) {
    if (!proc.pubProviders.containsKey(cpi.name)) {
        proc.pubProviders.put(cpi.name, cpr);
        try {
            proc.thread.scheduleInstallProvider(cpi);
        } catch (RemoteException e) {
        }
    }
} else {
    proc = startProcessLocked(cpi.processName,
            cpr.appInfo, false, 0, "content provider",
            new ComponentName(cpi.applicationInfo.packageName,
                    cpi.name), false, false, false);
    if (proc == null) {
        return null;
    }
}

如果查詢的ContentProvider所在進程處于運行狀態,那么AMS會通過這個進程給AMS的ApplicationThread這個Binder對象完成scheduleInstallProvider調用,這個過程比較簡單,最終會調用到目標進程的 installProvider 方法,而這個方法我們在上文已經分析過了。我們看一下如果目標進程沒有啟動,會發生什么情況。

如果ContentProvider所在的進程已經死亡,那么會調用startProcessLocked來啟動新的進程, startProcessLocked 有一系列重載函數,我們一路跟蹤,發現最終啟動進程的操作交給了 Process 類的 start 方法完成,這個方法通過socket與Zygote進程進行通信,通知Zygote進程fork出一個子進程,然后通過反射調用了之前傳遞過來的一個入口類的main函數,一般來說這個入口類就是ActivityThread,因此子進程fork出來之后會執行ActivityThread類的main函數。

在我們繼續觀察子進程ActivityThread的main函數執行之前,我們看看AMS進程這時候會干什么——startProcessLocked之后AMS進程和fork出來的DemoB進程分道揚鑣;AMS會繼續往下面執行。我們暫時回到AMS的 getContentProviderImpl 方法:

// Wait for the provider to be published...
synchronized (cpr) {
    while (cpr.provider == null) {
        if (cpr.launchingApp == null) {
            return null;
        }
        try {
            if (conn != null) {
                conn.waiting = true;
            }
            cpr.wait();
        } catch (InterruptedException ex) {
        } finally {
            if (conn != null) {
                conn.waiting = false;
            }
        }
    }
}

你沒看錯,一個死循環就是糊在上面:AMS進程會通過一個死循環等到進程B完成ContentProvider的安裝,等待完成之后會把ContentProvider的信息返回給進程A。那么,我們現在的疑惑是, 進程B在啟動之后,在哪個時間點會完成ContentProvider的安裝呢?

我們接著看ActivityThread的main函數,順便尋找我們上面那個問題的答案;這個分析實際上就是Android App的啟動過程,更詳細的過程可以參閱老羅的文章 Android應用程序啟動過程源代碼分析 ,這里只給出簡要調用流程:

最終,DemoB進程啟動之后會執行ActivityThread類的handleBindApplication方法,這個方法相當之長,基本完成了App進程啟動之后所有必要的操作;這里我們只關心ContentPRovider相關的初始化操作,代碼如下:

// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;

// don't bring up providers in restricted mode; they may depend on the
// app's custom Application class
if (!data.restrictedBackupMode) {
    List<ProviderInfo> providers = data.providers;
    if (providers != null) {
        installContentProviders(app, providers);
        // For process that contains content providers, we want to
        // ensure that the JIT is enabled "at some point".
        mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
    }
}

// Do this after providers, since instrumentation tests generally start their
// test thread at this point, and we don't want that racing.
try {
    mInstrumentation.onCreate(data.instrumentationArgs);
}
catch (Exception e) {
}

try {
    mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
}

仔細觀察以上代碼,你會發現: ContentProvider的安裝比Application的onCreate回調還要早!! 因此,分析到這里我們已經明白了前面提出的那個問題, 進程啟動之后會在Applition類的onCreate 回調之前,在Application對象創建之后完成ContentProvider的安裝 。

然后不要忘了,我們的AMS進程還在那傻傻等待DemoB進程完成ContentProviderAppB的安裝呢!在DemoB的Application的onCreate回調之前,DemoB的ContentProviderAppB已經安裝好了,因此AMS停止等待,把DemoB安裝的結果返回給請求這個ContentProvider的DemoA。我們必須對這個時序保持敏感,有時候就是失之毫厘,差之千里!!

到這里,有關ContentProvider的調用過程以及簡要的工作原理我們已經分析完畢,關于它如何共享數據,如何使用匿名共享內存這部分不是插件化的重點,感興趣的可以參考 Android應用程序組件Content Provider在應用程序之間共享數據的原理分析

不同之處

在實現ContentProvider的插件化之前,通過分析這個組件的工作原理,我們可以得出它的一些與眾不同的特性:

  1. ContentPRovider本身是用來共享數據的,因此它提供一般的CURD服務;它類似HTTP這種無狀態的服務,沒有Activity,Service所謂的生命周期的概念,服務要么可用,要么不可用;對應著ContentPRovider要么啟動,要么隨著進程死亡;而通常情況下,死亡之后還會被系統啟動。所以,ContentProvider,只要有人需要這個服務,系統可以保證是永生的;這是與其他組件的最大不同;完全不用考慮生命周期的概念。
  2. ContentProvider被設計為共享數據,這種數據量一般來說是相當大的;熟悉Binder的人應該知道,Binder進行數據傳輸有1M限制,因此如果要使用Binder傳輸大數據,必須使用類似socket的方式一段一段的讀,也就是說需要自己在上層架設一層協議;ContentProvider并沒有采取這種方式,而是采用了Android系統的匿名共享內存機制,利用Binder來傳輸這個文件描述符,進而實現文件的共享;這是第二個不同,因為其他的三個組建通信都是基于Binder的,只有ContentProvider使用了Ashmem。
  3. 一個App啟動過程中,ContentProvider組件的啟動是非常早的,甚至比Application的onCreate還要早;我們可以利用這個特性結合它不死的特點,完成一些有意義的事情。
  4. ContentProvider存在優先查詢本進程的特點,使得它的插件化甚至不需要Hook AMS就能完成。

思路分析

在分析ContentProvider的工作原理的過程中我們提出了一種插件化方案:在進程啟動之初,手動把ContentProvider安裝到本進程,使得后續對于插件ContentProvider的請求能夠順利完成。我們也指出它的一個嚴重缺陷,那就是它只能在插件系統內部掩耳盜鈴,在插件系統之外,第三方App依然無法感知到插件中的ContentProvider的存在。

如果插件的ContentProvider組件僅僅是為了共享給其他插件或者宿主程序使用,那么這種方案可以解決問題;不需要Hook AMS,非常簡單。

但是,如果希望把插件ContenProvider共享給整個系統呢?在分析AMS中獲取ContentProvider的過程中我們了解到,ContentProvider信息的注冊是在Android系統啟動或者新安裝App的時候完成的,而AMS把ContentProvider返回給第三方App也是在system_server進程完成;我們無法對其暗箱操作。

在完成Activity,Service組件的插件化之后,這種限制對我們來說已經是小case了:我們在宿主程序里面注冊一個貨真價實、被系統認可的StubContentProvider組件,把這個組件共享給第三方App;然后通過 代理分發技術 把第三方App對于插件ContentProvider的請求通過這個StubContentProvider分發給對應的插件。

但是這還存在一個問題,?由于第三方App查閱的其實是StubContentProvider,因此他們查閱的URI也必然是StubContentProvider的authority,要查詢到插件的ContentProvider,必須把要查詢的真正的插件ContentProvider信息傳遞進來。這個問題的解決方案也很容易,我們可以制定一個「插件查詢協議」來實現。

舉個例子,假設插件系統的宿主程序在AndroidManifest.xml中注冊了一個StubContentProvider,它的Authority為 com.test.host_authority ;由于這個組件被注冊在AndroidManifest.xml中,是系統認可的ContentProvider組件,整個系統都是可以使用這個共享組件的,使用它的URI一般為 content://com.test.host_authority ;那么,如果插件系統中存在一個插件,這個插件提供了一個PluginContentProvider,它的Authority為 com.test.plugin_authorith ,因為這個插件的PluginContentProvider沒有在宿主程序的AndroidMainifest.xml中注冊(預先注冊就失去插件的意義了),整個系統是無法感知到它的存在的;前面提到代理分發技術,也就是,我們讓第三方App請求宿主程序的StubContentProvider,這個StubContentProvider把請求轉發給合適的插件的ContentProvider就能完成了(插件內部通過預先installProvider可以查詢所有的ContentProvider組件);這個協議可以有很多,比如說:如果第三方App需要請求插件的StubContentProvider,可以以 content://com.test.host_authority/com.test.plugin_authorith 去查詢系統;也就是說,我們假裝請求StubContentProvider,把真正的需要請求的PluginContentPRovider的Authority放在路徑參數里面,StubContentProvider收到這個請求之后,拿到這個真正的Authority去請求插件的PluginContentPRovider,拿到結果之后再返回給第三方App。

這樣,我們通過「代理分發技術」以及「插件查詢協議」可以完美解決「共享」的問題,開篇提到了我們之前對于Activity,Service組件插件化方案中對于「共享」功能的缺失,按照這個思路,基本可以解決這一系列問題。比如,對于第三方App無法綁定插件服務的問題,我們可以注冊一個StubService,把真正需要bind的插件服務信息放在intent的某個字段中,然后在StubService的onBind中解析出這個插件服務信息,然后去拿到插件Service組件的Binder對象返回給第三方。

實現

上文詳細分析了如何實現ContentPRovider的插件化,接下來我們就實現這個過程。

預先installProvider

要實現預先installProvider,我們首先需要知道,所謂的「預先」到底是在什么時候?

前文我們提到過App進程安裝ContentProvider的時機非常之早,在Application類的onCreate回調執行之前已經完成了;這意味著什么?

現在我們對于ContentProvider插件化的實現方式是通過「代理分發技術」,也就是說在請求插件ContentProvider的時候會先請求宿主程序的StubContentProvider;如果一個第三方App查詢插件的ContentProvider,而宿主程序沒有啟動的話,AMS會啟動宿主程序并等待宿主程序的StubContentProvider完成安裝, 一旦安裝完成就會把得到的IContentProvider返回給這個第三方App ;第三方App拿到IContentPRovider這個Binder對象之后就可能發起CURD操作,如果這個時候插件ContentProvider還沒有啟動,那么肯定就會出異常;要記住,“這個時候”可能宿主程序的onCreate還沒有執行完畢呢!!

所以,我們基本可以得出結論,預先安裝這個所謂的「預先」必須早于Application的onCreate方法,在Android SDK給我們的回調里面,attachBaseContent這個方法是可以滿足要求的,它在Application這個對象被創建之后就會立即調用。

解決了實際問題,那么我們接下來就可以安裝ContentProvider了。

安裝ContentProvider也就是要調用ActivityThread類的 installProvider 方法,這個方法需要的參數有點多,而且它的第二個參數IActivityManager.ContentProviderHolder是一個隱藏類,我們不知道如何構造,就算通過反射構造由于SDK沒有暴露穩定性不易保證,我們看看有什么方法調用了這個installProvider。

installContentProviders這個方法直接調用installProvder看起來可以使用,但是它是一個private的方法,還有public的方法嗎?繼續往上尋找調用鏈,發現了installSystemProviders這個方法:

public final void installSystemProviders(List<ProviderInfo> providers) {
    if (providers != null) {
        installContentProviders(mInitialApplication, providers);
    }
}

但是,我們說過ContentProvider的安裝必須相當早,必須在Application類的attachBaseContent方法內,而這個 mInitialApplication 字段是在 onCreate 方法調用之后初始化的,所以,如果直接使用這個 installSystemProviders 勢必拋出空指針異常;因此,我們只有退而求其次,選擇 通過installContentProviders這個方法完成ContentPRovider的安裝

要調用這個方法必須拿到ContentProvider對應的ProviderInfo,這個我們在之前也介紹過,可以通過PackageParser類完成,當然這個類有一些兼容性問題,我們需要手動處理:

/**
 * 解析Apk文件中的 <provider>, 并存儲起來
 * 主要是調用PackageParser類的generateProviderInfo方法
 *
 * @param apkFile 插件對應的apk文件
 * @throws Exception 解析出錯或者反射調用出錯, 均會拋出異常
 */
public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {
    Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
    Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

    Object packageParser = packageParserClass.newInstance();

    // 首先調用parsePackage獲取到apk對象對應的Package對象
    Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, PackageManager.GET_PROVIDERS);

    // 讀取Package對象里面的services字段
    // 接下來要做的就是根據這個List<Provider> 獲取到Provider對應的ProviderInfo
    Field providersField = packageObj.getClass().getDeclaredField("providers");
    List providers = (List) providersField.get(packageObj);

    // 調用generateProviderInfo 方法, 把PackageParser.Provider轉換成ProviderInfo
    Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
    Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
    Class<?> userHandler = Class.forName("android.os.UserHandle");
    Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
    int userId = (Integer) getCallingUserIdMethod.invoke(null);
    Object defaultUserState = packageUserStateClass.newInstance();

    // 需要調用 android.content.pm.PackageParser#generateProviderInfo
    Method generateProviderInfo = packageParserClass.getDeclaredMethod("generateProviderInfo",
            packageParser$ProviderClass, int.class, packageUserStateClass, int.class);

    List<ProviderInfo> ret = new ArrayList<>();
    // 解析出intent對應的Provider組件
    for (Object service : providers) {
        ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(packageParser, service, 0, defaultUserState, userId);
        ret.add(info);
    }

    return ret;
}

解析出ProviderInfo之后,就可以直接調用installContentProvider了:

/**
 * 在進程內部安裝provider, 也就是調用 ActivityThread.installContentProviders方法
 *
 * @param context you know
 * @param apkFile
 * @throws Exception
 */
public static void installProviders(Context context, File apkFile) throws Exception {
    List<ProviderInfo> providerInfos = parseProviders(apkFile);

    for (ProviderInfo providerInfo : providerInfos) {
        providerInfo.applicationInfo.packageName = context.getPackageName();
    }

    Log.d("test", providerInfos.toString());
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    Method installProvidersMethod = activityThreadClass.getDeclaredMethod("installContentProviders", Context.class, List.class);
    installProvidersMethod.setAccessible(true);
    installProvidersMethod.invoke(currentActivityThread, context, providerInfos);
}

整個安裝過程 必須在Application類的attachBaseContent里面完成

/**
 * 一定需要Application,并且在attachBaseContext里面Hook
 * 因為provider的初始化非常早,比Application的onCreate還要早
 * 在別的地方hook都晚了。
 *
 * @author weishu
 * @date 16/3/29
 */
public class UPFApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        try {
            File apkFile = getFileStreamPath("testcontentprovider-debug.apk");
            if (!apkFile.exists()) {
                Utils.extractAssets(base, "testcontentprovider-debug.apk");
            }

            File odexFile = getFileStreamPath("test.odex");

            // Hook ClassLoader, 讓插件中的類能夠被成功加載
            BaseDexClassLoaderHookHelper.patchClassLoader(getClassLoader(), apkFile, odexFile);
            ProviderHelper.installProviders(base, getFileStreamPath("testcontentprovider-debug.apk"));
        } catch (Exception e) {
            throw new RuntimeException("hook failed", e);
        }
    }

}

代理分發以及協議解析

把插件中的ContentProvider安裝到插件系統中之后,在插件內部就可以自由使用這些ContentProvider了;要把這些插件共享給整個系統,我們還需要一個貨真價實的ContentProvider組件來執行分發:

<provider
 android:name="com.example.weishu.contentprovider_management.StubContentProvider"
 android:authorities="com.example.weishu.contentprovider_management.StubContentProvider"
 android:process=":p"
 android:exported="true" />

第三方App如果要查詢到插件的ContentPRovider,必須遵循一個「插件查詢協議」,這樣StubContentPRovider才能把對于插件的請求分發到正確的插件組件:

/**
 * 為了使得插件的ContentProvder提供給外部使用,我們需要一個StubProvider做中轉;
 * 如果外部程序需要使用插件系統中插件的ContentProvider,不能直接查詢原來的那個uri
 * 我們對uri做一些手腳,使得插件系統能識別這個uri;
 *
 * 這里的處理方式如下:
 *
 * 原始查詢插件的URI應該為:
 * content://plugin_auth/path/query
 *
 * 如果需要查詢插件,需要修改為:
 *
 * content://stub_auth/plugin_auth/path/query
 *
 * 也就是,我們把插件ContentProvider的信息放在URI的path中保存起來;
 * 然后在StubProvider中做分發。
 *
 * 當然,也可以使用QueryParamerter,比如:
 * content://plugin_auth/path/query/ -> content://stub_auth/path/query?plugin=plugin_auth
 * @param raw 外部查詢我們使用的URI
 * @return 插件真正的URI
 */
private Uri getRealUri(Uri raw) {
    String rawAuth = raw.getAuthority();
    if (!AUTHORITY.equals(rawAuth)) {
        Log.w(TAG, "rawAuth:" + rawAuth);
    }

    String uriString = raw.toString();
    uriString = uriString.replaceAll(rawAuth + '/', "");
    Uri newUri = Uri.parse(uriString);
    Log.i(TAG, "realUri:" + newUri);
    return newUri;
}

通過以上過程我們就實現了ContentProvider的插件化。需要說明的是,DroidPlugind的插件化與上述介紹的方案有一些不同之處:

  1. 首先DroidPlugin并沒有選擇預先安裝的方案,而是選擇Hook ActivityManagerNative,攔截它的getContentProvider以及publishContentProvider方法實現對于插件組件的控制;從這里可以看出它對ContentProvider與Service的插件化幾乎是相同的,Hook才是DroidPlugin Style ^_^.
  2. 然后,關于攜帶插件信息,或者說「插件查詢協議」方面;DroidPlugin把插件信息放在查詢參數里面,本文呢則是路徑參數;這一點完全看個人喜好。

小結

本文我們通過「代理分發技術」以及「插件查詢協議」完成了ContentProvider組件的插件化,并且給出了對「插件共享組件」的問題的一般解決方案。值得一提的是,系統的ContentProvider其實是lazy load的,也就是說只有在需要使用的時候才會啟動對應的ContentProvider,而我們對于插件的實現則是 預先加載 ,這里還有改進的空間,讀者可以思考一下解決方案。

由于ContentProvider的使用頻度非常低,而很多它使用的場景(比如系統)并不太需要「插件化」,因此在實際的插件方案中,提供ContentProvider插件化的方案非常之少;就算需要實現ContentProvider的插件化,也只是解決插件內部之間共享組件的問題,并沒有把插件組件暴露給整個系統。我個人覺得,如果只是希望插件化,那么是否支持ContentProvider無傷大雅,但是,如果希望實現虛擬化或者說容器技術,所有組件是必須支持插件化的。

至此,對于Android系統的四大組件的插件化已經全部介紹完畢;由于是最后一個要介紹的組件,我并沒有像之前一樣先給出組件的運行原理,然后一通分析最后給出插件方案,而是一邊分析代碼一邊給出自己的思路,把思考——推翻——改進的整個過程完全展現了出來,Android的插件化已經到達了百花齊放的階段,插件化之路也不只有一條,但是萬變不離其宗,希望我的分析和思考對各位讀者理解甚至創造插件化方案帶來幫助。接下來我會介紹「插件通信機制」,它與本文的ContentProvider以及我反復強調過的一些特性密切相關,敬請期待!

喜歡就點個贊吧,兜里有一塊錢的童鞋可以點擊下面的打賞然后掃一下二維碼哦~持續更新,請關注github項目 understand-plugin-framework 和我的博客! 另外很抱歉一個多月沒有更新博客了,每天看到各位的來訪記錄深感慚愧,實在是業務繁忙,身不由已!不出意外接下來會以正常速度更新內容,謝謝支持 ^_^

 

閱讀原文

 

 本文由用戶 wenzp521 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!