码上用它开始Flutter混合开发——FlutterBoost
為什么需要混合方案
具有一定規(guī)模的App通常有一套成熟通用的基礎(chǔ)庫,尤其是阿里系A(chǔ)pp,一般需要依賴很多體系內(nèi)的基礎(chǔ)庫。那么使用Flutter重新從頭開發(fā)App的成本和風(fēng)險都較高。所以在Native App進(jìn)行漸進(jìn)式遷移是Flutter技術(shù)在現(xiàn)有Native App進(jìn)行應(yīng)用的穩(wěn)健型方式。閑魚在實(shí)踐中沉淀出一套自己的混合技術(shù)方案。在此過程中,我們跟Google Flutter團(tuán)隊(duì)進(jìn)行著密切的溝通,聽取了官方的一些建議,同時也針對我們業(yè)務(wù)具體情況進(jìn)行方案的選型以及具體的實(shí)現(xiàn)。
官方提出的混合方案
基本原理
Flutter技術(shù)鏈主要由C++實(shí)現(xiàn)的Flutter Engine和Dart實(shí)現(xiàn)的Framework組成(其配套的編譯和構(gòu)建工具我們這里不參與討論)。Flutter Engine負(fù)責(zé)線程管理,Dart VM狀態(tài)管理和Dart代碼加載等工作。而Dart代碼所實(shí)現(xiàn)的Framework則是業(yè)務(wù)接觸到的主要API,諸如Widget等概念就是在Dart層面Framework內(nèi)容。
一個進(jìn)程里面最多只會初始化一個Dart VM。然而一個進(jìn)程可以有多個Flutter Engine,多個Engine實(shí)例共享同一個Dart VM。
我們來看具體實(shí)現(xiàn),在iOS上面每初始化一個FlutterViewController就會有一個引擎隨之初始化,也就意味著會有新的線程(理論上線程可以復(fù)用)去跑Dart代碼。Android類似的Activity也會有類似的效果。如果你啟動多個引擎實(shí)例,注意此時Dart VM依然是共享的,只是不同Engine實(shí)例加載的代碼跑在各自獨(dú)立的Isolate。
官方建議
引擎深度共享
在混合方案方面,我們跟Google討論了可能的一些方案。Flutter官方給出的建議是從長期來看,我們應(yīng)該支持在同一個引擎支持多窗口繪制的能力,至少在邏輯上做到FlutterViewController是共享同一個引擎的資源的。換句話說,我們希望所有繪制窗口共享同一個主Isolate。
但官方給出的長期建議目前來說沒有很好的支持。
多引擎模式
我們在混合方案中解決的主要問題是如何去處理交替出現(xiàn)的Flutter和Native頁面。Google工程師給出了一個Keep It Simple的方案:對于連續(xù)的Flutter頁面(Widget)只需要在當(dāng)前FlutterViewController打開即可,對于間隔的Flutter頁面我們初始化新的引擎。
例如,我們進(jìn)行下面一組導(dǎo)航操作:
Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3我們只需要在Flutter Page1和Flutter Page3創(chuàng)建不同的Flutter實(shí)例即可。
這個方案的好處就是簡單易懂,邏輯清晰,但是也有潛在的問題。如果一個Native頁面一個Flutter
頁面一直交替進(jìn)行的話,Flutter Engine的數(shù)量會線性增加,而Flutter Engine本身是一個比較重的對象。
多引擎模式的問題
- 冗余的資源問題.多引擎模式下每個引擎之間的Isolate是相互獨(dú)立的。在邏輯上這并沒有什么壞處,但是引擎底層其實(shí)是維護(hù)了圖片緩存等比較消耗內(nèi)存的對象。想象一下,每個引擎都維護(hù)自己一份圖片緩存,內(nèi)存壓力將會非常大。
- 插件注冊的問題。插件依賴Messenger去傳遞消息,而目前Messenger是由FlutterViewController(Activity)去實(shí)現(xiàn)的。如果你有多個FlutterViewController,插件的注冊和通信將會變得混亂難以維護(hù),消息的傳遞的源頭和目標(biāo)也變得不可控。
- Flutter Widget和Native的頁面差異化問題。Flutter的頁面是Widget,Native的頁面是VC。邏輯上來說我們希望消除Flutter頁面與Naitve頁面的差異,否則在進(jìn)行頁面埋點(diǎn)和其它一些統(tǒng)一操作的時候都會遇到額外的復(fù)雜度。
- 增加頁面之間通信的復(fù)雜度。如果所有Dart代碼都運(yùn)行在同一個引擎實(shí)例,它們共享一個Isolate,可以用統(tǒng)一的編程框架進(jìn)行Widget之間的通信,多引擎實(shí)例也讓這件事情更加復(fù)雜。
因此,綜合多方面考慮,我們沒有采用多引擎混合方案。
現(xiàn)狀與思考
前面我們提到多引擎存在一些實(shí)際問題,所以閑魚目前采用的混合方案是共享同一個引擎的方案。這個方案基于這樣一個事實(shí):任何時候我們最多只能看到一個頁面,當(dāng)然有些特定的場景你可以看到多個ViewController,但是這些特殊場景我們這里不討論。
我們可以這樣簡單去理解這個方案:我們把共享的Flutter View當(dāng)成一個畫布,然后用一個Native的容器作為邏輯的頁面。每次在打開一個容器的時候我們通過通信機(jī)制通知Flutter View繪制成當(dāng)前的邏輯頁面,然后將Flutter View放到當(dāng)前容器里面。
老方案在Dart側(cè)維護(hù)了一個Navigator棧的結(jié)構(gòu)。棧數(shù)據(jù)結(jié)構(gòu)特點(diǎn)就是每次只能從棧頂去操作頁面,每一次在查找邏輯頁面的時候如果發(fā)現(xiàn)頁面不在棧頂那么需要往回Pop。這樣中途Pop掉的頁面狀態(tài)就丟失了。這個方案無法支持同時存在多個平級邏輯頁面的情況,因?yàn)槟阍陧撁媲袚Q的時候必須從棧頂去操作,無法再保持狀態(tài)的同時進(jìn)行平級切換。
舉個例子:有兩個頁面A,B,當(dāng)前B在棧頂。切換到A需要把B從棧頂Pop出去,此時B的狀態(tài)丟失,如果想切回B,我們只能重新打開B之前頁面的狀態(tài)無法維持住。這也是老方案最大的一個局限。
如在pop的過程當(dāng)中,可能會把Flutter 官方的Dialog進(jìn)行誤殺。這也是一個問題。
而且基于棧的操作我們依賴對Flutter框架的一個屬性修改,這讓這個方案具有了侵入性的特點(diǎn)。這也是我們需要解決的一個問題。
具體細(xì)節(jié),大家可以參考老方案開源項(xiàng)目地址:
https://github.com/alibaba-flutter/hybrid_stack_manager
新一代混合技術(shù)方案 FlutterBoost
重構(gòu)計(jì)劃
在閑魚推進(jìn)Flutter化過程當(dāng)中,更加復(fù)雜的頁面場景逐漸暴露了老方案的局限性和一些問題。所以我們啟動了代號FlutterBoost(向C++ Boost致敬)的新混合技術(shù)方案。這次新的混合方案我們的主要目標(biāo)有:
- 可復(fù)用通用型混合方案
- 支持更加復(fù)雜的混合模式。比如支持主頁Tab這種情況
- 無侵入性方案:不再依賴修改Flutter的方案
- 支持通用頁面生命周期
- 統(tǒng)一明確的設(shè)計(jì)概念
跟老方案類似,新的方案還是采用共享引擎的模式實(shí)現(xiàn)。主要思路是由Native容器Container通過消息驅(qū)動Flutter頁面容器Container,從而達(dá)到Native Container與Flutter Container的同步目的。我們希望做到Flutter渲染的內(nèi)容是由Naitve容器去驅(qū)動的。
簡單的理解,我們想做到把Flutter容器做成瀏覽器的感覺。填寫一個頁面地址,然后由容器去管理頁面的繪制。在Native側(cè)我們只需要關(guān)心如果初始化容器,然后設(shè)置容器對應(yīng)的頁面標(biāo)志即可。
主要概念
Native層概念
- Container:Native容器,平臺Controller,Activity,ViewController
- Container Manager:容器的管理者
- Adaptor:Flutter是適配層
- Messaging:基于Channel的消息通信
Dart層概念
- Container:Flutter用來容納Widget的容器,具體實(shí)現(xiàn)為Navigator的派生類-
- Container Manager:Flutter容器的管理,提供show,remove等Api
- Coordinator: 協(xié)調(diào)器,接受Messaging消息,負(fù)責(zé)調(diào)用Container Manager的狀態(tài)管理。
- Messaging:基于Channel的消息通信
關(guān)于頁面的理解
在Native和Flutter表示頁面的對象和概念是不一致的。在Native,我們對于頁面的概念一般是ViewController,Activity。而對于Flutter我們對于頁面的概念是Widget。我們希望可統(tǒng)一頁面的概念,或者說弱化抽象掉Flutter本身的Widget對應(yīng)的頁面概念。換句話說,當(dāng)一個Native的頁面容器存在的時候,FlutteBoost保證一定會有一個Widget作為容器的內(nèi)容。所以我們在理解和進(jìn)行路由操作的時候都應(yīng)該以Native的容器為準(zhǔn),Flutter Widget依賴于Native頁面容器的狀態(tài)。
那么在FlutterBoost的概念里說到頁面的時候,我們指的是Native容器和它所附屬的Widget。所有頁面路由操作,打開或者關(guān)閉頁面,實(shí)際上都是對Native頁面容器的直接操作。無論路由請求來自何方,最終都會轉(zhuǎn)發(fā)給Native去實(shí)現(xiàn)路由操作。這也是接入FlutterBoost的時候需要實(shí)現(xiàn)Platform協(xié)議的原因。
另一方面,我們無法控制業(yè)務(wù)代碼通過Flutter本身的Navigator去push新的Widget。對于業(yè)務(wù)不通過FlutterBoost而直接使用Navigator操作Widget的情況,包括Dialog這種非全屏Widget,我們建議是業(yè)務(wù)自己負(fù)責(zé)管理其狀態(tài)。這種類型Widget不屬于FlutterBoost所定義的頁面概念。
理解這里的頁面概念,對于理解和使用FlutterBoost至關(guān)重要。
與老方案主要差別
前面我們提到老方案在Dart層維護(hù)單個Navigator棧結(jié)構(gòu)用于Widget的切換。而新的方案則是在Dart側(cè)引入了Container的概念,不再用棧的結(jié)構(gòu)去維護(hù)現(xiàn)有的頁面,而是通過扁平化key-value映射的形式去維護(hù)當(dāng)前所有的頁面,每個頁面擁有一個唯一的id。這種結(jié)構(gòu)很自然的支持了頁面的查找和切換,不再受制于棧頂操作的問題,之前的一些由于pop導(dǎo)致的問題迎刃而解。同時也不再需要依賴修改Flutter源碼的形式去進(jìn)行實(shí)現(xiàn),除去了實(shí)現(xiàn)的侵入性。
那這是如何做到的呢?
多Navigator的實(shí)現(xiàn)
Flutter在底層提供了讓你自定義Navigator的接口,我們自己實(shí)現(xiàn)了一個管理多個Navigator的對象。當(dāng)前最多只會有一個可見的Flutter Navigator,這個Navigator所包含的頁面也就是我們當(dāng)前可見容器所對應(yīng)的頁面。
Native容器與Flutter容器(Navigator)是一一對應(yīng)的,生命周期也是同步的。當(dāng)一個Native容器被創(chuàng)建的時候,Flutter的一個容器也被創(chuàng)建,它們通過相同的id關(guān)聯(lián)起來。當(dāng)Native的容器被銷毀的時候,Flutter的容器也被銷毀。Flutter容器的狀態(tài)是跟隨Native容器,這也就是我們說的Native驅(qū)動。由Manager統(tǒng)一管理切換當(dāng)前在屏幕上展示的容器。
我們用一個簡單的例子描述一個新頁面創(chuàng)建的過程:
這就是一個新頁面創(chuàng)建的主要邏輯,銷毀和進(jìn)入后臺等操作也類似有Native容器事件去進(jìn)行驅(qū)動。
總結(jié)
目前FlutterBoost已經(jīng)在生產(chǎn)環(huán)境支撐著在閑魚客戶端中所有的基于Flutter開發(fā)業(yè)務(wù),為更加負(fù)復(fù)雜的混合場景提供了支持。同時也解決了一些歷史遺留問題。
我們在項(xiàng)目啟動之初就希望FlutterBoost能夠解決Native App混合模式接入Flutter這個通用問題。所以我們把它做成了一個可復(fù)用的Flutter插件,希望吸引更多感興趣的朋友參與到Flutter社區(qū)的建設(shè)。我們的方案可能不是最好的,這個方案距離完美還有很大的距離,我們希望通過多分享交流以推動Flutter技術(shù)社區(qū)的發(fā)展與建設(shè)。我們更希望看到社區(qū)能夠涌現(xiàn)出更加優(yōu)秀的組件和方案。
在有限篇幅中,我們分享了閑魚在Flutter混合技術(shù)方案中積累的經(jīng)驗(yàn)和代碼。歡迎興趣的同學(xué)能夠積極與我們一起交流學(xué)習(xí)。
擴(kuò)展補(bǔ)充
性能相關(guān)
在兩個Flutter頁面進(jìn)行切換的時候,因?yàn)槲覀冎挥幸粋€Flutter View所以需要對上一個頁面進(jìn)行截圖保存,如果Flutter頁面多截圖會占用大量內(nèi)存。這里我們采用文件內(nèi)存二級緩存策略,在內(nèi)存中最多只保存2-3個截圖,其余的寫入文件按需加載。這樣我們可以在保證用戶體驗(yàn)的同時在內(nèi)存方面也保持一個較為穩(wěn)定的水平。
頁面渲染性能方面,Flutter的AOT優(yōu)勢展露無遺。在頁面快速切換的時候,Flutter能夠很靈敏的相應(yīng)頁面的切換,在邏輯上創(chuàng)造出一種Flutter多個頁面的感覺。
Release 1.0支持
項(xiàng)目開始的時候我們基于閑魚目前使用的Flutter版本進(jìn)行開發(fā),而后進(jìn)行了Release 1.0兼容升級測試目前沒有發(fā)現(xiàn)問題。
接入
只要是集成了Flutter的項(xiàng)目都可以用官方依賴的方式非常方便的以插件形式引入FlutterBoost,只需要對工程進(jìn)行少量代碼接入即可完成接入。
詳細(xì)接入文檔,請參閱GitHub主頁官方項(xiàng)目文檔。
現(xiàn)已開源
目前,新一代混合棧已經(jīng)在閑魚全面應(yīng)用。我們非常樂意將沉淀的技術(shù)回饋給社區(qū)。歡迎大家一起貢獻(xiàn),一起交流,攜手共建Flutter社區(qū)。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的码上用它开始Flutter混合开发——FlutterBoost的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Paxos 幽灵复现问题的看法
- 下一篇: 深度学习文本分类在支付宝投诉文本模型上的