原文地址: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:
2 |
android:name = ".MyApplication" |
3 |
android:icon = "@drawable/icon" |
4 |
android:label = "@string/app_name" > |
添加的Application類:
1 |
public class MyProxyApplication extends ProxyApplication
{ |
3 |
protected void initProxyApplication()
{ |
新的AndroidManifest.xml:
2 |
android:name = ".MyProxyApplication" |
3 |
android:icon = "@drawable/icon" |
4 |
android:label = "@string/app_name" > |
6 |
android:name = "DELEGATE_APPLICATION_CLASS_NAME" |
7 |
android:value = ".MyApplication" > |
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源代碼片段做參考。
02 |
public class Application extends ContextWrapper
{ |
04 |
public application()
{ |
10 |
public class ContextWrapper extends Context
{ |
13 |
public ContextWrapper(Context
base) { |
16 |
protected void attachBaseContext(Context
base) { |
18 |
throw new IllegalStateException( "Base
context already set" ); |
24 |
public AssetManager
getAssets() { |
25 |
return mBase.getAssets(); |
28 |
public Resources
getResources() |
30 |
return mBase.getResources(); |
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(); |
4 |
protected void attachBaseContext
(Context context) { |
5 |
super .attachBaseContext(context); |
6 |
initProxyApplication(); |
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; |
(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); |
再次對付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對象?我們可以從源代碼中找到答案:
02 |
if (context.getPackageName().equals(ai.packageName))
{ |
04 |
} else if (mInitialApplication
!= null && |
05 |
mInitialApplication.getPackageName().equals(ai.packageName))
{ |
06 |
c
= mInitialApplication; |
09 |
c
= context.createPackageContext(ai.packageName, |
10 |
Context.CONTEXT_INCLUDE_CODE); |
11 |
} catch (PackageManager.NameNotFoundException
e) { |
容易看出,因爲ProxyApplication對象的getPackageName()函數與ContentProvider對應的包名相同,就會複用ProxyApplication對象作爲Context,而不會再創建一個新的packageContext。於是解決方案也很簡單了:
2 |
public String
getPackageName() { |
由於ProxyApplication不是最終的Application,這並不會產生什麼副作用。
使用注意事項
不要保留ProxyApplication子類對象的引用,也不要在任何系統回調(包括onCreate)中做事情。onCreate()被基類用於加載DelegateApplication,而其它回調都不會再收到。
在ProxyApplication:onCreate()執行完成之後,虛擬機中所有的線程棧和所有的JAVA對象,都不會再有ProxyApplication對象的引用。ProxyApplication對象將在下一次GC運行時被回收,這也意味着從ProxyApplication到DelegateApplication的替換進行得非常徹底。自然地,ProxyApplication也收不到其它回調了。DelegateApplication會正常的接收所有的回調。
另外,在ProxyApplication子類中,如果需要獲取當前APK的包名,需要使用getBaseContext().getPackageName(),而不能簡單調用getPackageName()。原因在上面“再次對付ContentProvider”中有說明。