Proxy/Delegate Application框架
有的時候像捶,為了實現(xiàn)一些特殊需求抚官,如界面換膚扬跋、插件化等,我們希望改變應(yīng)用的運行環(huán)境(surrounding)凌节。例如钦听,我們希望某個應(yīng)用在運行時洒试,所有Class(包括自定義Application,下面假設(shè)它叫MyApplication)都被一個自定義的ClassLoader加載朴上。
要實現(xiàn)這個需求垒棋,需要在MyApplication被加載之前,先替換掉API層的默認ClassLoader痪宰,否則MyApplication就會被默認ClassLoader加載叼架。但這會產(chǎn)生一個悖論,MyApplication被加載之前衣撬,沒有任何應(yīng)用代碼可以運行碉碉,替換ClassLoader無法辦到。Proxy/Delegate Application框架就是用來解決這類問題的淮韭。
Proxy/Delegate Application簡介
定義
在Proxy/Delegate Application框架里垢粮,應(yīng)用一共有兩個Application對象,一個稱為ProxyApplication靠粪,另一個稱為DelegateApplication:
- ProxyApplication:框架會提供一個ProxyApplication抽象基類(abstract class)蜡吧,使用者需要繼承這個類,并重載其initProxyApplication()方法占键,在其中改變surrounding昔善,如替換ClassLoader等。
- DelegateApplication:即應(yīng)用原有的Application畔乙,應(yīng)用從getApplicationContext()等方法中取到的都是DelegateApplication君仆。注意DelegateApplication只是一個稱謂,并沒有一個叫DelegateApplication的基類存在牲距。
使用
使用Proxy/Delegate Application框架返咱,使用者可以在對原有Application類不做任何修改的情況下,改變整個應(yīng)用的運行環(huán)境牍鞠。所需要做的只是添加一個新的Application類咖摹,并相應(yīng)的修改AndroidManifest.xml。
老的AndroidManifest.xml:
<application
android:name=".MyApplication"
android:icon="@drawable/icon"
android:label="@string/app_name" >
添加的Application類:
public class MyProxyApplication extends ProxyApplication {
@Override
protected void initProxyApplication() {
// 在這里替換運行環(huán)境难述,如將ClassLoader替換為自定義的
// ......
}
}
新的AndroidManifest.xml:
<application
android:name=".MyProxyApplication"
android:icon="@drawable/icon"
android:label="@string/app_name" >
<meta-data
android:name="DELEGATE_APPLICATION_CLASS_NAME"
android:value=".MyApplication" >
</meta-data>
MyProxyApplication(ProxyApplication)對象對應(yīng)用是不可見的萤晴,應(yīng)用看到的Application是MyApplication(DelegateApplication),也就是以前的Application對象胁后。這樣對于應(yīng)用而已店读,似乎一切都沒有改變;但它的運行環(huán)境已經(jīng)改變攀芯,例如所有的類已經(jīng)被新的ClassLoader加載了屯断。整個實現(xiàn)是非侵入式的,已有代碼無須任何修改,只有AndroidManifest.xml略有改動裹纳。
ProxyApplication 實現(xiàn)
下面開始探討ProxyApplication本身如何實現(xiàn)。核心問題是兩個紧武,一是什么時機調(diào)用子類的initProxyApplication()方法剃氧,讓子類改變surrounding;二是如何加載DelegateApplication并讓應(yīng)用認為它就是真實的Application阻星。另外Android四大組件之一的ContentProvider會給我們帶來不少麻煩,需要妥善處理妥箕。
時機
理論上ProxyApplication對任何能夠訪問到的變量,包括Java層和Native層畦幢,都是可以替換(或者HOOK,類似的含義)的宇葱;比較有意義的除了ClassLoader外瘦真,還有Resources和各路Binder對象。通過這些手段可以實現(xiàn)非常多有意思的功能诸尽。具體如何替換ClassLoader、Resources等這里不深入討論您机,如有興趣,在網(wǎng)上可以找到很多相關(guān)資料年局。本文的重點是介紹框架本身际看,替換ClassLoader僅作為一個例子。
現(xiàn)在的問題是改變surrounding的時機必須足夠早矢否,特別是對于ClassLoader來說尤為重要。是否可以在Application: onCreate()里做蔼囊?我們通常認為衣迷,Application是一個Android應(yīng)用最早被加載的組件畏鼓;但當應(yīng)用注冊有ContentProvider的時候壶谒,這并不正確的。ContentProvider : onCreate()調(diào)用優(yōu)先于Application: onCreate()汗菜。
幸好挑社,我們還有另一個方法:attachBaseContext()巡揍。Android的幾個主要頂級組件(Application、Activity腮敌、Service)都是ContextWrapper的子類。ContextWrapper一方面繼承(inherit)了Context糜工,一方面又包含(composite)了一個Context對象(稱為mBase),對Context的實現(xiàn)為轉(zhuǎn)發(fā)給mBase對象處理油坝。這一個聽起來很繞的設(shè)計刨裆,是為了對這些頂級組件中的Context功能做延遲初始化(delay init)的處理免钻。這里不展開討論了崔拥,僅貼一些Android源代碼片段做參考。
// android.app.Application
public class Application extends ContextWrapper {
// ...
public application() {
super(null);
}
// ...
}
// android.content.ContextWrapper
public class ContextWrapper extends Context {
Context mBase;
// ...
public ContextWrapper(Context base) {
mBase = base;
}
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw newIllegalStateException("Base context already set");
}
mBase = base;
}
// ...
@Override
public AssetManager getAssets() {
return mBase.getAssets();
}
@Override
public Resources getResources()
{
return mBase.getResources();
}
// ...
}
ContextWrapper完成這個delay init語義的方法就是attachBaseContext()拆魏。可以這樣說慈俯,Application對象在剛剛構(gòu)造完成時是“殘廢”的渤刃,訪問所有Context的方法都會拋出NullPointerException。只有attachBaseContext()執(zhí)行完后贴膘,它的功能才完整卖子。
在ContentProvider: onCreate()中,我們知道Application: onCreate()還沒有運行刑峡,但已經(jīng)可以使用getContext().getApplicationContext()函數(shù)獲取Application對象,并訪問其Context方法突梦。顯然,Android的API設(shè)計者不能允許此時獲取的Application是“殘廢”的刊懈。結(jié)論是Application: attachBaseContext()必須要發(fā)生在ContentProvider: onCreate()之前,否則API將出現(xiàn)BUG虚汛;無論Android的系統(tǒng)版本如何變化,這一點也不能改變蛋辈。
于是殉疼,Application與ContentProvider的初始化次序是這樣的:Application: attachBaseContext()最早執(zhí)行捌年,然后是ContentProvider: onCreate()瓢娜,然后是Application: onCreate()礼预。我們的解決方案也就很簡單了:
public abstract class ProxyApplication extends Application {
protected abstract void initProxyApplication();
@Override
protected voidattachBaseContext (Context context) {
super.attachBaseContext(context);
initProxyApplication();
}
// ……
}
加載DelegateApplication
當子類的initProxyApplication()返回后托酸,ProxyApplication就要加載DelegateApplication,完成自己的歷史使命励堡。這一部分在onCreate()中完成,基本是些體力活刨疼,但也有些需要注意的地方鹅龄,下面分步驟簡述一下。
(1) 獲取DelegateApplication的Class Name
即從AndroidManifest.xml中獲取DELEGATE_APPLICAION的metadata值扮休,若不存在,則使用android.app.Application作為默認蜗搔。這一步比較簡單八堡。
String className = "android.app.Application";
String key = "DELEGATE_APPLICATION_CLASS_NAME";
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
super.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = appInfo.metaData;
if (bundle != null && bundle.containsKey(key)) {
className = bundle.getString(key);
if (className.startsWith("."))
className = super.getPackageName() + className;
}
(2) 加載DelegateApplication并生成對象
這里要注意的是使用哪個ClassLoader?答案是應(yīng)該用getClassLoader()(即Context:getClassLoader())秕重,而不是getClass().getClassLoader()。要仔細揣摩這兩者之間的差別二拐。
Class delegateClass = Class.forName(className, true, getClassLoader());
Application delegate = (Application) delegateClass.newInstance();
(3) 替換API層的所有Application引用
即把API層所有保存的ProxyApplication對象,都替換為新生成的DelegateApplication對象企软。以ProxyApplication的baseContext作為起點順藤摸瓜饭望,可以找到所有的位置仗哨,使用反射一一換掉铅辞。注意最后一個mAllApplications是List,要換掉其內(nèi)部的內(nèi)容苇倡。
baseContext.mOuterContext
baseContext.mPackageInfo.mApplication
baseContext.mPackageInfo.mActivityThread.mInitialApplication
baseContext.mPackageInfo.mActivityThread.mAllApplications
(4) 設(shè)置baseContext并調(diào)用onCreate
將控制權(quán)交給DelegateApplication囤踩。當然,后者會認為自己就是“正牌”的Application综慎,后續(xù)的其它組件也都會這么認為勤庐。這正是我們要的效果。
Method attach = Application.class.getDeclaredMethod("attach", Context.class);
attach.setAccessible(true);
attach.invoke(delegate, base);
delegate.onCreate();
ContentProvider
前面提到過埃元,Android的頂級組件Application、Activity阔拳、Service都是ContextWrapper类嗤,這個列表中并沒有ContentProvider。ContentProvider不是ContextWrapper遗锣,甚至不是Context,而是內(nèi)部有一個mContext變量弧圆,通過getContext()函數(shù)獲取這個Context。
那么搔预,ContentProvider:getContext()獲取到的是哪一個Context?實驗證明历造,ContentProvider:getContext()獲取的Context是Application船庇;準確的說,在Proxy/Delegate Application框架里鸭轮,是ProxyApplication。這就不符合框架的語義了荒典。那么吞鸭,我們需要像其它處理其它ProxyApplication引用一樣覆糟,把它換成DelegateApplication嗎?這是可行的:遍歷API層的ContentProvider列表滩字,將每一個ContentProvider中的mContext都替換為DelegateApplication。
但這種處理方式漓藕,會進一步增加對Android API層源代碼依賴挟裂,是否必要?畢竟Android的API文檔中诀蓉,并沒有規(guī)定ContentProvider:getContext()返回的必須是Application;如果要取得Application狐肢,正確的方式是getContext().getApplicationContext()沥曹。那么為什么getContext()就直接返回了Application對象碟联?我們可以從源代碼中找到答案:
// in ActivityThread:installProvider()
if (context.getPackageName().equals(ai.packageName)) {
c = context;
} else if (mInitialApplication != null &&
mInitialApplication.getPackageName().equals(ai.packageName)) {
c = mInitialApplication;
} else {
try {
c = context.createPackageContext(ai.packageName,
Context.CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
容易看出同窘,因為ProxyApplication對象的getPackageName()函數(shù)與ContentProvider對應(yīng)的包名相同,就會復用ProxyApplication對象作為Context想邦,而不會再創(chuàng)建一個新的packageContext。于是解決方案也很簡單了:
@Override
public String getPackageName() {
return "";
}
由于ProxyApplication不是最終的Application鹰椒,這并不會產(chǎn)生什么副作用呕童。
使用注意事項
不要保留ProxyApplication子類對象的引用,也不要在任何系統(tǒng)回調(diào)(包括onCreate)中做事情夺饲。onCreate()被基類用于加載DelegateApplication,而其它回調(diào)都不會再收到擂找。
在ProxyApplication: onCreate()執(zhí)行完成之后浩销,虛擬機中所有的線程棧和所有的JAVA對象,都不會再有ProxyApplication對象的引用慢洋。ProxyApplication對象將在下一次GC運行時被回收,這也意味著從ProxyApplication到DelegateApplication的替換進行得非常徹底败明。自然地太防,ProxyApplication也收不到其它回調(diào)了。DelegateApplication會正常的接收所有的回調(diào)杏头。
另外,在ProxyApplication子類中呢燥,如果需要獲取當前APK的包名寓娩,需要使用getBaseContext().getPackageName()呼渣,而不能簡單調(diào)用getPackageName()寞埠。原因在上面“再次對付ContentProvider”中有說明。