Android插件化進階——Atlas 源碼分析(一)

這兩天項目上要做 MVVM 和 DataBinding 的重構(gòu),所以插件化的文章就停了幾天,后面會分享一下關(guān)于 MVVM 架構(gòu)封裝相關(guān)的文章。這篇文章我準備作為我插件化系列文章的最終章,我將分析目前最成熟最強大的插件化框架 Atlas 的一些基本流程的源碼,以后如果有機會作更深入的研究,再進行插件化系列的更新。

Atlas 是手淘以及一系列阿里系 App 的插件化方案,實際上它又和其他插件化框架有一些區(qū)別,說的嚴謹點應該是一個容器化框架或者動態(tài)組件化框架,為什么這樣說,后面會講到。

Atlas 功能非常強大,技術(shù)也非常成熟,背后有阿里的一支技術(shù)實力非常強勁的團隊在維護,剛開始學習 Atlas 的時候,我發(fā)現(xiàn)它官方的文檔寫的不是特別詳細,導致一些配置我花了很多時間也沒有完全搞明白,所以開始的印象不是很好。但自從我看過了官方的 Gitbook 源碼文檔后,我對 Atlas 團隊的印象就徹底改觀了,可以說,Gitbook 文檔寫的非常詳細,而且邏輯清晰,實際上大家完全可以通過研讀官方的源碼文檔,就可以對 Atlas 的整體啟動流程和插件加載流程比較清楚了。

具體 Atlas 功能范圍以及和一些常用插件化框架的對比,可以看我之前的文章。

Gitbook 的地址

要看這個文檔,需要先把 atlas 從 Githud clone 下來,然后進入atlas-docs目錄,在終端輸入

gitbook serve

最后在瀏覽器輸入http://localhost:4000即可進入 Gitbook 觀看文檔,當然提前你得配置下 Gitbook,這個請讀者自行查閱步驟。這邊文章也是根據(jù)官網(wǎng)的分析流程進行修改優(yōu)化所得。言歸正傳,我們先來看下 Atlas 的啟動過程。

Atlas 的啟動過程

這里先說一下,為什么 Atlas 不算是真正意義上的插件化框架,而是一個動態(tài)組件化框架,主要原因是兩者的原理有區(qū)別。

插件化的核心思想實際上是一種埋坑機制借尸還魂,它會在宿主的 manifest 中預留很多的組件坑,在運行時,進行一系列的填坑操作,將插件的四大組件通過「借尸還魂」的手段來加載運行。

而 Atlas 有著本質(zhì)區(qū)別,在編譯期,每個插件 bundle 的 manifest 就已經(jīng)寫入到 apk 中了,所以運行的時候,不需要進行一些特殊操作,bundle 的組件的使用就像正常宿主中聲明的組件一樣,無需再進行額外處理了。

阿里的 Atlas 沙龍視頻中也非常明確的講述了 Atlas 的設計靈感就是 Android 系統(tǒng)本身,我們可以把 Android 系統(tǒng)看成一個容器化框架,那么 Android 系統(tǒng)中的所有 App 實際上就是一種插件,Android 系統(tǒng)本身是沒有這些插件的,而作為開發(fā)者,我們實際上開發(fā) App 就是開發(fā)插件的過程,通過發(fā)布,容器加載了插件,從而可以使用。

Atlas 就是按照這樣的思想,他們試圖將 Atlas 設計成這樣的組件,我們所有的 bundle 就是插件。這是從一個特殊的角度來看待 Atlas 框架實現(xiàn)的一個效果。我們下面來看一張時序圖,時序圖能夠幫我們更好的理清源碼的調(diào)用邏輯和思路。

時序圖

這張圖描述了集成了 Atlas 框架后,整個 App 的啟動流程,圖來源于官網(wǎng)。通過一些技術(shù)手段,我們繞過了一些系統(tǒng)的步驟,達到我們期望的目的。

本文只關(guān)注步驟 1-5。

1. 入口

app 的入口是application,這每一個 Android 開發(fā)者都了解,我們來看一下集成了 Atlas 后 application 是什么。代碼來源于atlas-demo。

<application
    android:name=".DemoApplication"
    android:allowBackup="true"
    ```
    >

看起來,入口就應該是DemoApplication,但我們看一下反編譯 apk 后拿到的 manifest

<application
    android:name="android.taobao.atlas.startup.AtlasBridgeApplication"
    android:allowBackup="true"
    ```
    >

