Android 外掛化框架 DynamicLoadApk 原始碼分析
DynamicLoadApk 應該算是 Android 外掛化諸多框架中資歷比較老的一個了。它的專案地址在: dynamic-load-apk 。該專案執行之後的效果是,使用 Gradle 編譯出外掛包和宿主包,都是以 APK 的形式。安裝宿主包之後,通過 ADB 將外掛包 push 到手機中。啟動宿主包時,它會自動進行掃描將外掛載入到應用中。點選外掛之後,進入到外掛的應用介面。
印象中最初接觸的外掛都是以單獨安裝的形式存在的,比如我可以做一個基礎的應用,然後在該應用的基礎上開發外掛。使用者可以對外掛進行選擇,然後下載並安裝,以讓自己的應用具有更豐富的功能。外掛化也算是一種比較實用的技術,畢竟我們使用 Chrome 和 AS 的時候不是一樣要載入外掛。只是比較反感的是去修改底層的程式碼,容易給系統帶來不穩定因素不說,技術到了一些人手裡,你知道他用來幹什麼。外掛化挺好,但真的要去推廣這項技術,還是看好 Google 官方去進行規範。
技術要服務於產品,好的產品不一定要高超的技術,技術並不是最重要的,重要的是你究竟想要表達什麼。這就像國內很多人只注重數理化,不注重人文學科。相比於國內的技術精英,我還是比較贊同 Google 站在整個生態的角度去考慮技術演進。前些日子社群裡對外掛化的討論: 移動開發的羅曼蒂克消亡史 。好吧,我自己的理解是,這從來就不是什麼羅曼蒂克。
DynamicLoadApk 外掛化的實現方式還是挺有意思的,它使用純 Java 實現,沒有涉及 Native 層的程式碼,下面我理了下 DynamicLoadApk 的 Demo 程式的整個執行過程。後續的文章我們就圍繞這張圖進行,

