Android 外掛化框架 DynamicLoadApk 原始碼解析

codekk發表於2015-07-14

1. 功能介紹

1.1 簡介

DynamicLoadApk 是一個開源的 Android 外掛化框架。

外掛化的優點包括:(1) 模組解耦,(2) 動態升級,(3) 高效並行開發(編譯速度更快) (4) 按需載入,記憶體佔用更低等等。

DynamicLoadApk 提供了 3 種開發方式,讓開發者在無需理解其工作原理的情況下快速的整合外掛化功能。

  1. 宿主程式與外掛完全獨立
  2. 宿主程式開放部分介面供外掛與之通訊
  3. 宿主程式耦合外掛的部分業務邏輯

三種開發模式都可以在 demo 中看到。

1.2 核心概念

(1) 宿主:主 App,可以載入外掛,也稱 Host。
(2) 外掛:外掛 App,被宿主載入的 App,也稱 Plugin,可以是跟普通 App 一樣的 Apk 檔案。

(3) 元件:指 Android 中的Activity、Service、BroadcastReceiver、ContentProvider,目前 DL 支援Activity、Service以及動態的BroadcastReceiver。

(4) 外掛元件:外掛中的元件。

(5) 代理元件:在宿主的 Manifest 中註冊,啟動外掛元件時首先被啟動的元件。目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。

(6) Base 元件:外掛元件的基類,目前包括 DLBasePluginActivity(外掛 Activity 的基類)、DLBasePluginFragmentActivity(外掛 FragmentActivity 的基類)、DLBasePluginService(外掛 Service 的基類)。

DynamicLoadApk 原理的核心思想可以總結為兩個字:代理。通過在 Manifest 中註冊代理元件,當啟動外掛元件時首先啟動一個代理元件,然後通過這個代理元件來構建、啟動外掛元件。

2. 總體設計

DynamicLoadApk 原始碼解析

上面是 DynamicLoadApk 的總體設計圖,DynamicLoadApk 主要分為四大模組:

(1) DLPluginManager

外掛管理模組,負責外掛的載入、管理以及啟動外掛元件。

(2) Proxy

代理元件模組,目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。

(3) Proxy Impl

代理元件公用邏輯模組,與(2)中的 Proxy 不同的是,這部分並不是一個元件,而是負責構建、載入外掛元件的管理器。這些 Proxy Impl 通過反射得到外掛元件,然後將外掛與 Proxy 元件建立關聯,最後呼叫外掛元件的 onCreate 函式進行啟動。

(4) Base Plugin

外掛元件的基類模組,目前包括 DLBasePluginActivity(外掛 Activity 的基類)、DLBasePluginFragmentActivity(外掛 FragmentActivity 的基類)、DLBasePluginService(外掛 Service 的基類)。

3. 流程圖

DynamicLoadApk 原始碼解析

上面是呼叫外掛 Activity 的流程圖,其他元件呼叫流程類似。

(1) 首先通過 DLPluginManager 的 loadApk 函式載入外掛,這步每個外掛只需呼叫一次。

(2) 通過 DLPluginManager 的 startPluginActivity 函式啟動代理 Activity。

(3) 代理 Activity 啟動過程中構建、啟動外掛 Activity。

4. 詳細設計

4.1 類關係圖

DynamicLoadApk 原始碼解析

以上是 DynamicLoadApk 主要類的關係圖,跟總體設計中介紹的一樣大致分為三部分。

(1) 對於 Proxy 部分,每個元件都存在 DLAttachable 介面,方便統一該元件不同類,如 Activity、FragmentActivity。每個元件的公共實現部分都統一放到了對應的 DLProxyImpl 中。

(2) 對於 Base Plugin 部分,每個元件都存在 DLPlugin 介面,同樣是方便統一該元件不同類。

4.2 類功能介紹

4.2.1 DLPluginManager.java

DynamicLoadApk 框架的核心類,主要功能包括:

(1) 外掛的載入和管理;

(2) 啟動外掛的元件,目前包括 Activity、Service。

主要屬性:

mNativeLibDir為外掛 Native Library 拷貝到宿主中後的存放目錄路徑。

