這兩天項目上要做 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 功能范圍以及和一些常用插件化框架的對比,可以看我之前的文章。
要看這個文檔,需要先把 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),它實際上就做了兩件事:
- 反射構(gòu)造 BridgeApplicationDelegate 實例
- 執(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ù)看BridgeApplicationDelegate中attachBaseContext的實現(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ù)multidexEnable和mRealApplicationName,在 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ù)。