Android中動態替換Application的實現

1.背景

最近一直在做優化相關事情,需要在啟動時干預載入dex文件的過程,而AndroidManifest設定的Application類已經在dex文件中,在載入dex之前,不能找到這個Application類。所以我們需要替換原有的Application為ProxyApplication。使其應用啟動時載入ProxyApplication,我們在其中實現載入dex等一些流程處理。而後要替換回原有的Application(以下為RealApplication),確保應用正常運行,並且要保持生命周期、初始化順序不變,屏蔽對於應用中getContext,getApplicationContext的影響。

2.需要處理的問題

在我們替換ProxyApplication之後,在初始化時創建RealApplication,還需要滿意以下幾個條件:

創建RealApplication,維護正常的生命周期,並進行回調。

對應用中屏蔽掉ProxyApplication,對於下層無感知。在Activity等調用getApplicationContext之後,應該返回RealApplication。

ContentProvider創建時機比較特殊,在滿足正常的初始化順序之後,也要屏蔽ProxyApplication的存在。

3.方案具體實現

在AndroidManifest.xml文件中替換Application為ProxyApplication,可以使用自動化方式,或者打包方式,細節過程這裡不做討論。這裡主要敘述創建RealApplication的過程。替換了ProxyApplication之後,對於系統而言ProxyApplication就是應用初始化的入口,所有的回調均是在ProxyApplication中發生。我們主要關注attachBaseContext和onCreate的回調。

3.1.創建RealApplication

創建RealApplication,我們可以使用反射的方式newInstance創建對象,然後執行回調attachBaseContext。但是對於不同的系統版本,內部執行的細節可能不同,或者有其它相關邏輯的處理,所以我們採用另一種方式進行處理。首先看系統源碼的如何實現,這裡選擇8.0.0的系統源碼進行分析,其它版本去androidxref.com/查看。

我們知道,Java層初始化可以認為是從android.app.ActivityThread開始,所以從ActivityThread開始查看。ActivityThread中存在靜態方法currentActivityThread返回實例。

ActivityThread內部存在成員變數AppBindData mBoundApplication。AppBindData是一個靜態內部類,其中包含成員變數LoadedApk info。查看android.app.LoadedApk源代碼,發現創建Application的makeApplication方法。

上面去掉不相關代碼之後,可以明顯看出:

如果緩存mApplication不為空,則直接返回。

mApplication為空時,則創建RealApplication,並且執行相關的回調。創建RealApplication時,類名是從mApplicationInfo.className中獲取。

添加新創建RealApplication到mActivityThread.mAllApplications。

賦值給緩存mApplication。

所以我們在調用makeApplication之前,需要將mApplication置為null,否則會直接返回ProxyApplication的實例。

首先,通過android.app.ActivityThread中靜態方法獲取實例

然後,通過ActivityThread實例,獲得LoadedApk實例。為了使makeApplication順利執行,先設置mApplication為null。移除mAllApplications中ProxyApplication的實例。LoadedApk中mApplicationInfo和AppBindData中appInfo都是ApplicationInfo類型,需要分別替換className欄位的值為RealApplication的實際類全名。

之後,反射調用系統的makeApplicati

最終,使用上面三個方法組合起來,完成我們的邏輯。

這樣,在ProxyApplication.attachBaseContext中,調用makeApplication創建RealApplication,並且內部已經完成對於RealApplication的attchBaseContext的回調。在ProxyApplication.onCreate中只需要回調RealApplication實例的onCreate,即可完成對於RealApplication的創建,已經內部替換以及正常的生命周期的回調。而且在Activity中調用getApplicationContext返回的值,實際上也是LoadedApk中mApplication的值,同時也保證對於Activity等地方屏蔽ProxyApplication的目的。

3.2.ContentProvider中getContext問題

通過閱讀系統的源代碼(或者自己打log),可以很容易的知道,Application和ContentProvider的初始化順序是:Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate

在保持正常Application生命周期的情況下,也要保持對ContentProvider中無感知。因為ContentProvider中也存在getContext方法,看ContentProvider的源代碼實現:

其中mContext被賦值的有兩個地方,一個在構造方法,一個是attchInfo的時候。繼續追蹤源代碼中使用構造方法初始化,或者調用attachInfo的地方,結果在android.app.ActivityThread中找到installProvider方法中存在著調用關係。

以上源代碼中,省略了不相關的部分代碼。可以看出,使用反射調用ContentProvider無參構造方法創建實例,然後調用了attachInfo,傳遞的Context為installProvider方法中的參數。那這個參數哪傳遞過來的呢?帶著疑問從源碼中繼續追蹤調用關係,發現installContentProviders內部在初始化每個ContentProvider時,分別調用了一次installProvider。看installContentProviders的主要邏輯:

可以明確,installContentProviders中調用installProvider時傳遞的Context,也是由方法調用時傳遞的參數。繼續向上追蹤發現ActivityThread.handleBindApplication在初始化ContentProvider時調用了installContentProviders,看源代碼中關鍵部分。

通過這部分的源代碼,我們明確知道,最終通過attachInfo設置給ContentProvider中的Context的實際類型是Application。其中返回Application這行語句中,data的類型是AppBindData,info的類型是LoadedApk,所以makeApplication的具體實現就是章節3.1中列舉的源代碼。

在App初始化時,系統調用makeApplication創建了ProxyApplication實例,同時回調了attachBaseContext(Context context)。所以這個方法返回的就是App初始化時ProxyApplication,調用發生ProxyApplication.attachBaseContext之後,ProxyApplication.onCreate之前。所以我們沒有辦法在這兩個方法生命周期內進行替換為RealApplication。

如果在attachBaseContext中設置mInitialApplication的值,在初始化ContentProvider之前,又被重新設置為ProxyApplication。這樣在初始化ContentProvider時傳遞的Context依然是ProxyApplication,在ContentProvider的onCreate調用getContext返回的依然不是RealApplication,這種情況也是不能滿足對下層無感知的需求。

如果在onCreate中設置mInitialApplication,也是不能起作用的,因為此時已經完成了ContentProvider的初始化過程。

仔細閱讀源代碼,發現初始化ContentProvider是有一系列條件控制,那麼我們可以提前修改data.providers為null,讓系統不執行ContentProvider的初始化。我們在ProxyApplication.attachBaseContext主動調用installContentProviders,傳遞RealApplication給ContentProvider。在ProxyApplication.onCreate中恢復data.providers的數據,減小對其它邏輯可能帶來的影響。具體的實現分兩部分,在attachBaseContext中調用我們實現的installContentProviders,源代碼如下:

在onCreate中調用的restoreProviderInfo,恢復AppBindData中的List<ProviderInfo> providers原來的值,具體源代碼如下:

4. 總結

以上大致講解了從源代碼的實現進行分析,了解系統基本的實現原理之後,進行相關的反射修改內部的值,達到我們替換Application的目的。這種方案,接入成本比較低,但是新系統出現之後,可能出現兼容性的問題,需要每次發布新系統之後進行相關的適配。但是這種解決問題的思路,可以借鑒一下,從別的地方進行Java Hook,實現一些黑科技的東西。而且替換Application這個方式,不只是可以應用在安裝速度優化上面,同時也可以應用在其他一些場景上。

推薦閱讀:

在安卓上部署伺服器
安卓最好的數獨遊戲是哪個?
金立將發布互聯網手機品牌 IUNI,它可能複製小米的成功嗎?
為什麼嗶哩嗶哩安卓客戶端體驗比優酷都好呢?
2015年後,如何看待「小米在做加法,魅族在做減法」的說法?

TAG:Android | 動態 | 應用軟體 |