mPackagesHolderHashMap,key 為包名,value 為表示外掛資訊的DLPluginPackage,儲存已經載入過的外掛資訊。

主要函式:

(1) getInstance(Context context)
獲取 DLPluginManager 物件的單例。
在私有建構函式中將mNativeLibDir變數賦值為宿主 App 應用程式資料目錄下名為pluginlib子目錄的全路徑。

(2) loadApk(String dexPath)
載入外掛。引數 dexPath 為外掛的檔案路徑。
這個函式直接呼叫 loadApk(final String dexPath, boolean hasSoLib)。

(3) loadApk(final String dexPath, boolean hasSoLib)
載入外掛 Apk。引數 dexPath 為外掛的檔案路徑,hasSoLib 表示外掛是否含有 so 庫。

注意:在啟動外掛的元件前,必須先呼叫上面兩個函式之一載入外掛,並且只能在宿主中呼叫。

流程圖如下:

DynamicLoadApk 原始碼解析

loadApk 函式呼叫 preparePluginEnv 函式載入外掛,圖中虛線框為 preparePluginEnv 的流程圖。

(4) preparePluginEnv(PackageInfo packageInfo, String dexPath)
載入外掛及其資源。流程圖如上圖。
呼叫createDexClassLoader(…)、createAssetManager(…)、createResources(…)函式完成相應初始化部分。

(5) createDexClassLoader(String dexPath)
利用DexClassLoader載入外掛,DexClassLoader 初始化函式如下:

public DexClassLoader (String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

其中dexPath為外掛的路徑。
optimizedDirectory優化後的dex存放路徑。這裡將路徑設定為當前 App 應用程式資料目錄下名為dex的子目錄中。
libraryPath為 Native Library 存放的路徑。這裡將路徑設定為mNativeLibDir屬性,其在getInstance(Context)函式中已經初始化。
parent父 ClassLoader,ClassLoader 採用雙親委託模式查詢類,具體載入方式可見 ClassLoader 基礎

(6) createAssetManager(String dexPath)
建立 AssetManager,載入外掛資源。
在 Android 中,資源是通過 R.java 中的 id 來呼叫訪問的。但是實現外掛化之後,宿主是無法通過 R 檔案訪問外掛的資源,所以這裡使用反射來生成屬於外掛的AssetManager,並利用addAssetPath函式載入外掛資源。

    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;
        }

    }

AssetManager 的無參建構函式以及addAssetPath函式都被hide了,通過反射呼叫。

(7) createResources(AssetManager assetManager)
利用AssetManager中已經載入的資源建立Resources,代理元件中會從這個Resources中讀取資源。
關於AssetManager、Resources深入的資訊可參考:Android 應用程式資源的查詢過程分析

(8) copySoLib(String dexPath)
呼叫SoLibManager拷貝 so 庫到 Native Library 目錄。

(9) startPluginActivity(Context context, DLIntent dlIntent)
啟動外掛 Activity,會直接呼叫startPluginActivityForResult(…)函式。
外掛自己內部 Activity 啟動依然是呼叫Context#startActivity(…)方法。

(10) startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode)
啟動外掛 Activity,流程圖如下:

DynamicLoadApk 原始碼解析

(11) startPluginService(final Context context, final DLIntent dlIntent)
啟動外掛 Service。
主要邏輯在函式fetchProxyServiceClass(…)中,流程與startPluginActivity(…)類似,只是換成了回撥的方式,在各種條件成立後呼叫原生方式啟動代理 Service,不再贅述。

(12) bindPluginService(…) unBindPluginService(…)
bind 或是 unBind 外掛 Service。邏輯與startPluginService(…)類似,不再贅述。

4.2.2 DLPluginPackage

外掛資訊對應的實體類,主要屬性如下:

    public String packageName;

    public String defaultActivity;

    public DexClassLoader classLoader;

    public AssetManager assetManager;

    public Resources resources;

    public PackageInfo packageInfo;

packageName為外掛的包名;
defaultActivity為外掛的 Launcher Main Activity;
classLoader為載入外掛的 ClassLoader;
assetManager為載入外掛資源的 AssetManager;
resources利用assetManager中已經載入的資源建立的Resources,代理元件中會從這個Resources中讀取資源。
packageInfo被PackageManager解析後的外掛資訊。
這些資訊都會在DLPluginManager#loadApk(…)時初始化。