首先是掃描檔案路徑並載入 APK,這裡需要解析 APK 檔案的資訊,它是本質上是通過 PMS 實現的;
public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) { mFrom = DLConstants.FROM_EXTERNAL; // 通過 PMS 獲取包資訊,這裡獲取了 Activity 和 Service 的資訊 PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); if (packageInfo == null) { return null; } DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath); if (hasSoLib) { copySoLib(dexPath); } return pluginPackage; } 複製程式碼
然後,它通過呼叫 preparePluginEnv()
方法來建立 AssetManager, DexClassLoader 和 Resource 等。我們的外掛類載入各種資源和類的時候使用的就是這哥仨:
private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) { DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName); if (pluginPackage != null) { return pluginPackage; } DexClassLoader dexClassLoader = createDexClassLoader(dexPath); AssetManager assetManager = createAssetManager(dexPath); Resources resources = createResources(assetManager); // create pluginPackage pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo); mPackagesHolder.put(packageInfo.packageName, pluginPackage); return pluginPackage; } private DexClassLoader createDexClassLoader(String dexPath) { File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE); dexOutputPath = dexOutputDir.getAbsolutePath(); DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader()); return loader; } private AssetManager createAssetManager(String dexPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null; } } private Resources createResources(AssetManager assetManager) { Resources superRes = mContext.getResources(); Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); return resources; } 複製程式碼
然後點選外掛的時候要啟動外掛的 Activity,
PluginItem item = mPluginItems.get(position); DLPluginManager pluginManager = DLPluginManager.getInstance(this); pluginManager.startPluginActivity(this, new DLIntent(item.packageInfo.packageName, item.launcherActivityName)); 複製程式碼
這裡會把要啟動的包名和啟動類名包裝到 DLIntent 中。DLIntent 是 Intent 的子類。啟動外掛的進一步的邏輯在 DLPluginManager 的 startPluginActivity()
方法中。按照上文的描述,這裡主要做了以下四件事情:
- 判斷要啟動的 Activity 是否是外掛 Activity:因為要啟動的類也可能不是外掛類,所以我們需要分成兩種情況來進行處理,普通的 Activity 直接呼叫
Context.startActivity()
外掛 Activity 需要呼叫代理 Activity 來執行。 - 判斷包名,獲取外掛相關資訊:這裡就算是一個安全的校驗吧,主要是從之前解析的 APK 資訊中進行校驗。
- 使用外掛的 DexClassLoader 載入啟動類:先要使用類載入器載入外掛的 Activity 到記憶體中,外掛 Activity 的資訊會作為 Intent 的引數一起傳遞給代理 Activity。
- 使用
DLIntent.setClass()
啟動代理類:要啟動的代理類可能是 DLProxyFragmentActivity 和 DLProxyActivity,所以這裡我們先使用getProxyActivityClass()
得到代理類。該方法中使用了 Class 的isAssignableFrom()
方法來判斷某個例項是否是指定型別的。比如DLBasePluginActivity.class.isAssignableFrom(clazz)
表示 clazz 是否是 DLBasePluginActivity 型別的。
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) { // 1.判斷要啟動的 Activity 是否是外掛 Activity if (mFrom == DLConstants.FROM_INTERNAL) { dlIntent.setClassName(context, dlIntent.getPluginClass()); performStartActivityForResult(context, dlIntent, requestCode); return DLPluginManager.START_RESULT_SUCCESS; } // 2.判斷包名,獲取外掛相關資訊 String packageName = dlIntent.getPluginPackage(); if (TextUtils.isEmpty(packageName)) { throw new NullPointerException("disallow null packageName."); } DLPluginPackage pluginPackage = mPackagesHolder.get(packageName); if (pluginPackage == null) { return START_RESULT_NO_PKG; } // 3.使用外掛的 DexClassLoader 載入啟動類 final String className = getPluginActivityFullPath(dlIntent, pluginPackage); Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className); if (clazz == null) { return START_RESULT_NO_CLASS; } Class<? extends Activity> activityClass = getProxyActivityClass(clazz); if (activityClass == null) { return START_RESULT_TYPE_ERROR; } // 4.使用 DLIntent.setClass() 啟動代理類,並傳入外掛類和包資訊 dlIntent.putExtra(DLConstants.EXTRA_CLASS, className); dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName); dlIntent.setClass(mContext, activityClass); performStartActivityForResult(context, dlIntent, requestCode); return START_RESULT_SUCCESS; } private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) { Class<? extends Activity> activityClass = null; if (DLBasePluginActivity.class.isAssignableFrom(clazz)) { activityClass = DLProxyActivity.class; } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) { activityClass = DLProxyFragmentActivity.class; } return activityClass; } private void performStartActivityForResult(Context context, DLIntent dlIntent, int requestCode) { if (context instanceof Activity) { ((Activity) context).startActivityForResult(dlIntent, requestCode); } else { context.startActivity(dlIntent); } } 複製程式碼
這裡需要注意下,我們的外掛 Activity 是需要繼承 DLBasePluginActivity 或者 DLProxyFragmentActivity。這兩個類中重寫了 Activity 的許多生命週期方法。在代理 Activity 啟動之後,代理 Activity 會被傳遞到前面兩個基類中。比如,當外掛類想要獲取 AssetsManager 的時候,會呼叫到這兩個基類的 getAssetsManager()
,然後基類通過代理類得到之前我們建立的 AssetsManager.
按照上述流程,代理類被正常啟動。啟動之後它會建立 DLProxyImpl 例項,並在 onCreate()
方法中呼叫 DLProxyImpl 的 onCreate()
方法:
public void onCreate(Intent intent) { intent.setExtrasClassLoader(DLConfigs.sPluginClassloader); mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE); mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS); mPluginManager = DLPluginManager.getInstance(mProxyActivity); mPluginPackage = mPluginManager.getPackage(mPackageName); mAssetManager = mPluginPackage.assetManager; mResources = mPluginPackage.resources; initializeActivityInfo(); handleActivityInfo(); launchTargetActivity(); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) protected void launchTargetActivity() { try { Class<?> localClass = getClassLoader().loadClass(mClass); Constructor<?> localConstructor = localClass.getConstructor(new Class[] {}); Object instance = localConstructor.newInstance(new Object[] {}); mPluginActivity = (DLPlugin) instance; ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager); mPluginActivity.attach(mProxyActivity, mPluginPackage); Bundle bundle = new Bundle(); bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL); mPluginActivity.onCreate(bundle); } catch (Exception e) { e.printStackTrace(); } } 複製程式碼
這裡的主要邏輯在上述兩個方法中。第一個方法中會根據包名從 DLPluginManager 中獲取包的類載入器,然後使用該載入其載入器載入外掛類,反射觸發其構造方法,獲取例項。然後呼叫代理 Activity 的 attach()
方法將該外掛類複製給代理類。然後當 AMS 回撥代理類的各個生命週期的時候,代理類呼叫外掛類的各個生命週期。(這裡會使用類載入器再次載入外掛類,其實這是沒必要的,我們可以直接使用 Intent 將外掛類的 Class 通過序列化的方式傳遞過來,然後直接觸發其構造方法即可,無需再次執行類載入邏輯。)
好了,以上就是 DynamicLoadApk 的原理,其實本質就是:外掛類作為一個普通的類被呼叫,它不歸 AMS 負責。當我們啟動外掛的時候,實際啟動的是代理類,當 AMS 回撥代理類的生命週期的時候,代理類再呼叫外掛類的各個生命週期方法。只是,對資源和類載入的部分需要注意下,因為我們需要進行自定義配置來把它們的路徑指向我們的外掛包。