Shadow對插件包管理的設計

在Shadow開源的代碼中,首先分爲core層和dynamic層。core層就完成了插件框架的所有功能,dynamic層又將插件框架動態化起來了。而後core層自己主要也分爲兩部分,一部分是loader相關的,一部分是manager相關的。其中loader就是解決插件框架核心功能的,好比將插件apk加載起來,將其中沒有安裝的Activity啓動起來。而manager的功能就是管理插件包。這篇文章咱們就梳理一下manager在管理插件包相關的設計。Manager對於啓動插件的過程管理,在另外的文章中再談。android

InstalledApk

咱們先假設沒有Manager實現,單純用core.loader,看看啓動插件時須要提供什麼參數。保證這個場景可用是咱們設計SDK時的一個原則。所以有一個test-none-dynamic-host的半成品sample,目前只是驗證這個場景是能夠編譯成功、啓動成功的。將來還須要增強這個測試用例。git

加載插件的入口方法是com.tencent.shadow.core.loader.ShadowPluginLoader#loadPlugin,這個方法接收的參數類型是com.tencent.shadow.core.common.InstalledApk。因此InstalledApk就是交給Loader的插件全部描述了。github

InstalledApk是咱們設計的始終不變的結構體,它處於打包在宿主的common模塊。dynamic層加載loader實現、runtime實現時採用的也是這個結構體。咱們能夠看到InstalledApk只有4個變量:apkFilePathoDexPathlibraryPathparcelExtras。其中前3個是DexClassLoader加載apk所需的參數,肯定了這3個參數就能夠加載apk到DexClassLoader中了。第4個參數parcelExtras則是一個擴展字段,咱們將另一個Parcelable序列化後存儲到這個變量中,達到動態擴展參數的目的。這個另外的Parcelable就是com.tencent.shadow.core.load_parameters.LoadParameters。這個LoadParameters能夠任意修改,只須要manager和loader同時更新就能夠了。算法

須要注意的是InstalledApk這個類的名字代表了這是一個已經安裝了的插件。「免安裝」的插件是免於安裝到系統,但仍是須要安裝到咱們的插件框架管理器(Manager)中的。Manager對於插件的安裝,也是仿照系統安裝正常app來設計的。主要工做就是將插件apk複製到Manager管理的特別目錄中,就像系統安裝apk是也將apk複製到data目錄中同樣。而後也像系統同樣,將apk中的so解壓複製出來。因此,構造InstalledApk傳給loadPlugin以前,Manager要將插件apk置於宿主的data目錄中,再將插件apk中所需ABI的so也解壓到宿主的data目錄中。必須將插件apk放在宿主的data目錄中是由於咱們的DexClassLoader只有權限加載宿主data目錄中的文件。數據庫

LoadParameters

LoadParameters是插件的加載參數結構體。這是一個能夠由動態實現的Manager和動態實現的Loader同時修改的類。這個結構體和宿主中的代碼無關。目前LoadParameters中有4個參數:businessNamepartKeydependsOnhostWhiteListjson

businessName是業務名。Shadow容許一個Loader同時加載多個插件。只要這些插件沒有so庫衝突,這些插件能夠是徹底不相關的業務的。Shadow經過ClassLoader設計保證了多插件間Java類是隔離的,可是無法爲Native的so庫劃出單獨的內存空間。除了代碼有可能衝突,對data目錄的使用也是有可能衝突的。由於Shadow的原理是「插件是宿主代碼的一部分」,因此全部插件均可以訪問宿主的data目錄。若是插件和宿主或者多插件之間有使用相同的data目錄邏輯,好比MultiDex會向一個固定的SharePreference持久化數據,就會出現持久化數據的衝突。爲了解決這個問題,引入的businessName。當businessName爲空時,Shadow就認爲這個插件跟宿主是同一個業務,這個插件直接使用宿主的data目錄。當businessName設置了值時,Shadow就會在宿主的data目錄中以businessName爲參數新建一個子目錄做爲插件的data根目錄使用。這樣相同businessName的插件就會使用同一個data目錄了,不一樣businessName的插件的data目錄就至關因而隔離的了。c#