4.2.3 DLAttachable.java/DLServiceAttachable.java

DLServiceAttachable 與 DLAttachable 類似,下面先分析 DLAttachable.java。

DLAttachable 是一個介面,主要作用是以統一所有不同型別的代理 Activity,如DLProxyActivity、DLProxyFragmentActivity,方便作為同一介面統一處理。
DLProxyActivity和DLProxyFragmentActivity都實現了這個類。

DLAttachable 目前只有一個

attach(DLPlugin pluginActivity, DLPluginManager pluginManager)

抽象函式,表示將外掛Activity和代理Activity繫結在一起,其中的pluginActivity引數就是指外掛Activity。

同樣 DLServiceAttachable 類似,作用是統一所有不同型別的代理 Service,實現外掛Service和代理Service的繫結。雖然目前只有DLProxyService。

4.2.4 DLPlugin.java/DLServicePlugin.java

DLPlugin 與 DLServicePlugin 類似,下面先分析 DLPlugin.java。

DLPlugin 是一個介面,包含Activity生命週期、觸控、選單等抽象函式。
DLBase*Activity 都實現了這個類,這樣外掛的 Activity 間接實現了此類。
主要作用是統一所有不同型別的外掛 Activity,如Activity、FragmentActivity,方便作為同一介面統一處理,所以這個類叫DLPluginActivity更合適。

同樣 DLServicePlugin 主要作用是統一所有不同型別的外掛 Service,方便作為統一介面統一處理,目前包含Service生命週期等抽象函式。

4.2.5 DLProxyActivity.java/DLProxyFragmentActivity.java

代理 Activity,他們是在宿主 Manifest 中註冊的元件,也是啟動外掛 Activity 時,真正被啟動的 Activity,他們的內部會完成外掛 Activity 的初始化和啟動。

這兩個類大同小異,所以這裡只分析DLProxyActivity。

首先來看下它的成員變數。

(1). DLPlugin mRemoteActivity
表示真正需要啟動的外掛Activity。這個屬性名應該叫做pluginActivity更合適。

上面我們已經介紹了,DLPlugin是所有外掛Activity都間接實現了的介面。

接下來在代理Activity的生命週期、觸控、選單等函式中我們都會同時呼叫 mRemoteActivity 的相關函式,模擬外掛Activity的相關功能。

(2). DLProxyImpl impl

主要封裝了外掛Activity的公用邏輯,如初始化外掛 Activity 並和代理 Activity 繫結、獲取資源等。

4.2.6 DLProxyImpl.java/DLServiceProxyImpl.java

DLProxyImpl 與 DLServiceProxyImpl 類似,下面先分析 DLProxyImpl.java。

DLProxyImpl 主要封裝了外掛Activity的公用邏輯,如初始化外掛 Activity 並和代理 Activity 繫結、獲取資源等,相當於把DLProxyActivity和DLProxyFragmentActivity的公共實現部分提出出來,核心邏輯位於下面介紹的 onCreate() 函式。

主要函式:

(1) DLProxyImpl(Activity activity)
建構函式,引數為代理 Activity。

(2) public void onCreate(Intent intent)
onCreate 函式,會在代理 Activity onCreate 函式中被呼叫,流程圖如下:

DynamicLoadApk 原始碼解析

其中第一步設定 intent 的 ClassLoader是用於 unparcel Parcelable 資料的,可見介紹: android.os.BadParcelableException

(3) protected void launchTargetActivity()
載入待啟動外掛 Activity 完成初始化流程,並通過DLPlugin和DLAttachable介面的 attach 函式實現和代理 Activity 的雙向繫結。流程圖見上圖虛線框部分。

(4) private void initializeActivityInfo()
獲得待啟動外掛的 ActivityInfo。

(5) private void handleActivityInfo()
設定代理 Activity 的主題等資訊。

其他的 get* 函式都是獲取一些外掛相關資訊,會被代理 Activity 呼叫。

同樣 DLServiceProxyImpl 主要封裝了外掛Service的公用邏輯,如初始化外掛 Service 並和代理 Activity 繫結。

