本文爲 Android 開源項目源碼解析 中 DynamicLoadApk 部分
項目地址:DynamicLoadApk,分析的版本:144571b,Demo 地址:DynamicLoadApk Demo
分析者:FFish,分析狀態:完成,校對者:Trinea,校對狀態:初審完成java
DynamicLoadApk 是一個開源的 Android 插件化框架。android
插件化的優勢包括:(1) 模塊解耦,(2) 動態升級,(3) 高效並行開發(編譯速度更快) (4) 按需加載,內存佔用更低等等。git
DynamicLoadApk 提供了 3 種開發方式,讓開發者在無需理解其工做原理的狀況下快速的集成插件化功能。github
三種開發模式均可以在 demo 中看到。網絡
(1) 宿主:主 App,能夠加載插件,也稱 Host。
(2) 插件:插件 App,被宿主加載的 App,也稱 Plugin,能夠是跟普通 App 同樣的 Apk 文件。框架
(3) 組件:指 Android 中的Activity
、Service
、BroadcastReceiver
、ContentProvider
,目前 DL 支持Activity
、Service
以及動態的BroadcastReceiver
。ide
(4) 插件組件:插件中的組件。函數
(5) 代理組件:在宿主的 Manifest 中註冊,啓動插件組件時首先被啓動的組件。目前包括 DLProxyActivity(代理 Activity)、DLProxyFragmentActivity(代理 FragmentActivity)、DLProxyService(代理 Service)。工具
(6) Base 組件:插件組件的基類,目前包括 DLBasePluginActivity(插件 Activity 的基類)、DLBasePluginFragmentActivity(插件 FragmentActivity 的基類)、DLBasePluginService(插件 Service 的基類)。fetch
DynamicLoadApk 原理的核心思想能夠總結爲兩個字:代理。經過在 Manifest 中註冊代理組件,當啓動插件組件時首先啓動一個代理組件,而後經過這個代理組件來構建、啓動插件組件。
上面是 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 的基類)。
上面是調用插件 Activity 的流程圖,其餘組件調用流程相似。
(1) 首先經過 DLPluginManager 的 loadApk 函數加載插件,這步每一個插件只需調用一次。
(2) 經過 DLPluginManager 的 startPluginActivity 函數啓動代理 Activity。
(3) 代理 Activity 啓動過程當中構建、啓動插件 Activity。
以上是 DynamicLoadApk 主要類的關係圖,跟整體設計中介紹的同樣大體分爲三部分。
(1) 對於 Proxy 部分,每一個組件都存在 DLAttachable 接口,方便統一該組件不一樣類,如 Activity、FragmentActivity。每一個組件的公共實現部分都統一放到了對應的 DLProxyImpl 中。
(2) 對於 Base Plugin 部分,每一個組件都存在 DLPlugin 接口,一樣是方便統一該組件不一樣類。
DynamicLoadApk 框架的核心類,主要功能包括:
(1) 插件的加載和管理;
(2) 啓動插件的組件,目前包括 Activity、Service。
主要屬性:
mNativeLibDir
爲插件 Native Library 拷貝到宿主中後的存放目錄路徑。 mPackagesHolder
HashMap,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 初始化函數以下:
javapublic 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
函數加載插件資源。
javaprivate 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(…)
相似,再也不贅述。
插件信息對應的實體類,主要屬性以下:
javapublic 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(…)
時初始化。
DLServiceAttachable 與 DLAttachable 相似,下面先分析 DLAttachable.java。
DLAttachable 是一個接口,主要做用是以統一全部不一樣類型的代理 Activity,如DLProxyActivity
、DLProxyFragmentActivity
,方便做爲同一接口統一處理。DLProxyActivity
和DLProxyFragmentActivity
都實現了這個類。
DLAttachable 目前只有一個
javaattach(DLPlugin pluginActivity, DLPluginManager pluginManager)
抽象函數,表示將插件Activity
和代理Activity
綁定在一塊兒,其中的pluginActivity
參數就是指插件Activity
。
一樣 DLServiceAttachable 相似,做用是統一全部不一樣類型的代理 Service,實現插件Service
和代理Service
的綁定。雖然目前只有DLProxyService
。
DLPlugin 與 DLServicePlugin 相似,下面先分析 DLPlugin.java。
DLPlugin 是一個接口,包含Activity
生命週期、觸摸、菜單等抽象函數。
DLBase*Activity 都實現了這個類,這樣插件的 Activity 間接實現了此類。
主要做用是統一全部不一樣類型的插件 Activity,如Activity
、FragmentActivity
,方便做爲同一接口統一處理,因此這個類叫DLPluginActivity
更合適。
一樣 DLServicePlugin 主要做用是統一全部不一樣類型的插件 Service,方便做爲統一接口統一處理,目前包含Service
生命週期等抽象函數。
代理 Activity,他們是在宿主 Manifest 中註冊的組件,也是啓動插件 Activity 時,真正被啓動的 Activity,他們的內部會完成插件 Activity 的初始化和啓動。
這兩個類大同小異,因此這裏只分析DLProxyActivity
。
首先來看下它的成員變量。
(1). DLPlugin mRemoteActivity
表示真正須要啓動的插件Activity
。這個屬性名應該叫作pluginActivity
更合適。
上面咱們已經介紹了,DLPlugin
是全部插件Activity
都間接實現了的接口。
接下來在代理Activity
的生命週期、觸摸、菜單等函數中咱們都會同時調用 mRemoteActivity 的相關函數,模擬插件Activity
的相關功能。
(2). DLProxyImpl impl
主要封裝了插件Activity
的公用邏輯,如初始化插件 Activity 並和代理 Activity 綁定、獲取資源等。
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 綁定。
插件 Activity 基類,插件中的Activity
都要繼承 DLBasePluginActivity/DLBasePluginFragmentActivity 之一(目前尚不支持 ActionBarActivity)。
主要做用是根據是否被代理,肯定一些函數直接走父類邏輯仍是代理 Activity 或是空邏輯。
DLBasePluginActivity
繼承自Activity
,同時實現了DLPlugin
接口。這兩個類大同小異,因此這裏只分析DLProxyActivity
。
主要變量:
javaprotected Activity mProxyActivity; protected Activity that; protected DLPluginManager mPluginManager; protected DLPluginPackage mPluginPackage;
mProxyActivity
爲代理 Activity,經過attach(…)
函數綁定。that
與mProxyActivity
等同,只是爲了和this
指針區分,表示真實的Context
,這裏真實指的是被代理狀況下爲代理 Activity,未被代理狀況下等同於 this。
插件 Service 基類,插件中的 Service 要繼承這個基類,主要做用是根據是否被代理,肯定一些函數直接走父類邏輯仍是代理 Service 或是空邏輯。
主要變量含義與DLBasePluginActivity
相似,不重複介紹。
PS:截止目前這個類仍是不完善的,至少和DLBasePluginActivity
對比,還不支持非代理的狀況
繼承自 Intent,封裝了待啓動組件的 PackageName 和 ClassName。
調用SoLibManager
拷貝 so 庫到 Native Library 目錄。
主要函數:
(1) copyPluginSoLib(Context context, String dexPath, String nativeLibDir)
函數中以ZipFile
形式加載插件,循環讀取其中的文件,若是爲.so
結尾文件、符合當前平臺 CPU 類型且還沒有拷貝過最新版,則新建Runnable
拷貝 so 文件。
這個類中大都是無用或是不應放在這裏的函數,也許是大版本升級及維護人過多後對工具函數的維護不夠所致。
緣由是插件和宿主屬於不一樣的 ClassLoader,若是同時打包 dl-lib.jar,會由於 ClassLoader 隔離致使類型轉換錯誤,具體可見:ClassLoader 隔離
Eclipse 打包解決方式見項目主頁;
Android Studio 打包解決方式見 5.2;
Ant 打包須要修改 build.xml 中 dex target 引用到的 compileclasspath 屬性。
在使用 DynamicLoadApk 時有個地方要注意,就是插件 Apk 在打包的時候不能把 dl-lib.jar 文件打包進去,否則會報錯(java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation)。換句話說,dl-lib.jar 要參與編譯,但不參與打包。該框架做者已經給出了 Eclipse 下的解決方案。我這裏再說下怎麼在 Android Studio 裏使用。
groovydependencies { provided fileTree(dir: 'dl-lib', include: ['*.jar']) }
(1) 還未支持廣播;
(2) Base Plugin 中的 that 還未去掉,須要覆寫 Activity 的相關方法;
(3) 插件和宿主資源 id 可能重複的問題沒有解決,須要修改 aapt 中資源 id 的生成規則;
(4) 不支持自定義主題,不支持系統透明主題;
(5) 插件中的 so 處理有異常;
(6) 不支持靜態 Receiver;
(7) 不支持 Provider;
(8) 插件不能直接用 this;
除了 DynamicLoadApk 用代理的方式實現外,目前還有兩種插件化方案:
(1) 用 Fragment 以及 schema 的方式實現。
(2) 利用字節碼庫動態生成一個插件類 A 繼承自待啓動插件 Activity,啓動插件 A。這個插件 A 名稱固定且已經在 Manifest 中註冊。
具體可見:Android 插件化
最後 H5 框架愈來愈多,也能解決插件化解決的自動升級這部分功能,硬件、網絡也在改善,將來何如?