Proxy/Delegate Application框架

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:

  1. ProxyApplication:框架會提供一個ProxyApplication抽象基類(abstract class)蜡吧,使用者需要繼承這個類,并重載其initProxyApplication()方法占键,在其中改變surrounding昔善,如替換ClassLoader等。
  2. 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”中有說明。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蓝角,一起剝皮案震驚了整個濱河市饭冬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昌抠,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裁厅,死亡現(xiàn)場離奇詭異侨艾,居然都是意外死亡,警方通過查閱死者的電腦和手機蒋畜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門姻成,熙熙樓的掌柜王于貴愁眉苦臉地迎上來愿棋,“玉大人,你說我怎么就攤上這事糠雨。” “怎么了琅攘?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵松邪,是天一觀的道長。 經(jīng)常有香客問我逗抑,道長寒亥,這世上最難降的妖魔是什么荧关? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任忍啤,我火速辦了婚禮,結(jié)果婚禮上同波,老公的妹妹穿的比我還像新娘。我一直安慰自己参萄,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布校赤。 她就那樣靜靜地躺著筒溃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浑测。 梳的紋絲不亂的頭發(fā)上歪玲,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音滥崩,去河邊找鬼。 笑死蜂科,一個胖子當著我的面吹牛短条,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茸时,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼厕氨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起命斧,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤国葬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后汇四,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡序宦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年背苦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秕噪。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡厚宰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铲觉,到底是詐尸還是另有隱情,我是刑警寧澤备燃,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布并齐,位于F島的核電站客税,受9級特大地震影響况褪,放射性物質(zhì)發(fā)生泄漏更耻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一食侮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锯七,春花似錦、人聲如沸域蜗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袱蜡。三九已至,卻和暖如春坪蚁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巫橄。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工茵典, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人统阿。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓扶平,卻偏偏與公主長得像帆离,于是被迫代替她去往敵國和親结澄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容