partKey是插件apk的別名。由於插件apk的文件名是有可能由於帶了版本號或者什麼參數而變化,因此有這樣一個partKey做爲一個插件apk的不變的別名。partKey能夠用於在表示一個插件依賴另一個插件時使用。Shadow內部在實現區分多插件邏輯時也會用partKey做爲該apk的Key。在Loader等接口上的partKey參數指的也是這個partKeyapp

dependsOn聲明的是當前插件依賴哪些其餘插件。指定依賴的插件,要填寫插件的partKey。假設插件A依賴插件B,Shadow會將插件B的ClassLoader做爲插件A的parent。這樣插件A就能夠訪問插件B的類了。具備dependsOn聲明的插件,它的ClassLoader是標準的雙親委派邏輯,不具有直接加載白名單中聲明的宿主類的能力。代碼體如今com.tencent.shadow.core.loader.blocs.LoadApkBloc#loadPlugin中構造PluginClassLoader時傳入的specialClassLoadernull。所以,插件A若是依賴了插件B,在插件A的hostWhiteList中聲明的宿主類是無效的。要將hostWhiteList聲明在插件B中才有效。這個問題有人反饋過:github.com/Tencent/Sha… ,值得優化。插件A依賴插件B,還應該能使插件A中的資源依賴插件B中的資源,這應該表如今構造插件A的Resource對象時,將插件B做爲插件A的android.content.pm.ApplicationInfo#sharedLibraryFiles。關於跨插件依賴資源,代碼尚未上傳,須要整理一下。請關注com.tencent.shadow.core.loader.blocs.CreateResourceBloc#create方法的實現變化。框架

hostWhiteList就是爲了容許插件訪問宿主的類而設計的參數。hostWhiteList中設置的是Java類的包名。沒有設置dependsOn的插件會將宿主的ClassLoader做爲parent,可是插件的ClassLoader不是正常的雙親委派邏輯。插件ClassLoader同時還將宿主的ClassLoader的parent做爲名爲specialClassLoader的變量持有。插件的ClassLoader加載類的主路徑是先嚐試本身加載,本身加載不到,再用specialClassLoader加載。當要加載的類處於hostWhiteList中則採用正常的雙親委派,用parent(也就是宿主的ClassLoader)加載。這樣設計的目的就是爲了讓插件和宿主類隔離,又能夠容許插件複用宿主的部分類。ide

關於so文件

處於apk中的so不能直接運行,要先解壓到data目錄中才能運行。這不是插件框架的限制,正常安裝的App也是同樣的過程。正常安裝一個App,系統會在安裝時根據當前手機的ABI自動肯定一個合適的ABI,而後從apk中解壓出指定ABI的so到data目錄。因此插件框架在安裝插件apk時也須要決定採用哪一個ABI。Shadow目前的設計,沒有自動化這一過程,須要在繼承的PluginManager子類上Override com.tencent.shadow.core.manager.BasePluginManager#getAbi方法,返回須要解壓的ABI。

插件的ABI不能像正常app同樣任意決定,由於如今大部分手機都是64位的了,而Android系統不容許在一個進程中混用64位的so和32位的so。因此在64位仍是32位這個選擇上,插件要和宿主保持一致。一個特殊的狀況,若是宿主沒有任何so,安裝在64位手機上時,系統會認爲這是一個64位應用。進而致使插件不能加載32位的so。這個問題,除了讓宿主先加載一個32位的so以外,我尚未找到合適的解決方法。

解壓so的方法是com.tencent.shadow.core.manager.BasePluginManager#extractSo。能夠在實現中看到so目錄的肯定是根據UUID肯定的,跟partKey無關。這是由於咱們沒有技術手段能支持在同一個進程中將so隔離開。因此在同一個進程中加載的多個插件的so,相互之間沒有隔離,都至關因而宿主加載的so。所以,插件A依賴插件B中的so,不須要特別聲明。插件A同插件B有衝突so,又須要在同一個進程中工做,也須要so的設計方自行解決。