4.2.7 DLBasePluginActivity.java/DLBasePluginFragmentActivity.java

外掛 Activity 基類,外掛中的Activity都要繼承 DLBasePluginActivity/DLBasePluginFragmentActivity 之一(目前尚不支援 ActionBarActivity)。

主要作用是根據是否被代理,確定一些函式直接走父類邏輯還是代理 Activity 或是空邏輯。

DLBasePluginActivity繼承自Activity,同時實現了DLPlugin介面。這兩個類大同小異,所以這裡只分析DLProxyActivity。
主要變數:

    protected Activity mProxyActivity;

    protected Activity that;

    protected DLPluginManager mPluginManager;

    protected DLPluginPackage mPluginPackage;

mProxyActivity為代理 Activity,通過attach(…)函式繫結。
that與mProxyActivity等同,只是為了和this指標區分,表示真實的Context,這裡真實指的是被代理情況下為代理 Activity,未被代理情況下等同於 this。

4.2.8 DLBasePluginService.java

外掛 Service 基類,外掛中的 Service 要繼承這個基類,主要作用是根據是否被代理,確定一些函式直接走父類邏輯還是代理 Service 或是空邏輯。

主要變數含義與DLBasePluginActivity類似,不重複介紹。
PS:截止目前這個類還是不完善的,至少和DLBasePluginActivity對比,還不支援非代理的情況

4.2.9 DLIntent.java

繼承自 Intent,封裝了待啟動元件的 PackageName 和 ClassName。

4.2.10 SoLibManager.java

呼叫SoLibManager拷貝 so 庫到 Native Library 目錄。

主要函式:

(1) copyPluginSoLib(Context context, String dexPath, String nativeLibDir)
函式中以ZipFile形式載入外掛,迴圈讀取其中的檔案,如果為.so結尾檔案、符合當前平臺 CPU 型別且尚未拷貝過最新版,則新建Runnable拷貝 so 檔案。

4.2.11 DLUtils.java

這個類中大都是無用或是不該放在這裡的函式,也許是大版本升級及維護人過多後對工具函式的維護不夠所致。

5. 雜談

5.1 外掛不能打包 dl-lib.jar

原因是外掛和宿主屬於不同的 ClassLoader,如果同時打包 dl-lib.jar,會因為 ClassLoader 隔離導致型別轉換錯誤,具體可見:ClassLoader 隔離

Eclipse 打包解決方式見專案主頁;
Android Studio 打包解決方式見 5.2;
Ant 打包需要修改 build.xml 中 dex target 引用到的 compileclasspath 屬性。

5.2 在 Android Studio 下使用 DynamicLoadApk

在使用 DynamicLoadApk 時有個地方要注意,就是外掛 Apk 在打包的時候不能把 dl-lib.jar 檔案打包進去,不然會報錯(java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation)。換句話說,dl-lib.jar 要參與編譯,但不參與打包。該框架作者已經給出了 Eclipse 下的解決方案。我這裡再說下怎麼在 Android Studio 裡使用。

dependencies {
        provided fileTree(dir: 'dl-lib', include: ['*.jar'])
    }

5.3 DynamicLoadApk 待完善的問題

(1) 還未支援廣播;
(2) Base Plugin 中的 that 還未去掉,需要覆寫 Activity 的相關方法;
(3) 外掛和宿主資源 id 可能重複的問題沒有解決,需要修改 aapt 中資源 id 的生成規則;
(4) 不支援自定義主題,不支援系統透明主題;
(5) 外掛中的 so 處理有異常;
(6) 不支援靜態 Receiver;
(7) 不支援 Provider;
(8) 外掛不能直接用 this;

5.4 其他外掛化方案

除了 DynamicLoadApk 用代理的方式實現外,目前還有兩種外掛化方案:
(1) 用 Fragment 以及 schema 的方式實現。
(2) 利用位元組碼庫動態生成一個外掛類 A 繼承自待啟動外掛 Activity,啟動外掛 A。這個外掛 A 名稱固定且已經在 Manifest 中註冊。
具體可見:Android 外掛化

最後 H5 框架越來越多,也能解決外掛化解決的自動升級這部分功能,硬體、網路也在改善,未來何如?

相關文章