如何改變Android應用的運行環境

原文地址:http://blogs.360.cn/blog/proxydelegate-application/

有的時候,爲了實現一些特殊需求,如界面換膚、插件化等,我們希望改變應用的運行環境(surrounding)。例如,我們希望某個應用在運行時,所有Class(包括自定義Application,下面假設它叫MyApplication)都被一個自定義的ClassLoader加載。

要實現這個需求,需要在MyApplication被加載之前,先替換掉API層的默認ClassLoader,否則MyApplication就會被默認ClassLoader加載。但這會產生一個悖論,MyApplication被加載之前,沒有任何應用代碼可以運行,替換ClassLoader無法辦到。Proxy/Delegate Application框架就是用來解決這類問題的。

Proxy/Delegate Application簡介

在Proxy/Delegate Application框架裏,應用一共有兩個Application對象,一個稱爲ProxyApplication,另一個稱爲DelegateApplication:

(1) ProxyApplication:框架會提供一個ProxyApplication抽象基類(abstract class),使用者需要繼承這個類,並重載其initProxyApplication()方法,在其中改變surrounding,如替換ClassLoader等。

(2) DelegateApplication:即應用原有的Application,應用從getApplicationContext()等方法中取到的都是DelegateApplication。注意DelegateApplication只是一個稱謂,並沒有一個叫DelegateApplication的基類存在。

使用Proxy/Delegate Application框架,使用者可以在對原有Application類不做任何修改的情況下,改變整個應用的運行環境。所需要做的只是添加一個新的Application類,並相應的修改AndroidManifest.xml。

老的AndroidManifest.xml:

1 <application
2 android:name=".MyApplication"
3 android:icon="@drawable/icon"
4 android:label="@string/app_name" >

添加的Application類:

1 public class MyProxyApplication extends ProxyApplication {
2     @Override
3     protected void initProxyApplication() {
4         // 在這裏替換運行環境,如將ClassLoader替換爲自定義的
5         // ......
6     }
7 }

新的AndroidManifest.xml:

1 <application
2 android:name=".MyProxyApplication"
3 android:icon="@drawable/icon"
4 android:label="@string/app_name" >
5     <meta-data
6     android:name="DELEGATE_APPLICATION_CLASS_NAME"
7     android:value=".MyApplication" >
8     </meta-data>

MyProxyApplication(ProxyApplication)對象對應用是不可見的,應用看到的Application是MyApplication(DelegateApplication),也就是以前的Application對象。這樣對於應用而已,似乎一切都沒有改變;但它的運行環境已經改變,例如所有的類已經被新的ClassLoader加載了。整個實現是非侵入式的,已有代碼無須任何修改,只有AndroidManifest.xml略有改動。


下面開始探討ProxyApplication本身如何實現。核心問題是兩個,一是什麼時機調用子類的initProxyApplication()方法,讓子類改變surrounding;二是如何加載DelegateApplication並讓應用認爲它就是真實的Application。另外Android四大組件之一的ContentProvider會給我們帶來不少麻煩,需要妥善處理。

ProxyApplication實現:時機

理論上ProxyApplication對任何能夠訪問到的變量,包括Java層和Native層,都是可以替換(或者HOOK,類似的含義)的;比較有意義的除了ClassLoader外,還有Resources和各路Binder對象。通過這些手段可以實現非常多有意思的功能。具體如何替換ClassLoader、Resources等這裏不深入討論,如有興趣,在網上可以找到很多相關資料。本文的重點是介紹框架本身,替換ClassLoader僅作爲一個例子。

現在的問題是改變surrounding的時機必須足夠早,特別是對於ClassLoader來說尤爲重要。是否可以在Application:onCreate()裏做?我們通常認爲,Application是一個Android應用最早被加載的組件;但當應用註冊有ContentProvider的時候,這並不正確的。ContentProvider:onCreate()調用優先於Application:onCreate()。

幸好,我們還有另一個方法:attachBaseContext()。Android的幾個主要頂級組件(Application、Activity、Service)都是ContextWrapper的子類。ContextWrapper一方面繼承(inherit)了Context,一方面又包含(composite)了一個Context對象(稱爲mBase),對Context的實現爲轉發給mBase對象處理。這一個聽起來很繞的設計,是爲了對這些頂級組件中的Context功能做延遲初始化(delay init)的處理。這裏不展開討論了,僅貼一些Android源代碼片段做參考。

01 // android.app.Application
02 public class Application extends ContextWrapper {
03     // ...
04     public application() {
05         super(null);
06     }
07     // ...
08 }
09 // android.content.ContextWrapper
10 public class ContextWrapper extends Context {
11     Context mBase;
12     // ...
13     public ContextWrapper(Context base) {
14         mBase = base;
15     }
16     protected void attachBaseContext(Context base) {
17         if (mBase != null) {
18             throw new IllegalStateException("Base context already set");
19         }
20         mBase = base;
21     }
22     // ...
23     @Override
24     public AssetManager getAssets() {
25         return mBase.getAssets();
26     }
27     @Override
28     public Resources getResources()
29     {
30         return mBase.getResources();
31     }
32     // ...
33 }

ContextWrapper完成這個delay init語義的方法就是attachBaseContext()。可以這樣說,Application對象在剛剛構造完成時是“殘廢”的,訪問所有Context的方法都會拋出NullPointerException。只有attachBaseContext()執行完後,它的功能才完整。

在ContentProvider:onCreate()中,我們知道Application:onCreate()還沒有運行,但已經可以使用getContext().getApplicationContext()函數獲取Application對象,並訪問其Context方法。顯然,Android的API設計者不能允許此時獲取的Application是“殘廢”的。結論是Application:attachBaseContext()必須要發生在ContentProvider:onCreate()之前,否則API將出現BUG;無論Android的系統版本如何變化,這一點也不能改變。