config.json的設計

config.json是一個能夠插件包的描述。Manager經過com.tencent.shadow.core.manager.installplugin.PluginConfig#parseFromJson方法將json轉換爲com.tencent.shadow.core.manager.installplugin.PluginConfig對象,而後再經過com.tencent.shadow.core.manager.installplugin.InstalledDao#insert方法將插件包描述的全部信息寫入到數據庫中持久化存儲。在這個過程當中,將apk文件的相對路徑轉換成絕對路徑。

config.json中有兩部份內容,一部分是插件包的版本信息,另外一部分是插件apk描述。

跟版本信息相關的字段有:versioncompact_versionUUIDUUID_NickName

version表示的是該config.json文件採用的格式版本;compact_version表示的是當前config.json文件跟哪些格式的舊版本是兼容的,能夠被支持舊版本格式的Manager使用。

UUID表示的是插件包內容的版本,只有相同UUID的apk才能一同工做。apk有3中類型:Loader、Runtime、Plugin。因此在同一個config.json中描述的的全部apk都具備相同的UUID,因此能一同工做。UUID是要按照通常UUID生成算法生成的,以保證屢次發佈的插件版本不會重複。UUID_NickName則是通常業務使用的版本名,對插件更新邏輯沒有實際做用。

須要注意的是,多個config.json是能夠採用同一個UUID的。由於咱們有時須要分段下載插件包,下載一部分先啓動一部分。因此能夠將插件apk分到多個config.json中,採用相同的UUID。而且,Loader和Runtime只須要存在於其中一個插件包中就能夠了。

Loader、Runtime和Plugin的描述都有兩個基本信息:apkNamehash。這個設計是爲了便於將來實現即便UUID不一樣的config.json中也可能存在相同hash,沒有發生變化的插件。那麼根據hash相同,則能夠決定跨UUID複用本地已經存在的插件了。

對於Plugin來講,還存在partKeybusinessNamehostWhiteList,做用如以前所說的,能夠在這裏設置這些參數。

插件包生成Gradle插件

爲了方便直接生成插件包,無需手工填寫config.json,咱們實現了一個生成插件包的Gradle插件。也就是Sample中看到的shadow {packagePlugin {}}DSL。這樣能夠經過Gradle腳本動態填寫config.json的一些參數。

執行packageDebugPlugin任務時會自動生成UUID。若是要複用以前UUID,能夠在插件包生成的build目錄放一個uuid.txt文件,將UUID指定在裏面。這部分代碼見com.tencent.shadow.core.gradle.extensions.PackagePluginExtension#toJson。須要注意的是,若是在源碼依賴的sample中這樣固定了UUID,會致使更新代碼的sample-plugin不能正常更新安裝,由於它的UUID 老是不變的。

生成的插件包zip的文件名,能夠經過PluginSuffix環境變量添加後綴。代碼見com.tencent.shadow.core.gradle.CreatePackagePluginTaskKt#createPackagePluginTask

關於這個Gradle插件,目前的實現主要仍是知足咱們業務的基本需求。看起來設計上是不夠通用,也不是很健壯的。有幾個簡單的單元測試在projects/sdk/core/gradle-plugin/src/test中。歡迎你們貢獻代碼。

爲何咱們的插件包是個zip包?

其實仔細分析一下前面所講的全部設計,會發現咱們不該該將config.json和全部apk一塊兒打包在一個zip包中。這樣作不到跨UUID複用apk。這是由於咱們在開發Shadow時,新舊插件框架同時運行,並且沒有人力修改插件的發佈系統。插件的發佈系統一直是發佈zip包的。因此Shadow在前面全部涉及的基礎上,封裝了一層從zip安裝插件包的實現。這一實現將來修改時應該不會影響底層設計。所以咱們將來也會修改這一設計。

相關文章
相關標籤/搜索