VOOZH about

URL: https://read01.com/R8Q8xk.html

⇱ 微店 Android 插件化實踐 - 壹讀


Saturday, Apr 11, 2026

微店 Android 插件化實踐

2017/06/14 來源:CSDN博客

隨著微店業務的發展,App 不可避免地也遇到了 65535 的大坑。除此之外,業務模塊增多、代碼量增大所帶來的問題也逐漸顯現出來。模塊耦合度高、協作開發困難、編譯時間過長等問題嚴重影響了開發進程。在預研了多種方案以後,插件化似乎是解決這些問題比較好的一個方向。雖然業界已經有很多優秀的開源插件化框架,但預研後發現在使用上對我們會有一定的局限。要麼追求低侵入性而 Hook 大量系統底層代碼穩定性不敢保證,要麼有很高的侵入性不滿足微店定製化的需求。技術要很好地服務業務,我們想在穩定性和低侵入性上尋找一個平衡……

👁 Image
...
圖 1 微店插件化改造流程

微店從 2016 年 4 月份開始進行插件化改造,到年底基本完成(可見圖 1 路線)。現在一共有 29 個模塊以插件化的方式運行,其中既有如商品、訂單等的業務模塊,也有像 Network、Cache 等的基礎模塊,目前我們的插件化框架已經很好地支持了微店多 Feature 快速並行疊代開發。完成一個插件化框架的 Demo 並不是多難的事兒,然而要開發一款完善的插件化框架卻非易事, 本篇將我們插件化改造過程中所涉及到的一些技術點以及思考與大家分享一下。

插件化技術原理

插件化技術聽起來高深莫測,實際要解決的就是三個問題:

  1. 代碼加載;
  2. 資源加載;
  3. 組件的生命周期。
我們知道 Android 和 Java 一樣都是通過ClassLoader來完成類加載,對於動態加載在實現方式上有兩種機制可供選擇,分別為單ClassLoader機制和多
  • 單 ClassLoader 機制:類似於 Google MulDex 機制,運行時把所有的類合併在一塊,插件和宿主程序的類全部都通過宿主的 ClassLoader 加載,雖然代碼簡單,但是魯棒性很差;

  • 多 ClassLoader 機制:每個插件都有一個自己的 ClassLoader,類的隔離性會很好。另外多 ClassLoader 還有一個優點,為插件的熱部署提供了可能。如果插件需要升級,直接新建一個 ClassLoader 加載新的插件,然後替換掉原來的即可。

我們的框架在類加載時採用的是多ClassLoader機制,框架會創建兩種ClassLoader。第一種是BundleClassLoader,每個 Bundle 安裝時會分配一個,負責該 Bundle 的類加載;第二種是DispatchClassLoader,它本身並不負責真正類的加載,只是類加載的一個分發器,持有宿主及所有 Bundle 的ClassLoader。關係如圖 2 所示。
👁 Image
...
圖2 插件化框架ClassLoader關係如何 Hook 系統的ClassLoader?應用類通過來加載,PathClassLoader存在於LoadedApk中,那麼,如何才能替換LoadedApkPathClassLoader為我們的呢?大家首先想到的是反射,但可惜LoadedApk對象是@Hide的,要替換首先需要 Hook 拿到LoadedApk對象,然後再通過反射替換PathClassLoader。要反射兩次特別是LoadedApk對象的獲取我們認為風險很高,那還有沒有其他方案可以注入?我們知道 Java 類加載時基於雙親委派機制,加載應用類的PathClassLoader其 Parent 為BootClassLoader,能否在調用鏈上插入呢?
👁 Image
...
圖3ClassLoader從圖 3 大家可以看到,我們通過修改類的父子關係成功地把插入到類的加載鏈中。修改類的父子關係直接通過反射修改ClassLoaderparent欄位即可,雖然也是反射的私有屬性,但相對於HookLoadedApk這個私有對象的私有方法,風險要相對小很多。

類加載優化

不管是或,對於依賴 Bundle 類的查找都是通過遍歷來實現的。由於我們把 Network、Cache 等基礎組件也進行了插件化,所以 Bundle 依賴會比較多,這個遍歷過程會有一定的性能損耗。我們想加載類時能否根據 ClassName 快速定位到該類屬於哪一個 Bundle?最終,我們採用的方案是:在編譯階段會收集 Bundle 所包含的PackageName信息,在插件安裝階段構造一個PackageName與 Bundle 的對應表,這樣加載 Class 時,根據包名可快速定位該 Class 屬於哪一個 Bundle。當然,由於混淆的原因,不同插件的包名可能重複,對此,我們通過規範來進行保證。

資源加載