於是,Application與ContentProvider的初始化次序是這樣的:Application:attachBaseContext()最早執行,然後是ContentProvider:onCreate(),然後是Application:onCreate()。我們的解決方案也就很簡單了:

1 public abstract class ProxyApplication extends Application {
2     protected abstract void initProxyApplication();
3     @Override
4     protected void attachBaseContext (Context context) {
5         super.attachBaseContext(context); 
6         initProxyApplication();
7     }
8     // ……
9 }

ProxyApplication實現:加載DelegateApplication

當子類的initProxyApplication()返回後,ProxyApplication就要加載DelegateApplication,完成自己的歷史使命。這一部分在onCreate()中完成,基本是些體力活,但也有些需要注意的地方,下面分步驟簡述一下。

(1) 獲取DelegateApplication的Class Name

即從AndroidManifest.xml中獲取DELEGATE_APPLICAION的metadata值,若不存在,則使用android.app.Application作爲默認。這一步比較簡單。

01 String className = "android.app.Application";
02 String key = "DELEGATE_APPLICATION_CLASS_NAME";
03 ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
04     super.getPackageName(), PackageManager.GET_META_DATA);
05 Bundle bundle = appInfo.metaData;
06 if (bundle != null && bundle.containsKey(key)) {
07     className = bundle.getString(key);
08     if (className.startsWith("."))
09         className = super.getPackageName() + className;
10 }

(2) 加載DelegateApplication並生成對象

這裏要注意的是使用哪個ClassLoader?答案是應該用getClassLoader()(即Context:getClassLoader()),而不是getClass().getClassLoader()。要仔細揣摩這兩者之間的差別。

1 Class delegateClass = Class.forName(className, true, getClassLoader());
2 Application delegate = (Application) delegateClass.newInstance();

 (3) 替換API層的所有Application引用

即把API層所有保存的ProxyApplication對象,都替換爲新生成的DelegateApplication對象。以ProxyApplication的baseContext作爲起點順藤摸瓜,可以找到所有的位置,使用反射一一換掉。注意最後一個mAllApplications是List,要換掉其內部的內容。

1 baseContext.mOuterContext
2 baseContext.mPackageInfo.mApplication
3 baseContext.mPackageInfo.mActivityThread.mInitialApplication
4 baseContext.mPackageInfo.mActivityThread.mAllApplications

 (4) 設置baseContext並調用onCreate

將控制權交給DelegateApplication。當然,後者會認爲自己就是“正牌”的Application,後續的其它組件也都會這麼認爲。這正是我們要的效果。

1 Method attach = Application.class.getDeclaredMethod("attach", Context.class);
2 attach.setAccessible(true);
3 attach.invoke(delegate, base);
4 delegate.onCreate();

再次對付ContentProvider

前面提到過,Android的頂級組件Application、Activity、Service都是ContextWrapper,這個列表中並沒有ContentProvider。ContentProvider不是ContextWrapper,甚至不是Context,而是內部有一個mContext變量,通過getContext()函數獲取這個Context。

那麼,ContentProvider:getContext()獲取到的是哪一個Context?實驗證明,ContentProvider:getContext()獲取的Context是Application;準確的說,在Proxy/Delegate Application框架裏,是ProxyApplication。這就不符合框架的語義了。那麼,我們需要像其它處理其它ProxyApplication引用一樣,把它換成DelegateApplication嗎?這是可行的:遍歷API層的ContentProvider列表,將每一個ContentProvider中的mContext都替換爲DelegateApplication。

但這種處理方式,會進一步增加對Android API層源代碼依賴,是否必要?畢竟Android的API文檔中,並沒有規定ContentProvider:getContext()返回的必須是Application;如果要取得Application,正確的方式是getContext().getApplicationContext()。那麼爲什麼getContext()就直接返回了Application對象?我們可以從源代碼中找到答案:

01 // in ActivityThread:installProvider()
02 if (context.getPackageName().equals(ai.packageName)) {
03     c = context;
04 else if (mInitialApplication != null &&
05         mInitialApplication.getPackageName().equals(ai.packageName)) {
06     c = mInitialApplication;
07 else {
08     try {
09         c = context.createPackageContext(ai.packageName,
10                 Context.CONTEXT_INCLUDE_CODE);
11     catch (PackageManager.NameNotFoundException e) {
12         // Ignore
13     }
14 }

容易看出,因爲ProxyApplication對象的getPackageName()函數與ContentProvider對應的包名相同,就會複用ProxyApplication對象作爲Context,而不會再創建一個新的packageContext。於是解決方案也很簡單了:

1 @Override
2 public String getPackageName() {
3     return "";
4 }

由於ProxyApplication不是最終的Application,這並不會產生什麼副作用。


使用注意事項

不要保留ProxyApplication子類對象的引用,也不要在任何系統回調(包括onCreate)中做事情。onCreate()被基類用於加載DelegateApplication,而其它回調都不會再收到。

在ProxyApplication:onCreate()執行完成之後,虛擬機中所有的線程棧和所有的JAVA對象,都不會再有ProxyApplication對象的引用。ProxyApplication對象將在下一次GC運行時被回收,這也意味着從ProxyApplication到DelegateApplication的替換進行得非常徹底。自然地,ProxyApplication也收不到其它回調了。DelegateApplication會正常的接收所有的回調。

另外,在ProxyApplication子類中,如果需要獲取當前APK的包名,需要使用getBaseContext().getPackageName(),而不能簡單調用getPackageName()。原因在上面“再次對付ContentProvider”中有說明。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章