1.背景
最近一直在做優(yōu)化相關(guān)事情守谓,需要在啟動時干預(yù)加載dex文件的過程犁珠,而AndroidManifest設(shè)定的Application類已經(jīng)在dex文件中缭裆,在加載dex之前颤枪,不能找到這個Application類。所以我們需要替換原有的Application為ProxyApplication嗽桩。使其應(yīng)用啟動時加載ProxyApplication岳守,我們在其中實現(xiàn)加載dex等一些流程處理。而后要替換回原有的Application(以下為RealApplication)碌冶,確保應(yīng)用正常運行,并且要保持生命周期涝缝、初始化順序不變扑庞,屏蔽對于應(yīng)用中g(shù)etContext,getApplicationContext的影響拒逮。
2.需要處理的問題
在我們替換ProxyApplication之后罐氨,在初始化時創(chuàng)建RealApplication,還需要滿意以下幾個條件:
創(chuàng)建RealApplication滩援,維護正常的生命周期栅隐,并進行回調(diào)。
對應(yīng)用中屏蔽掉ProxyApplication,對于下層無感知租悄。在Activity等調(diào)用getApplicationContext之后谨究,應(yīng)該返回RealApplication。
ContentProvider創(chuàng)建時機比較特殊泣棋,在滿足正常的初始化順序之后胶哲,也要屏蔽ProxyApplication的存在。
3.方案具體實現(xiàn)
在AndroidManifest.xml文件中替換Application為ProxyApplication潭辈,可以使用自動化方式鸯屿,或者打包方式,細節(jié)過程這里不做討論把敢。這里主要敘述創(chuàng)建RealApplication的過程寄摆。替換了ProxyApplication之后,對于系統(tǒng)而言ProxyApplication就是應(yīng)用初始化的入口修赞,所有的回調(diào)均是在ProxyApplication中發(fā)生婶恼。我們主要關(guān)注attachBaseContext和onCreate的回調(diào)。
3.1 創(chuàng)建RealApplication
創(chuàng)建RealApplication榔组,我們可以使用反射的方式newInstance創(chuàng)建對象熙尉,然后執(zhí)行回調(diào)attachBaseContext。但是對于不同的系統(tǒng)版本搓扯,內(nèi)部執(zhí)行的細節(jié)可能不同检痰,或者有其它相關(guān)邏輯的處理,所以我們采用另一種方式進行處理锨推。首先看系統(tǒng)源碼的如何實現(xiàn)铅歼,這里選擇8.0.0的系統(tǒng)源碼進行分析,其它版本去http://androidxref.com/查看换可。
我們知道椎椰,Java層初始化可以認為是從android.app.ActivityThread開始,所以從ActivityThread開始查看沾鳄。ActivityThread中存在靜態(tài)方法currentActivityThread返回實例慨飘。
ActivityThread內(nèi)部存在成員變量AppBindData mBoundApplication。AppBindData是一個靜態(tài)內(nèi)部類译荞,其中包含成員變量LoadedApk info瓤的。查看android.app.LoadedApk源代碼,發(fā)現(xiàn)創(chuàng)建Application的makeApplication方法吞歼。
上面去掉不相關(guān)代碼之后圈膏,可以明顯看出:
如果緩存mApplication不為空,則直接返回篙骡。
mApplication為空時稽坤,則創(chuàng)建RealApplication丈甸,并且執(zhí)行相關(guān)的回調(diào)。創(chuàng)建RealApplication時尿褪,類名是從mApplicationInfo.className中獲取睦擂。
添加新創(chuàng)建RealApplication到mActivityThread.mAllApplications。
賦值給緩存mApplication茫多。
所以我們在調(diào)用makeApplication之前祈匙,需要將mApplication置為null,否則會直接返回ProxyApplication的實例天揖。
首先夺欲,通過android.app.ActivityThread中靜態(tài)方法獲取實例
然后,通過ActivityThread實例今膊,獲得LoadedApk實例些阅。為了使makeApplication順利執(zhí)行,先設(shè)置mApplication為null斑唬。移除mAllApplications中ProxyApplication的實例市埋。LoadedApk中mApplicationInfo和AppBindData中appInfo都是ApplicationInfo類型,需要分別替換className字段的值為RealApplication的實際類全名恕刘。
之后缤谎,反射調(diào)用系統(tǒng)的makeApplication
最終,使用上面三個方法組合起來褐着,完成我們的邏輯坷澡。
這樣,在ProxyApplication.attachBaseContext中含蓉,調(diào)用makeApplication創(chuàng)建RealApplication频敛,并且內(nèi)部已經(jīng)完成對于RealApplication的attchBaseContext的回調(diào)。在ProxyApplication.onCreate中只需要回調(diào)RealApplication實例的onCreate馅扣,即可完成對于RealApplication的創(chuàng)建斟赚,已經(jīng)內(nèi)部替換以及正常的生命周期的回調(diào)。而且在Activity中調(diào)用getApplicationContext返回的值差油,實際上也是LoadedApk中mApplication的值拗军,同時也保證對于Activity等地方屏蔽ProxyApplication的目的。
3.2 ContentProvider中g(shù)etContext問題
通過閱讀系統(tǒng)的源代碼(或者自己打log)蓄喇,可以很容易的知道食绿,Application和ContentProvider的初始化順序是:Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate
在保持正常Application生命周期的情況下,也要保持對ContentProvider中無感知公罕。因為ContentProvider中也存在getContext方法,看ContentProvider的源代碼實現(xiàn):
其中mContext被賦值的有兩個地方耀销,一個在構(gòu)造方法楼眷,一個是attchInfo的時候铲汪。繼續(xù)追蹤源代碼中使用構(gòu)造方法初始化,或者調(diào)用attachInfo的地方罐柳,結(jié)果在android.app.ActivityThread中找到installProvider方法中存在著調(diào)用關(guān)系掌腰。
以上源代碼中,省略了不相關(guān)的部分代碼张吉〕萘海可以看出,使用反射調(diào)用ContentProvider無參構(gòu)造方法創(chuàng)建實例肮蛹,然后調(diào)用了attachInfo勺择,傳遞的Context為installProvider方法中的參數(shù)。那這個參數(shù)哪傳遞過來的呢伦忠?帶著疑問從源碼中繼續(xù)追蹤調(diào)用關(guān)系省核,發(fā)現(xiàn)installContentProviders內(nèi)部在初始化每個ContentProvider時,分別調(diào)用了一次installProvider昆码∑遥看installContentProviders的主要邏輯:
可以明確,installContentProviders中調(diào)用installProvider時傳遞的Context赋咽,也是由方法調(diào)用時傳遞的參數(shù)旧噪。繼續(xù)向上追蹤發(fā)現(xiàn)ActivityThread.handleBindApplication在初始化ContentProvider時調(diào)用了installContentProviders,看源代碼中關(guān)鍵部分脓匿。
通過這部分的源代碼淘钟,我們明確知道,最終通過attachInfo設(shè)置給ContentProvider中的Context的實際類型是Application亦镶。其中返回Application這行語句中日月,data的類型是AppBindData,info的類型是LoadedApk缤骨,所以makeApplication的具體實現(xiàn)就是章節(jié)3.1中列舉的源代碼爱咬。
在App初始化時,系統(tǒng)調(diào)用makeApplication創(chuàng)建了ProxyApplication實例绊起,同時回調(diào)了attachBaseContext(Context context)精拟。所以這個方法返回的就是App初始化時ProxyApplication,調(diào)用發(fā)生ProxyApplication.attachBaseContext之后虱歪,ProxyApplication.onCreate之前蜂绎。所以我們沒有辦法在這兩個方法生命周期內(nèi)進行替換為RealApplication。
如果在attachBaseContext中設(shè)置mInitialApplication的值笋鄙,在初始化ContentProvider之前师枣,又被重新設(shè)置為ProxyApplication。這樣在初始化ContentProvider時傳遞的Context依然是ProxyApplication萧落,在ContentProvider的onCreate調(diào)用getContext返回的依然不是RealApplication践美,這種情況也是不能滿足對下層無感知的需求洗贰。
如果在onCreate中設(shè)置mInitialApplication,也是不能起作用的陨倡,因為此時已經(jīng)完成了ContentProvider的初始化過程敛滋。
仔細閱讀源代碼,發(fā)現(xiàn)初始化ContentProvider是有一系列條件控制兴革,那么我們可以提前修改data.providers為null绎晃,讓系統(tǒng)不執(zhí)行ContentProvider的初始化。我們在ProxyApplication.attachBaseContext主動調(diào)用installContentProviders杂曲,傳遞RealApplication給ContentProvider庶艾。在ProxyApplication.onCreate中恢復(fù)data.providers的數(shù)據(jù),減小對其它邏輯可能帶來的影響解阅。具體的實現(xiàn)分兩部分落竹,在attachBaseContext中調(diào)用我們實現(xiàn)的installContentProviders,源代碼如下:
在onCreate中調(diào)用的restoreProviderInfo货抄,恢復(fù)AppBindData中的List<ProviderInfo> providers原來的值述召,具體源代碼如下:
4. 總結(jié)
以上大致講解了從源代碼的實現(xiàn)進行分析,了解系統(tǒng)基本的實現(xiàn)原理之后蟹地,進行相關(guān)的反射修改內(nèi)部的值积暖,達到我們替換Application的目的。這種方案怪与,接入成本比較低夺刑,但是新系統(tǒng)出現(xiàn)之后,可能出現(xiàn)兼容性的問題分别,需要每次發(fā)布新系統(tǒng)之后進行相關(guān)的適配遍愿。但是這種解決問題的思路,可以借鑒一下耘斩,從別的地方進行Java Hook沼填,實現(xiàn)一些黑科技的東西。而且替換Application這個方式括授,不只是可以應(yīng)用在安裝速度優(yōu)化上面坞笙,同時也可以應(yīng)用在其他一些場景上。