資源加載方案可選擇的餘地不多,都是用AssetManager@hide方法addAssetPath,直接構造插件 Apk 的AssetManagerResouce對象。需要注意的是,我們採用的是資源合併的方案,通過addAssetsPath方法添加資源時,需要同時添加插件程序的資源文件和宿主程序的資源,及其依賴的資源。這樣可以將Resource合併到一個Context中,解決資源訪問時需要切換上下文的問題。另外,若不進行資源合併,插件也無法引入宿主的資源。

資源 ID 衝突問題

由於我們在構造AssetManager時,會把宿主、插件及依賴插件的資源合併在一起,那麼宿主資源 ID 與插件資源 ID,或插件資源 ID 之間都有可能重複。我們知道資源 ID 是在編譯時生成的,其生成的規則是0xPPTTNNNN,要解決衝突就需要對資源進行分段,資源分段常用的有兩種方式,分別為固定 PP 段與固定 TT 段。當時採用哪種資源分段方案對於我們來說是一個比較糾結的選擇,固定 PP 段需要修改 AAPT,代價比較大,固定 TT 段相對來說則較為簡單。初始我們採用的是固定 TT 段,但後來隨著插件的增多,TT 段明顯不夠用,後來還是採用修改 AAPT 固定 PP 段。大家要上插件化,如果可預見後續插件比較多,建議直接採用固定 PP 段方案。

除了 ID 衝突以外,資源名也有可能重複,對於資源名重複的問題我們通過規範來約束,所有的插件都分配有固定的資源前綴。

如何 Hook 資源加載過程

Android 通過Resources對象完成資源加載,要 Hook 資源加載過程,首先想到的是能否替換系統的Resources對象為我們自定義的Resources對象。調研發現要替換Resouce對象,至少要替換兩個系統對象LoadedApkContextImplmResources屬性,並且LoadedApkContextImpl都是私有對象,基於兼容性的考慮我們放棄了這種方案,而採用直接複寫ActivityApplication的獲取資源的相關方法來完成 Bundle 資源的加載。由於該方案對ApplicationActivity都有侵入,所以會帶來一定的接入成本。為此,我們在編譯過程中用代碼注入的方式完成資源加載的 Hook,資源的加載操作對插件開發來說是完全透明的。

註:資源 Hook 涉及到複寫的方法有如下幾個:

Override public Resources getResources { } Override public AssetManager getAssets { } Override public Resources.Theme getTheme { } @Overridepublic Object getSystemService(String name) { if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) { // 自定義 LayoutInflater } returnsuper.getSystemService(name); }

組件生命周期

對於 Android 來說,並不是類加載進來就可以使用了,很多組件都是有生命的。因此對於這些有血有肉的類,必須給它們注入生命力,也就是所謂的組件生命周期管理。很多插件化框架,比如 DroidPlugin 通過大量 Hook AMS、PMS 等來實現組件的生命周期,從而實現無侵入性。但技術肯定是服務於業務,四大組件真的都需要做插件化嗎?在無侵入性和兼容性上該如何抉擇?對於這個問題我們給出的答案是穩定壓倒一切。綜合當前的業務形態,我們插件化框架定位只實現ActivityBroadCastReceiver插件化,犧牲部分功能以求穩定性可控。插件化只是把靜態廣播轉為動態廣播,下面重點分解一下Activity插件化。

Activity 插件化

Activity 插件化實現大致有以下兩種方式:

  • 一種是靜態代理,寫一個 PluginActivity 繼承自 Activity 基類,把 Activity 基類里涉及生命周期的方法全都重寫一遍;
  • 另一種方式是動態替換,宿主中預註冊樁 StubActivity,通過在系統不同層次 Hook,從而實現 StubActivityRealActivity 之間的轉換,以達到偷梁換柱的目的。

由於第一種方案對插件開發侵入性太大,我們採用的是第二種方案。既然如此,我們就需要對圖 4 中①和②兩個點進行 Hook。

👁 Image
...
圖4 Hook 點選取
  • 對於①Hook:業內一般的做法是 HookActivityThread 類有成員變 mInstrumentation,它會負責創建 Activity 等操作,可以通過篡改 mInstrumentation 為我們自己的 InstrumentationHook,在其 execStartActivity 方法中完成 RealActivity->StubActivity 的轉化。

  • 對於②Hook:不同的框架選擇在系統不同的層次上進行 Hook,來完成 StubActivity->RealActivity 的還原。

👁 Image
...
圖5 現有插件化框架 Hook 策略從圖 5 可以看出第二種方案不管在哪一點上的 Hook 都會涉及到系統私有對象的操作,從而引入不可控風險。而我們的原則是儘量少地 Hook,若是以犧牲低侵入性為代價,有沒有一種更安全的方案呢?並且由於只對Activity進行插件化,所有啟動Activity的地方都是通過ContextstartActivity方法調起,我們只要複寫ApplicationActivitystartActivity方法,在startActivity方法調用時完成RealActivity->StubActivity,在類加載時實現StubActivity->RealActivity就可以了。同樣,複寫方法所引入的侵入性完全可以在編譯期通過代碼注入的方式解決掉。註:實際上,雖然startActivity有很多重寫方法,但我們只需複寫以下兩個就可以了:@OverridepublicvoidstartActivityForResult(Intent intent, int requestCode) { } @Overridepublicvoid(Intent intent, int requestCode, Bundle options) { }另外,對於ActivityLanchMode,我們是通過在宿主中每種LaunchMode都預註冊了多個(8 個)StubActivity來實現。值得注意的一點是,如果插件Activity為透明主題,由於系統限制不能動態設置透明主題,所以對於每種LaunchMode類型我們都增加了一個默認是透明主題的StubActivity為了儘可能地保證穩定性,我們插件Activity支持兩種運行模式,一種是預註冊模式,一種是免註冊模式。對於靜態插件(隨 App 打包)我們默認運行在預註冊模式下,對於動態插件(伺服器下發)才運行在免註冊模式下。值得說明的是,靜態插件與宿主AndroidManifest合併是在編譯期自動完成的。

插件間依賴

我們拆分插件時,首先明確的是每個插件的業務邊界,有了邊界才有所謂的內聚性,才能區分外部使用者和內部實現者。基於這樣拆分,我們可以看出每個插件既可以依賴於其他插件,也可能被其他插件依賴。為了簡化業務插件與基礎插件之間的依賴關係,我們規定基礎插件不能依賴業務插件,業務插件可以依賴基礎插件,業務插件與業務插件之間、基礎插件與基礎插件之間可以互相依賴。總結來看,插件之間的依賴主要有兩種形式:

  1. 頁面跳轉(比如商品 Bundle 跳轉到店鋪 Bundle 某一頁面):Android 可以用 Intent 解耦頁面跳轉,但考慮到多端統一,我們採用的是類似於總線機制,所有跳轉都通過 Page Bus 處理,每個頁面都對應一個別名,跳轉時根據別名來進行。

  2. 功能調用(商品 Bundle 用到店鋪 Bundle 信息):我們把每個插件抽象為一個服務提供者,插件對外暴露的服務稱之為本地 Service,它以 Interface 的形式定義,服務提供者保證版本之間的兼容。本地 Service 在插件的 AndroidManifest 中聲明,插件安裝時向框架註冊本地 Service,其他插件使用時直接根據服務別名查詢服務。我們會把本地 Service 的查詢過程直接綁定到 Context 的 getSystemService 方法上,整個使用過程就和調用 Android 系統服務一樣。此外,除了服務以外,插件還有可能對外暴露一些 Class,為了增加內聚性,我們通過@annotation 的方式聲明對外暴露的 Class,在編譯階段 Export 供其他插件依賴,未被註解的類就算是 public,對其他插件也是不可見的。

插件的依賴關係定義在每個插件的AndroidManifest舉個例子,下面是模塊在中的聲明:其中,versionName為聲明的依賴插件的最小版本號,插件安裝階段會校驗依賴條件是否滿足,若不滿足會進行相應處理(Debug 模式拋RuntimException,Release 模式輸出 error log 並上報監控後台)。動態部署及 HotPatch

插件化以後,動態部署和 HotPatch 也是需要說明的兩個點:

ActivityBroadcastReceiver的免註冊,若插件沒有新增其他類型(Service、Provider)的組件,則該插件支持動態部署。由於我們採用多ClassLoader機制,理論上是支持熱更新的,但考慮到插件有對外導出 Class,為了減少風險,我們對於動態插件生效時間延遲到應用切換至後台以後,當用戶切換到後台時直接 Kill 進程。

註:

  1. 插件更新支持增量更新;
  2. 對於插件更新檢查有兩個觸發時機:一個是進程初始化時(Pull),另一個是主動 Push 觸發(Push)。

HotPatch

插件化後,App 分為宿主和插件,宿主為源碼依賴,插件為二進位依賴。對於宿主和插件,我們採用不同的 HotPatch 方案:

  • 插件——因為插件支持動態部署,若插件需要補丁,我們直接升級插件即可。況且插件支持增
    是升級,補丁包的大小也可以得到有效控制;
  • 宿主——宿主不支持動態部署,只能走傳統的 HotPatch 方案,經過多種方案的對比,我們采
    用的是類似於 Tinker 方案,具體原因大家可以參考《微信熱補丁演進之路》。

但我們並不是直接使用的 Tinker,而是在實現思路上與 Tinker 一致,採用全 Dex 替換的方式來規避其他方案的問題。由於我們不僅業務組件實現了插件化,而且大部分基礎組件(Network、Cache 等)也實現了插件化,所以宿主並不是很大(<2.5M),況且宿主里的代碼都比較穩定。

微信的 Tinker 方案在補丁包的大小上的確有很大的優勢,我們敬佩其技術探究的精神,但對於其穩定性持有懷疑態度,基於宿主包可控的前提下,我們選擇犧牲流量來保證穩定性。

代碼管理

我們定位每個插件都是可以獨立疊代 App,插件化以後,整個的工程組織方式為如圖 6 的形式。

👁 Image
...
圖6 微店工程組織方式在此之中每個工程都對應一個 Git 庫,主庫包含多個子庫,對於這種工程結構,我們很自然地想到用SubModule來管理微店工程。然而事與願違,使用一段SubModule
  • 開發某個插件時,對於其他插件應該都是二進位依賴,不再需要其他插件的源碼,但 SubModule 會把所有子工程的源碼都 Checkout 出來。考慮到 Gradle 的生命周期,這樣嚴重影響了編譯速度;另外,主工程包含所有子工程的源碼也增加誤操作的風險(全量編譯、引用本地包而非 Release 包);

  • 代碼提交複雜且經常出現衝突:我們知道每次 Git 提交都會產生一個 Sha 值,主工程管理所有子工程的 Sha 值,每次子工程變動,除了提交子工程以外,還需要同步更新主工程的 Sha 值。這樣每次子工程的變動都涉及到兩次 Commit,更嚴重的是,如果兩個人同時改動同一個子工程,但忘記了同步提交主工程的 Sha 值,則會產生衝突,而且這種情況下無法更新、無法回滾、無法合併,崩潰……

針對使用Submodule過程中遇到的問題,我們引入了 Repo 來管理工程代碼。Repo 不像Submodule那樣,通過建立一種主從關係,用主 Module 管理子 Module。在 Repo 里,所有 Module 都是平級關係,每個 Module 的版本管理完全獨立於任何其他 Module,不會像 Submodule 那樣,提交了子 Module 代碼,也會對主 Module 造成影響。

另外,我們在使用過程中,還發現了另外一些好處:

  • 剝離了主 Module 和子 Module 的關係,檢出、同步、提交等操作都比 Sumodule 要快好多倍;
  • 模塊管理配置由一個陌生的 .gitmodules 變成了所有人都更熟悉的 XML 文件,便於配置管理。
開發調試

插件化以前,我們對所有模塊都是源碼依賴。插件化以後,運行某一模塊時,僅對宿主及當前模塊是源碼依賴,對於其他模塊全部是二進位依賴。集成方式的改變就涉及到如下兩個問題:

  • 打包時如何集成插件包?
  • 如何進行斷點調試?

插件包集成

我們插件的二進位包是so包,其實這些so都是正常的 Apk 結構,改為so放入 lib 目錄只是為了安裝時借用系統的能力從 Apk 中解壓出來,方便後續安裝。我們目前所有的庫都是基於 Maven 來管理,插件既然是so包,正好借用 Maven 管理能力同時,基於開源的 Gradle 插件android-native-dependencies實現了插件的集成。

斷點調試

開發插件時,對於其他插件的二進位包都是依賴的已發布版,所有已發布的插件都是混淆包。若開發過程中涉及到其他插件的斷點調試,則會出現無法對應源碼。

對於這種情況,我們制定了一個策略,在 Debug 模式下,會優先使用本地編譯的包。若要調試其他插件,可以把插件源碼檢出來編譯本地包(得益於 Repo 檢出過程非常方便),打包過程若檢索到有本地包,會替換掉從 Maven 遠程倉庫下載的包,當然,這個替換過程是通過編譯腳本自動完成的。

總結

雖然 Android 插件化在國內發展有幾年,各種方案百花齊放,但真的要在業務快速疊代的過程中完成插件化改造工作,其中酸爽也只有親歷者才能體會到。近年來隨著 React Native、Weex 及微信小程序的興起,很多以前需要插件化才能解決的問題,現在或許有了更好的解決方向。但,技術服務於業務,穩定壓倒一切,與大家共勉。

作者: 彭昌虎,先後在華為、騰訊從事Android開發工作,2011年加入微店,負責口袋購物、微店等多款產品的架構設計,2016年主導微店App完成插件化改造工作。
責編: 唐小引(@唐門教主),歡迎技術投稿、約稿、給文章糾錯,請發送郵件至tangxy@csdn.net
版權聲明: 本文為 CSDN 原創文章,未經允許,請勿轉載。

您可能感興趣
免責聲明:本文內容來源于CSDN博客,文章觀點不代表壹讀立場,如若侵犯到您的權益,或涉不實謠言,敬請向我們提出檢舉
最新文章 / 服務條款 / 私隱保護 / DMCA / 聯絡我們

壹讀/READ01.COM