技术实践 | Android Flutter 多实例实践
導讀:Flutter CLI 工具支持將 Flutter Module 打包成 Android AAR 包以供外部依賴使用,即 Flutter AAR。在一個沒有使用 Flutter 技術棧的 Android 工程中集成 Flutter AAR 是沒有任何問題的,但如果目標工程本身已經使用了 Flutter 框架,在此基礎上再接入 Flutter AAR 就會失敗,我們稱之為 Flutter 多實例問題。本文主要介紹在 Android 平臺下 Flutter 多實例問題的一種解決方案。
文|李成達 網易云信資深移動端開發工程師
企業的業務往往是復雜多樣的,如果是 ToC 的業務,我們大多時候需要開發一個體驗良好的應用 APP;而如果是 ToB 的業務,我們往往需要提供一個易于接入和使用的 SDK。在 ToC 業務上,Flutter 框架提供的跨平臺、高效開發與高性能特性,使得移動端應用開發變得更加簡單且高效;那在 ToB 業務上,SDK 的開發是否能夠享受 Flutter 框架提供的這些紅利呢?這一點對于像我們網易云信這樣的服務、能力提供商而言尤為重要。網易云信是集網易 21 年 IM 以及音視頻技術打造的融合通信云服務專家,穩定易用的通信與視頻 PaaS 平臺,其服務大多以能力 SDK 的形式對外提供,如果能夠提高 SDK 的生產效率和研發效能,好處不言而喻。所以,上面的問題答案當然是肯定的!就像使用 Flutter 開發 APP 一樣,我們同樣可以使用 Flutter 進行 SDK 開發,從而在 Android / iOS 甚至更多平臺中共享一致的業務邏輯實現,減小人力、提高生產效率和研發效能。
在使用 Flutter 進行 SDK 開發時,產物的打包方式主要有以下兩種形式:
-
Flutter Package / Flutter Plugin:該打包方式需要以 Dart 源碼形式發布到 Pub.dev 或 GitHub,第三方開發者在接入時本質上是以源碼的形式依賴,同時接入方本地需要搭建并引入 Flutter 開發環境。此種方式有明顯的缺陷:首先,源碼發布會將 SDK 內部實現細節完全暴露在外( Flutter 框架并未提供類似 Proguard 的混淆工具),這對企業的非開源項目而言是不可接受的;其次,它變相要求接入方使用 Flutter 技術棧,這對于當前沒有在目標項目中使用 Flutter 開發的接入方而言,門檻較高不說,接入體驗也不太友好。
-
Android AAR:AAR 是 Android 應用官方的依賴形式,并不存在明顯的短板。通過 Flutter 框架提供的 CLI 工具,可以很方便地將 Flutter Module 打包成 AAR 發布出去,不用擔心泄漏業務源碼,也不損失接入體驗。因為打包工具會將 Flutter 層的業務代碼編譯成 AOT 共享庫,而平臺層的 Java 業務代碼則可以開啟混淆避免反編譯(為了簡便,后面統一使用 Flutter AAR 命名由 Flutter Module 打包而成的 Android AAR 包)。
綜上所言,對于企業的一個商業 SDK 項目來說,如果選擇使用 Flutter 技術棧進行開發,那么使用 Flutter AAR 形式來發布才是明智之舉。但其實這又會引入新的問題。在前文 Flutter 混合開發基礎中我們介紹了,一個 Flutter APP 的包結構,它包含有引擎庫 `libflutter.so`、業務庫 `libapp.so`、 以及`flutter_assets` 等部分。同理,一個 Flutter Module 打包出來的 AAR 也會包含類似的結構以及產物文件。那在一個 Flutter APP 中,應該以何種姿勢接入 Flutter AAR 呢?可以預見的是,它們之間必然存在沖突,文件沖突已經顯而易見,類、資源、甚至 Flutter Engine 也可能會沖突,這種常規的 Flutter AAR 包顯然是無法集成到 Flutter APP 工程中使用的。有問題就有答案,接下來,我們就一起來分析、探索該問題的解決方案。
Flutter APP 集成 Flutter AAR 問題分析
上面說到 Flutter APP 無法集成常規打包出來的 Flutter AAR,因為存在一系列的沖突,但具體會出現什么樣的錯誤,還是需要我們真正動手去集成才能知道。對這個環節感興趣的小伙伴可以親自動手嘗試,不再贅述,下面直接給出結論說明兩者共存存在哪些問題:
-
構建失敗,其實就是因為文件、類沖突導致編譯失敗。主要沖突有:
-
Flutter 版本依賴沖突:Flutter APP 宿主工程與 Flutter AAR 使用的 Flutter 版本不一致導致,包括 Flutter Embedding Jar 與 Flutter SO Jar,前者包含平臺層 Java 代碼,后者包含 libflutter.so 引擎庫文件。通過 Gradle 我們可以解決這個依賴的版本沖突,例如強制使用其中某個版本,但這樣做極有可能會出現運行時錯誤。
-
Flutter Plugin 平臺代碼 / 資源沖突:Flutter APP 和 Flutter AAR 引用了相同的 Plugin 但版本不一致導致。插件中會包含平臺層的代碼,版本不一致同樣可能會導致編譯失敗或者運行時錯誤。
-
GeneratedPluginRegistrant.java 文件沖突:該文件為 Flutter 工具生成的插件自動注冊類,用于 Flutter Engine 啟動時自動加載所需插件。Flutter APP 與 Flutter AAR 均有對應的類文件,負責加載各自依賴的插件,兩者缺一不可。
-
libapp.so 沖突:這是 Dart 代碼經過 AOT 生成的動態庫,Flutter APP 和 Flutter AAR 都會生成與其對應的 so 庫,我們不能單純的只使用它們其中之一,因為它們本身包含的 AOT 代碼是從不同的源碼編譯過來的。
-
運行時錯誤
-
同一個 Flutter Engine 不支持加載多個 AOT 庫:Flutter Engine 在初始化時會動態鏈接 libapp.so 這個 AOT 庫,解析其中的數據段,并執行代碼段中的機器指令。但在我們的場景中,運行時其實是包含有兩個 AOT 庫的,它們都需要加載到 Flutter Engine 中來,使用同一個Engine 是無法滿足需求的,因為在 Flutter 的實現中,一個 Engine 只能對應一個 AOT 庫。
-
圖片資源、字體庫無法正常顯示:此類資源會被打包至 flutter_assets 中,并且會生成對應的 Manifest 資源描述清單文件。但 Flutter APP 生成的資源清單文件會覆蓋 Flutter AAR 中的資源清單文件,這樣導致 Flutter Engine 在加載資源時,無法從清單文件中查詢到對應的資源,因此加載失敗。
以上就是我們在 Flutter APP 中接入 Flutter AAR 遇到的問題。針對這些問題,我們首先想到的是,Flutter Team 或者開源社區是不是已經有此類問題的解決方案了?但在經過調研后發現目前并沒有。Flutter 框架是支持多個 Engine 的,包括 Flutter 2.0 新支持的 Engine Group 僅支持加載和運行同一個 AOT 庫下的代碼,明顯不能滿足我們的需求。我們還給官方提了對應Issue(https://github.com/flutter/flutter/issues/64542) 進行討論,但是暫時還沒有得到滿意的解決方案,為此我們不得已走上了自己探索解決方案的自強之路。
解決方案探索
通過上面的分析,我們已經了解了接入過程中出現的具體錯誤以及出錯原因。在真正著手探索解決方案前,還應設立目標解決方案應該滿足的一些原則:
-
首先方案應該朝著最小引擎改動、甚至無改動的方向努力。因為 Flutter 框架一直在不斷迭代演進,如果我們修改了引擎這塊的邏輯,除非這些改動能通過 PR 進入主干分支,否則引擎一旦更新,我們的方案就得重新適配,后期維護工作大。
-
其次方案應該盡量不依賴宿主工程做額外的改造或支持。首先 Flutter APP 接入 Flutter AAR 就跟普通 Android APP 接入 Android AAR 一樣簡單,不應引入額外的插件或是 Gradle 腳本;其次 Flutter AAR 和 Flutter APP 的 Flutter 運行時環境應該盡量隔離。
明確目標之后,我們再來看看入手點在哪里。由于需要盡量避免引擎改動,那應該是自上而下,首先從應用層切入,看能否找到對策。這就需要我們深入源碼,從上到下了解 Flutter 框架的初始化、運行機制。這里不做單獨講解,在具體問題分析解決上再說明。現在我們再回過頭來看最初遇到的一系列問題,并嘗試運用所掌握的 Android 、Flutter 框架知識來解決。
Class 沖突解決監控?
Class 沖突是因為 Flutter AAR 與 Flutter APP 都有自己的 Plugins 依賴、以及可能會依賴不同版本的 Flutter Embedding Jar,這些依賴庫里都包含有平臺代碼,這會導致編譯期類重復而失敗。那如何解決這個問題呢?
最簡單也是最暴力的方法就是對 Flutter AAR 依賴的所有 Plugin 以及 Embedding Jar 源碼進行重命名(修改類名或者包名),雖然能解決問題,但工作量巨大、修改面廣、不靈活,一旦 Plugin 或 Flutter 版本更新都需要重新修改。
那有沒有更好的辦法呢?答案是自定義 ClassLoader。具體的,在構建 Flutter AAR 時,在源代碼編譯成 .class 階段完成之后,將所有的插件、Flutter Embedding Jar 對應的 .class 文件搜集起來,打包成一個 DEX 文件放入 Flutter AAR 的 assets 中。在運行時,需要將 assets 下的 DEX 文件拷貝到應用的 data 私有目錄下,再通過 DexClassLoader 去動態加載這個 DEX。這里需要注意的是 DEX 文件是版本號的概念的,它跟 Flutter AAR 的版本號是綁定的,意味著每次加載這個 DEX 時,我們首先需要檢查當前私有目錄下的文件版本是否與 Flutter AAR 版本一致,一致則直接加載即可,不一致需要刪除原 DEX 文件并重新拷貝后再加載。關鍵代碼如下:
針對 DEX 文件的加載一般而言我們只需要使用 DexClassLoader 這個系統類就行了,但這里我們需要繼承 BaseDexClassLoader,并重寫 findClass 方法。
默認類的加載基于雙親委派模型,一般都是先請求父加載器加載,如果父加載器加載失敗子加載器才有機會加載。但在這里,我們 findClass 的邏輯需要反其道而行之。Flutter AAR 需要加載的類應該優先使用子加載器從 DEX 文件中加載,加載失敗后才能通過父加載器加載。代碼如下:
庫文件沖突解決?
libflutter.so 是Flutter Engine 動態庫文件,在運行時會被 Flutter Embedder Jar 加載進來。這個庫文件沖突,我們不能單純使用宿主中同名的庫文件,因為兩者的 Engine 版本可能不一致以及不違背運行時 Flutter ?版本隔離的目標。
這里解決沖突最簡單的方法就是重命名。通過閱讀代碼,我們發現 Android 以 so 庫的路徑為 key 保存所有已經加載的動態庫,即便是完全相同的 so 庫,只要文件路徑不一致,就可以同時 load 進來。因此,這里通過重命名能解決文件沖突的問題,也不會影響到 so 的加載。
libapp.so 沖突也是類似的,我們同樣需要對 Flutter AAR 中的 libapp.so 重命名。此外,我們還需要特殊處理這兩個 so 的加載流程。因為 Flutter 運行時硬編碼了動態庫的名稱,如果不修改加載流程,在查考庫時就會找到 Flutter APP 生成的庫文件,而不是我們 Flutter AAR 的庫文件。
Flutter Engine 的初始化是在 FlutterLoader 這個類中,在這里會加載 libflutter.so 并配置一系列的參數初始化 Native Engine。我們需要做的就是替換 libflutter.so 的加載邏輯,轉而去加載重命名后的 Engine 庫文件。對于 libapp.so ,它并不是在 Java 層加載的,而是由 Native Engine 通過 dlopen 鏈接的。通過查閱 Engine 的代碼我們發現通過 --aot-shared-library-name 選項可以設置要加載的目標 libapp.so 路徑。關鍵代碼如下:
Flutter 資源沖突解決?
Flutter 相關資源是打包放到 assets 目錄下的,且通過對應的 Manifest 文件來聲明,分別是:FontManifest.json 與 AssetsManifest.json 文件。這兩個文件分別列出了 Flutter 依賴的所有字體資源與路徑映射關系、圖片資源與路徑映射關系。
Flutter-Engine 在運行時通過這兩個文件來解析圖片與字體資源,Flutter AAR 中雖然也包含了這兩個文件,但會被 Flutter APP 宿主中的同名文件覆蓋,導致字體或資源無法加載。所以,這里有兩個簡單方案:
-
支持編譯期合并對應的資源清單 json 文件;這需要開發 Plugin 插件供宿主使用,實現復雜而且接入不友好。
-
Flutter AAR 中抽離出一個獨立的資源包 Package 供 Flutter APP 依賴,資源包中僅包含 Flutter AAR 引用的所有圖片、字體資源(不包含任何業務邏輯,因此可以放心的發布到pub平臺),宿主在 Flutter 層依賴這個 Package,這樣宿主在構建時 Flutter 工具會合并所有的的資源,并生成完整的資源清單文件。
至此,我們解決了 Flutter AAR 與 Flutter APP 的共存問題。當然整個方案落地下來,其中還會碰到其他一些問題,比如:生成的 DEX 文件需要訪問宿主中的其他類的時候,在混淆啟用的情況下,應該如何保證 DEX 訪問主ClassLoader中的類、方法沒有問題;再如:Flutter AAR 的 DEX 中如果包含有 Android 組件怎么辦?Android 四大組件都是需要由應用的主ClassLoader進行加載的,如果主 DEX 中沒有包含這些類,那么肯定啟動失敗;等等諸如此類問題,這里不再一一列舉。
結語
下圖所示為 Flutter 多實例運行時的架構圖。類似于多 Flutter Engine,以上方案實現的多 Flutter 實例,也是通過創建多個 Native 的 AndroidShellHolder 來實現的。不同的是,在多 Engine 下不同的 ShellHolder 綁定相同的 libapp.so,而多實例下綁定的是不同的 libapp.so ,因此該方案能在運行時隔離 Flutter APP 與 Flutter AAR 的 Flutter 運行時環境。
該方案的主要優勢表現在:
-
無 Engine 定制,可維護性較高
-
Flutter APP 與 Flutter AAR 的 Flutter 版本、運行時環境相互獨立
有得必有失,相對地,在其他方面,該方案有所不足:
-
使用了獨立的 Flutter Engine 庫文件,因此會導致包體積增加
-
會加載兩個不同的 Flutter Engine ,內存會有所增加
綜上,在 SDK 開發中采用 Flutter 技術,同樣能夠發揮 Flutter 在 APP 開發中的優勢,前提是我們能夠解決好 Flutter 多實例的問題。本文主要講解了 Android Flutter 多實例的一種實現思路,希望能夠對大家有所幫助。
本文作者?
李成達,網易云信資深移動端開發工程師,熱衷于研究跨平臺開發技術以及工程提效,目前主要負責視頻會議組件化 SDK 的相關研發工作。
*各渠道文章轉載需注明來源及作者
總結
以上是生活随笔為你收集整理的技术实践 | Android Flutter 多实例实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于谨防诈骗的温馨提示
- 下一篇: 游戏社交崛起!四缺一,开黑吗?