1. 程式人生 > >Android外掛化原理解析——ContentProvider的外掛化

Android外掛化原理解析——ContentProvider的外掛化

目前為止我們已經完成了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的典型程式碼如下:

1
2
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實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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的實現:

1
2
3
4
5
6
7
8
9
10
11
12
@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所在的程序是否存活,如果程序已死,可能需要在必要的時候喚起這個程序;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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操作的時候,可以獲取到外掛的元件,這樣或許就可以實現外掛化了。具體來說,我們要做的事情就是讓ActivityThreadacquireExistingProvider方法能夠返回外掛的ContentProvider資訊,我們看看這個方法的實現:

1
2
3
4
5
6
7
8
9
10
11
12
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,下面的程式碼會被執行,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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資訊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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的查詢過程是不可能的。

安裝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方法所做的,如下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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應用程式啟動過程原始碼分析,這裡只給出簡要呼叫流程:

App啟動簡要流程

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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這個方法:

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

但是,我們說過ContentProvider的安裝必須相當早,必須在Application類的attachBaseContent方法內,而這個mInitialApplication欄位是在onCreate方法呼叫之後初始化的,所以,如果直接使用這個installSystemProviders勢必丟擲空指標異常;因此,我們只有退而求其次,選擇通過installContentProviders這個方法完成ContentProvider的安裝

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * 解析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了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 在程序內部安裝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裡面完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * 一定需要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元件來執行分發:

1
2
3
4
5