標籤:
1. 功能介紹1.1 簡介
DynamicLoadApk 是一個開源的 Android 外掛程式化架構。
外掛程式化的優點包括:(1) 模組解耦,(2) 動態升級,(3) 高效並行開發(編譯速度更快) (4) 按需載入,記憶體佔用更低等等。
DynamicLoadApk 提供了 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 主要分為四大模組:
(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. 流程圖
上面是調用外掛程式 Activity 的流程圖,其他組件調用流程類似。
(1) 首先通過 DLPluginManager 的 loadApk 函數載入外掛程式,這步每個外掛程式只需調用一次。
(2) 通過 DLPluginManager 的 startPluginActivity 函數啟動代理 Activity。
(3) 代理 Activity 啟動過程中構建、啟動外掛程式 Activity。
4. 詳細設計4.1 類別關係圖
以上是 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 庫。
注意:在啟動外掛程式的組件前,必須先調用上面兩個函數之一載入外掛程式,並且只能在宿主中調用。
流程圖如下:
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 基礎。
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 架構越來越多,也能解決外掛程式化解決的自動升級這部分功能,硬體、網路也在改善,未來何如?
(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,流程圖如下:
(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 函數中被調用,流程圖如下:
其中第一步設定 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 裡使用。
都說程式員的工資高,卻很少瞭解他們加班的痛苦,你是不是每次也在心裡想,按時間折算下來這個工資都給少了,於是會想在心裡呐喊,要麼漲工資,要麼漲工資,要麼漲工資,為什嗎??因為不讓我們加班,這是不可能的!!!
想要顛覆自己的工作模式嗎?想要減少自己的加班時間嗎?加入我們,和我們一起探尋屬於我們程式員的自由模式吧!
一款針對程式員的原生APP,以共用知識技能為目的,以懸賞方式線上互動互動平台。
我們擁有高達近20人頂尖的技術團隊,以及優秀的產品及運營團隊。團隊領軍人物均在行業內有10年以上的豐富經驗。
現在我們正在招募原始的參與英雄,您將同我們一起改變程式員的工作方式,改變程式員的世界!同時也會有豐厚的報酬。作為我們的原始的參與者,您將同我們一起體驗這款程式員神器,您可以提出專業的建議,我們會虛心採納。每一個人都會是英雄,而您就會是我們需要的英雄!同時您也可以邀請您的朋友一起參與這場英雄的招募互動。
我們不會耽誤你太多時間,我們只需要您的專業看法,只要您從一個月內抽出1個小時,以後您每天都可以節省兩個小時,一切都是為了我們自己!
來?還是不來?
接頭人暗號:1955246408 (QQ)
Android 開源項目源碼解析之DynamicLoadApk 源碼解析