很奇怪,實際的入口卻是AtlasBridgeApplication,原來,Atlas 在編譯期就已經(jīng)偷偷的替換了入口 application,但實際上,運行期依然會調(diào)用你自己寫的 DemoApplication 的相關(guān)方法。我們來看下AtlasBridgeApplication的源碼。根據(jù)常規(guī)的啟動流程,我們跟進方法attachBaseContext

protected void attachBaseContext(Context base){
    super.attachBaseContext(base);
    //一些邏輯

    //1.構(gòu)造 BridgeApplicationDelegate 對象
    Class BridgeApplicationDelegateClazz =getBaseContext().getClassLoader.loadClass("android.taobao.atlas.bridge.BridgeApplicationDelegate");
    Constructor<?> con = BridgeApplciationDelegateClazz.getConstructor(parTypes);
    mBridgeApplicationDelegate = con.newInstance(this,getProcessName(...));

    //2.執(zhí)行 BridgeApplicationDelegate 的 attachBaseContext 方法
    Method method = BridgeApplicationDelegateClazz.getDeclaredMethod("attachBaseContext");
    method.invoke(mBridgeApplicationDelegate);
}

如果大家是看源碼,這段代碼還是比較復雜也比較長,但實際上我們抽取關(guān)鍵部分發(fā)現(xiàn),它實際上就做了兩件事:

  1. 反射構(gòu)造 BridgeApplicationDelegate 實例
  2. 執(zhí)行 BridgeApplicationDelegate 的 attachBaseContext 方法

這時候我們就要保持耐心,進入 BridgeApplicationDelegate 類,先看它的構(gòu)造方法:

public BridgeApplicationDelegate(Application rawApplication ...){
    mRawApplication = rawApplication;
    PackageManagerDelegate.delegatepackageManager(
        rawApplication.getBaseContext()
    );
}

跟進方法delegatepackageManager:

public static void delegatepackageManager(Context context){
    mBaseContext = context;
    1.反射pm
    PackageManager manager = mBaseContext.getPackageManager();
    Class ApplicationPackageManager = Class.forName("android.app.ApplicationPackageManager");
    Field field = ApplicationPackageManager.getDeclaredField("mPM");
    field.setAccessible(true);
    Object rawPm = field.get(manager);
    2.動態(tài)代理
    Class IPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
    mPackageManagerProxyhandler = new PackageManagerProxyhandler(rawPm);
    mProxyPm = Proxy.newProxyInstance(mBaseContext.getClassLoader,new Class[]{IPackageManagerClass},mPackageManagerProxyhandler);
    3.替換pm

這段代碼,注釋寫的比較清楚了,核心思想就是把系統(tǒng)的 pm 替換為我們實現(xiàn)的動態(tài)代理PackageManagerProxyhandler,具體該動態(tài)代理的實現(xiàn)我們先不細講,動態(tài)代理不熟悉的同學可以自己去查閱下資料,網(wǎng)上很多,我們現(xiàn)在繼續(xù)看BridgeApplicationDelegateattachBaseContext的實現(xiàn)。

2. BridgeApplicationDelegate.attachBaseContext

public void attachBaseContext(){
    //2.1 hook 之前要準備的工作
    AtlasHacks.defineAndVerify();
    
    //2.2 回調(diào)預留接口
    launcher.initBeforeAtlas(mRawApplication.getBaseContext());

    //2.3 初始化 atlas
    Atlas.getInstance().init(mRawApplication,mIsUpdated);

    //2.4 處理 provider
    AtlasHacks.ActivityThread$AppBindData_providers.set(mBoundApplication,null);
}

通過注釋我們可以看到,方法被分成了四個部分,我們將一個個來看這四個步驟,需要提醒的是,大家需要記住現(xiàn)在的代碼位置,對應好文章開頭的時序圖,理清我們整體的代碼邏輯。

3. AtlasHacks.defineAndVerify

我們知道,Android 上所有的動態(tài)加載方案,有三個關(guān)鍵的地方是必須要處理的:

  • 動態(tài)加載 class
  • 動態(tài)加載資源
  • 處理四大組件

為了實現(xiàn)這三個目標,我們需要在系統(tǒng)關(guān)鍵調(diào)用處進行 Hook,例如我們會通過對 ClassLoader 做一些手腳來進行動態(tài)加載 Class,四大組件的處理比較特殊,這在之前我們也提到了。

回到正題,我們來看看 Atlas 為了實現(xiàn)上述的要求做了什么。我們先看下AtlasHacks這個類,這個類是用來定義 Atlas 需要 Hook 的類、方法、屬性等字段。這段代碼的靜態(tài)成員變量的代碼風格特別棒,大家可以學習一下,通過設置 AS 可以做到這樣的效果。

//AtlasHacks.java

     // Classes
    public static HackedClass<Object>                           LoadedApk;
    public static HackedClass<Object>                           ActivityThread;
    public static HackedClass<android.content.res.Resources>    Resources;

    // Fields
    public static HackedField<Object, Instrumentation>          ActivityThread_mInstrumentation;
    public static HackedField<Object, Application>              LoadedApk_mApplication;
    public static HackedField<Object, Resources>                LoadedApk_mResources;

    // Methods
    public static HackedMethod                                  ActivityThread_currentActivityThread;
    public static HackedMethod                                  AssetManager_addAssetPath;
    public static HackedMethod                                  Application_attach;
    public static HackedField<Object, ClassLoader>              LoadedApk_mClassLoader;

    // Constructor
    public static Hack.HackedConstructor                        PackageParser_constructor;

我們跟進方法defineAndVerify函數(shù):

//AtlasHacks.java
public static boolean defineAndVerify() throws AssertionArrayException{
    allClasses();
    allConstructors();
    allFields();
    allMethods();
}

這幾個方法就是對之前定義字段進行賦值,例如allFields()方法:

public static void allFields() throws HackAssertionException{
    ActivityThread_mInsturmentation = ActivityThread.field("mInstrumentation").ofType(Instrumentation.class);
    ActivityThread_mAllApplications = ActivityThread.field("mAllApplications").ofGenericType(ArrayList.class);
}

執(zhí)行到這里,Atlas 框架的準備工作就完成了,下面就是整個框架的初始化。

4. 回調(diào)預留接口

這里的回調(diào)接口就是調(diào)用我們在 Atlas 配置的時候設置的preLaunch()方法,該方法會在 Atlas 初始化之前運行的。我們來探索下 Atlas 是如何找到這個方法并調(diào)用的。

在時序圖中的第 4 步,BridgeApplicationDelegate.attachBaseContext()這個方法中,做了一個接口回調(diào)。

public void attachBaseContext(){
    String preLaunchStr = (String)RuntimeVariables.getFrameworkProperty("preLaunch");
    AtlasPreLauncher launcher = (AtlasPreLauncher) Class.forName(preLaunchStr).newInstance();
    launcher.initBeforeAtlas(mRawApplication.getBaseContext());
}

通過preLaunch字段讀取類名,反射類上的initBeforeAtlas方法,AtlasPreLauncher實際上還是個接口,供接入者使用,在這個點上,Atlas 還沒有對系統(tǒng)進行 hook,目前仍然是 Android 原生的運行環(huán)境。那么這個preLaunch字段到底在哪里定義的呢?我們來反推一下。

進入RuntimeVariables.java

public static Object getFrameworkProperty(String fieldName){
    Field field = FrameworkPropertiesClazz.getDeclaredField(fieldName);
    return field.get(FrameworkPropertiesClazz);
}

我們跟進FrameworkProperties發(fā)現(xiàn)這個類啥都沒用,是個空實現(xiàn),這種反常肯定有鬼,我們直接去看反編譯的代碼

public class FrameworkProperties{
    public static String autoStartBundles;
    public static String preLaunch;

    static{
        autoStartBundles = "com.taobao.firstbundle";
        preLaunch = "com.taobao.demo.DemoPreLaunch";
    }
}

看到這里我們就回想起來我們在 Gradle 配置的代碼了,原來 Atlas 在 gradle 插件在編譯的時候干了不少事。

    tBuildConfig{
        autoStartBundles = ['com.taobao.firstbundle']
        preLaunch = 'com.taobao.demo.DemoPreLaunch'
    }

這個部分牽扯開發(fā)-編譯-運行三個階段,下圖可以幫助你捋一下關(guān)系。


三個階段

5. atlas.init

準備工作做好之后,就是初始化了。我們回到Atlas.java這個類中。

public void init(Application application,boolean reset){
    //讀取配置項
    ApplicationInfo appInfo = mRawApplication.get...;
    mRealApplicationName = appInfo.metaData.getString("REAL_APPLICATION");
    boolean multidexEnable = appInfo.metaData.getBoolean("multidex_enable");

    if(multidexEnable){
      MultiDex.install(mRawApplication);
   }
    //...   
}

這里讀取了兩個在編譯期由 Atlas 插件寫到 manifest 中的數(shù)據(jù)multidexEnablemRealApplicationName,在 manifest 中它們是這樣的:

<meta-data android:name="REAL_APPLICATION" android:value="com.taobao.demo.DemoApplication"/>
<meta-data android:name="multidex_enable" android:value="true"/>

multidexEnable 是 true,這個是在 gradle 中可配的。
mRealApplicationName 實際上是 DemoApplication,即 app 工程在 manifest 中指定的啟動路徑。下面我們繼續(xù)看 init 方法。

public void init(Application application,boolean reset) {
   //...
   Atlas.getInstance().init(mRawApplication, mIsUpdated);
}

public void init(Application application,boolean reset) throws AssertionArrayException, Exception {
     //...

     //1. 換classloader 
     AndroidHack.injectClassLoader(packageName, newClassLoader);
     //2. 換Instrumentatio
     AndroidHack.injectInstrumentationHook(new InstrumentationHook(AndroidHack.getInstrumentation(), application.getBaseContext()));
     //3. hook ams
     try {
         ActivityManagerDelegate activityManagerProxy = new ActivityManagerDelegate();
         Object gDefault = null;
         if(Build.VERSION.SDK_INT>25 || (Build.VERSION.SDK_INT==25&&Build.VERSION.PREVIEW_SDK_INT>0)){
             gDefault=AtlasHacks.ActivityManager_IActivityManagerSingleton.get(AtlasHacks.ActivityManager.getmClass());
         }else{
               gDefault=AtlasHacks.ActivityManagerNative_gDefault.get(AtlasHacks.ActivityManagerNative.getmClass());
         }
         AtlasHacks.Singleton_mInstance.hijack(gDefault, activityManagerProxy);
      }catch(Throwable e){}
      //4. hook H
      AndroidHack.hackH();
}

這里,注釋也把具體步驟寫了,主要是對系統(tǒng)關(guān)鍵地方進行了 hook,hook 的具體細節(jié)大家可以看源碼或者田維術(shù)的 blog。

6. 處理 provider

在 2.4 步驟中,處理了 provider。

public void attachBaseContext(){
    //...

   //2.4 處理provider
   Object mBoundApplication = AtlasHacks.ActivityThread_mBoundApplication.get(activityThread);
   mBoundApplication_provider = AtlasHacks.ActivityThread$AppBindData_providers.get(mBoundApplication);
   if(mBoundApplication_provider!=null && mBoundApplication_provider.size()>0){
           AtlasHacks.ActivityThread$AppBindData_providers.set(mBoundApplication,null);
    }
}

這里先讀取 provider 數(shù)據(jù),如果有的話,就從系統(tǒng)中刪除,讓系統(tǒng)認為 apk 并沒有申請任何 provider,那么為啥要這么做呢,我們先回顧下 app 啟動的流程:


啟動流程

上圖中可以看到,第 4 步和第 7 步這兩個關(guān)鍵調(diào)用之間,第 5 步調(diào)用了installContentProviders

private void installContentProviders(Context context, List<ProviderInfo> providers) {
    for (ProviderInfo cpi : providers) {
        installProvider(context, null, cpi,...);
    }
}

收集了所有 provider 的信息,并且調(diào)用了installProvider方法:

private IActivityManager.ContentProviderHolder installProvider(Context context,IActivityManager.ContentProviderHolder holder, ProviderInfo info,...) {
    final java.lang.ClassLoader cl = c.getClassLoader();
   localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
   //...
}

這里的函數(shù)會根據(jù) manifest 中登記的 provider 信息,實例化對象。但有些 provider 是存在于 bundle 中的,在 主 dex 中并不存在,如果不先清除掉 provider 的信息,進行延遲加載,程序就會出現(xiàn)ClassNotFind崩潰,這就是為啥要有一個清除操作的原因。

未完待續(xù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容