Android外掛化之旅
一、概述
Android外掛化技術一直是安卓開發中一個重要的方向,大概12年就被提出,發展至今已逐漸趨於成熟,很多大廠都有自己的一套外掛化方案,諸如淘寶的Atlas,滴滴的VirtualAPK,360的RePlugin等。外掛化技術的發展得益於業務的不斷新增,諸如淘寶APP,裡面有聚划算,拍賣,餓了麼,淘票票等業務功能模組(這裡只考慮原生介面),如果今天餓了麼有個Bug要修復發版,明天淘票票想加多個功能,是否每次都需要去更新淘寶客戶端?這個代價未免太大,同時,作為淘寶的開發人員,我是否還需要幫忙去維護餓了麼的第三方業務程式碼?而作為餓了麼開發人員,我自己又要維護自己客戶端的程式碼,又要維護在你淘寶上的程式碼嗎?在這種擁有眾多業務的大廠裡,外掛化技術就應運而生。
二、概念區分
近年來,除了外掛化技術,元件化技術,熱修復等也同樣廣受關注,這裡主要做一下概念的區分:
外掛化:也叫動態載入技術,分宿主APK和外掛APK,宿主APK可以理解為就是安裝到手機的主APK(諸如手機淘寶),各個功能模組抽取變成外掛APK(諸如餓了麼,淘票票),這些外掛APK可以隨著宿主APK一起編譯打包安裝到手機上,也可以變成遠端APK放在伺服器,按需下載安裝,實現功能的動態配置。從廣義上理解,可以把Android系統當成一個宿主APK,各個安裝到手機上的軟體當成外掛APK,從而組成一個外掛化系統。
元件化:元件化技術實現了在Debug除錯階段,每個功能模組可以獨立變成APP除錯,但在打包編譯階段,其最終還是將所有模組打包成一個APK。
熱修復:熱修復技術有助於我們在使用者無感知的時候修復APK,悄無聲息的將Bug修復掉,我們希望熱修復它是不新增資原始檔,四大元件等操作,只是單純的解決程式碼邏輯上的Bug,可以簡單理解外掛化技術是熱修復的高階版
三、外掛化的優缺點
優點:
- 讓使用者無需安裝APK就能升級應用功能,減少發版頻率,增加使用者體驗
- 按需編譯載入,有效減小主APK體積,實現功能的靈活配置
- 模組化,降低耦合性,有利於多人合作開發同一個專案
缺點:
- Android上的黑科技越來越不被Android新系統待見,諸如Android 9.0系統已禁止非 SDK 介面的呼叫,而外掛化技術中又或多或少使用了一些反射。這會使得外掛化技術在新系統的表現上存在一些欠缺。
- 專案的構建過程變得複雜
四、外掛化技術中的兩個主要問題
正常情況下,apk被安裝後,apk裡面的程式碼和資源會被存放到系統的某處,以便系統能找到它。而外掛APK未被安裝,系統是找不到它裡面的程式碼和資源的,所以如何載入外掛APK中的程式碼和資源就成為了主要問題。針對這兩個問題,下面主要介紹一種經典思路,達到拋磚引玉,有助於我們對外掛化有個更好認識
如何載入外掛APK中的Java程式碼?
Android中兩個主要的Classloader,PathClassLoader和DexClassLoader,都是繼承自BaseDexClassLoader:
DexClassLoader:可以載入包含classes.dex實體的.jar或.apk檔案
PathClassLoader:只能載入已安裝APK的dex檔案
顯然DexClassLoader可以滿足我們外掛化中對Java程式碼的動態載入,如下程式碼所示可以通過傳入APK路徑獲取相應的DexClassLoader,接著通過呼叫DexClassLoader的loadClass方法獲取相應的類例項:
//dexPath傳入當前外掛APK在SD卡中的路徑 DexClassLoader pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader()); //根據類名獲取位元組碼物件 Class<?> mClass=pluginDexClassLoader loadClass("這裡傳入需要載入的完整路徑類名"); //通過位元組碼物件建立類的例項 Object newInstance = mClass.newInstance();
類的例項可以通過上述拿到,然而這又會出現另外一個問題:已知Android系統中Activity頁面的生命週期是由系統控制的,如果單純使用DexClassLoader載入外掛APK中的Activity,加載出來的也只是一個普通的物件,不具備頁面的生命週期,曾看到過一個很生動的比喻:如果說系統建立的Activity是一個擁有四肢能動能跳的人的話,那麼我們手動建立的Activity只是一個人偶,這個人偶雖然也有四肢,但是他動不了,因為他沒有對應的掌控者。
針對這個問題,可以使用代理來實現,就如為了讓這個木偶動起來,可以將這個木偶綁到活人身上,當活人動的時候,木偶也能跟著動。
具體的思路:
如何使用代理模式?可以先在宿主APK中註冊好一個空的代理Activity頁面,這個代理Activity擁有正常的生命週期,然後將外掛Activity 和代理Activity 繫結起來,當代理Activity觸發某一個生命週期的時候,也去通知外掛Activity,讓外掛Activity擁有一個偽生命週期。
之前人們的採用的方法是使用反射去管理代理Activity的生命週期,但這樣存在一些不便,比如反射程式碼寫起來複雜,並且過多使用反射有一定的效能開銷,後來採用了一種更為優雅的方式,就是採用介面機制,將代理Activity的生命週期提取出來作為一個介面,暫命名為PluginInterface,然後讓外掛Activity實現他:
public interface PluginInterface { void onCreate(Bundle saveInstance); void attachContext(Activity context); void onStart(); void onResume(); void onRestart(); void onDestroy(); void onStop(); void onPause(); }
接著回到代理Activity,第一步,當呼叫外掛Activity的時候,實際是呼叫了代理Activity,在代理Activity的onCreate生命週期裡,使用之前說的載入類的方法建立外掛Activity類例項,然後在代理Activity的各個生命週期動態的呼叫外掛Activity的偽生命週期,以此達到同步效果,代理Activity的具體程式碼如下:
public class ProxyActivity extends Activity { private PluginInterface pluginInterface; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //拿到要啟動的Activity String className = getIntent().getStringExtra("className"); try { //載入該Activity的位元組碼物件 Class<?> aClass = PluginManager.getInstance().getPluginDexClassLoader().loadClass(className); //建立該Activity的示例 Object newInstance = aClass.newInstance(); //面向介面程式設計,外掛Activity需要實現PluginInterface介面 if (newInstance instanceof PluginInterface) { pluginInterface = (PluginInterface) newInstance; //將代理Activity的例項傳遞給外掛Activity,以此讓外掛APK用於宿主的上下文 pluginInterface.attachContext(this); //建立bundle用來與外掛apk傳輸資料 Bundle bundle = new Bundle(); //將當前生命週期同步給外掛Activity pluginInterface.onCreate(bundle); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } @Override public void onStart() { pluginInterface.onStart(); super.onStart(); } @Override public void onResume() { pluginInterface.onResume(); super.onResume(); } @Override public void onRestart() { pluginInterface.onRestart(); super.onRestart(); } @Override public void onDestroy() { pluginInterface.onDestroy(); super.onDestroy(); } @Override public void onStop() { pluginInterface.onStop(); super.onStop(); } @Override public void onPause() { pluginInterface.onPause(); super.onPause(); } /** * 在外掛APK中,外掛Activity調起其本身的Activity,實際還是一直呼叫代理Activity,不斷重複上述流程 */ @Override public void startActivity(Intent intent) { Intent newIntent = new Intent(this, ProxyActivity.class); newIntent.putExtra("className", intent.getComponent().getClassName()); super.startActivity(newIntent); } }
如何載入外掛APK中的資原始檔?
宿主APK中是沒有外掛APK中的資源的,如果在代理Activity中直接像平時一樣使用R.來引用外掛APK中的資源的話是會報錯的。Activity中有兩個系統方法是和載入資源有關,我們需要在代理Activity中重寫這兩個方法,返回相應外掛APK的Resource物件,這樣才能順利引用外掛APK中的資源。
/** Return an AssetManager instance for your application's package. */ public abstract AssetManager getAssets(); /** Return a Resources instance for your application's package. */ public abstract Resources getResources();
AssetManager 中有一個addAssetPath方法,該方法可以通過傳入指定的APK路徑然後獲取該APK的AssetManager,但這個方法是一個隱藏方法,需要通過反射來獲取,緊接著將獲取到的AssetManager傳入Resources構造方法中,以此拿到相應外掛APK中的Resources物件,示例程式碼如下:
//dexPath是Plugin的路徑, //optimizedDirectory是Plugin的快取路徑, //libraryPath可以為null, //parent為父類載入器 pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader()); pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES); { AssetManager assets = null; try { assets = AssetManager.class.newInstance(); Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class); addAssetPath.invoke(assets, dexPath); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
接下來重寫代理Activity中的getResources()方法,返回剛才新建立的Resources方法
/** * 注意:三方呼叫拿到對應載入的三方Resources * @return */ @Override public Resources getResources() { return pluginResources; }
五、市場上的外掛化框架
名稱 | 團隊 | Github |
---|---|---|
DroidPlugin | 奇虎360 | ofollow,noindex">DroidPlugin |
PluginManager | 個人開發者 | PluginManager |
AndroidDynamicLoader | 個人開發者 | AndroidDynamicLoader |
dynamic-load-apk | 任玉剛 | dynamic-load-apk |
Small | 開源組織Wequick | Small |
DynamicAPK | 攜程 | DynamicAPK |
VirtualAPK | 滴滴 | VirtualAPK |
RePlugin | 奇虎360 | RePlugin |
Atlas | 手機淘寶 | Atlas |
其中任玉剛的dynamic-load-apk外掛化框架就是採用了上述所說的代理思路,上訴有些框架已經很久沒有維護了,現在比較熱門且還在維護的應屬360的RePlugin,嘀嘀的VirtualAPK,手機淘寶的Atlas以及Small框架,其中Small框架支援Android和ios,較為輕量,但似乎還沒辦法做到按需載入。而淘寶Atlas框架相比其他具有更豐富的功能,除了可以按需載入相應的功能模組外,還具備熱修復功能。
六、是否使用外掛化技術的思考:
- 是否存在版本較多需要不斷更新發版的情況?
- 是否有較多的業務模組?
- 是否開發人員眾多?
- .....