開發與維運

Flutter 在餓了麼的應用與沉澱

Flutter作為當前最火的跨平臺研發方案,它到底好在哪裡?餓了麼從2018年下半年開始接觸Flutter,並在多個App大量落地Flutter業務。餓了麼對Flutter的期待是保質提效,賦能業務。阿里巴巴新零售淘系技術AliFlutter系列第八場直播中邀請了蜂鳥大前端資深iOS工程師李永光為大家介紹餓了麼為了”保質提效,賦能業務”,選擇Flutter作為跨平臺研發方案的緣由,Flutter在餓了麼應用與落地情況,餓了麼在Flutter應用過程中的基礎建設和沉澱。相信能給大家帶來更多嘗試使用Flutter、以及把Flutter實際用於業務開發的信心和決心。

演講嘉賓簡介:李永光,花名雍光,蜂鳥大前端資深iOS工程師。4年深耕移動端,餓了麼最早的一批Flutter玩家,重點參與了Flutter在蜂鳥團隊的業務開發落地、工程架構演進。

以下內容根據演講視頻以及PPT整理而成。
觀看回放http://mudu.tv/watch/5817421

本次分享主要圍繞以下四個方面:

            一、背景介紹  
            二、Flutter 在餓了麼的應⽤  
            三、基礎建設與沉澱  
            四、展望與規劃  

一、為什麼選擇使⽤ Flutter

Flutter的三大特點及框架

打開Flutter官網,映入眼簾的是Flutter的三大特點。首先是快速開發,毫秒級的繪製,亞秒級的熱加載,豐富的Widget,可以快速開發出用戶界面。二是富有表現力的UI,豐富的動效接口,流暢的滑動,支持iOS和Android兩種風格的Widget,可以組合出非常漂亮且靈活的界面。三是可媲美Native的運行性能,AOT模式下,Flutter代碼可以被編譯成ARM機器碼。下圖是Flutter架構圖,最底層是Flutter應用的各個平臺,包括iOS、Android、Windows、MacOS、Ubuntu等。各個平臺分別實現Embedder平臺相關,如提供繪製的畫布,在Android和iOS上就是OpenGL Context, 還包含了線程設置和事件循環。再上一層是Flutter Engine層,使用了C++,包含DartVM,Skia 2D渲染等通用能力,還包含文字繪製等關鍵能力。最上一層是Flutter Framework層, 使用了Dart, 包含了Material和Cupertino兩種風格的Widget,以及一部分渲染邏輯,當然還包含框架與引擎之間的通信接口。Flutter的渲染基本與平臺無關,平臺只是提供了畫布,這為Flutter的UI一致性和運行一致性提供了堅實的基礎。

Flutter原理

下圖右側是Flutter UI渲染的三棵樹,分別包含描述節點,虛擬節點和真實節點。Widget樹在運行期會生成Element樹和RenderObject樹,Widget樹在更新時會觸發Element樹的比對和更新,以觸發RenderObject樹的最小更新。Flutter與RN等方案最大的不同就在於Flutter的RenderObject是自己實現的,而RN是使用Native控件。

屏幕快照 2020-06-22 下午5.05.14.png

客戶端研發方案對比

下圖給出了五種不同研發方案在四個維度上的對比,包括Native、Flutter、RN、Weex和H5。1)在性能方面,H5最差,Native最優。Flutter相比於RN,在AOT編譯模式下,由於沒有JS虛擬機,相對運行性能更高。而且,Flutter的渲染是直接對接底層的,RN還需要操作Native控件,有著非常高的通信成本。Flutter的頁面加載速度和流暢度都比RN更優,在測試中也是符合實際預期的。

在動態性方面,H5可以實時發佈,所以動態性最好。RN有JS虛擬機,可以一定程度上進行代碼的動態下方和執行。在AOT編譯下,Flutter的動態性可以認為是與Native差不多的,都非常弱。如果有此類需求,需要藉助其它方式,如DSL等。

跨端一致性可以分為UI一致性和運行一致性。H5藉助於瀏覽器規範,其UI一致性是非常好的,運行一致性也不錯。Native由於開發棧的不同,其RenderObject,API和實現都有很大的不同,因此跨端一致性非常差。Flutter實現了渲染層,在Android和iOS上RenderObject的實現是一致的。正是因為有了渲染層,平臺相關性非常低,所以可以像H5一樣做到一處編寫,處處運行。

開發體驗方面,H5開發預覽調試都非常方便,Flutter的各個插件使用起來也比較方便,HotReload使得開發,調試和運行都可以節省不少時間。

總結起來,Flutter在動態性上有所詬病以外,性能可以與Native媲美,極強的跨端一致性,還可以提供非常好的開發體驗。

屏幕快照 2020-06-22 下午5.08.11.png

餓了麼的選擇

餓了麼對研發方案的期望是用最少的人,搬最快的磚,用戶無感知!簡而言之是研發效率高,用戶體驗好。最後,餓了麼選擇了Flutter作為跨平臺研發方案。Flutter具備完備好用的工具,如Pub,IDE插件,HotReload,優秀的跨端一致性,富有表現力的UI,這些特點可以為開發調試和驗證工作節省很多時間,從而大大提升研發效率。跨端一致性,豐富的UI,以及高性能使得Flutter開發的頁面與Native開發的頁面體驗無差別,而且Android與iOS可以保持高度的一致性。另外,Flutter採用的自有渲染引擎在未來可擴展的餘地很多, Flutter是Google出品的,Flutter現在的社區也是非常火熱的,阿里巴巴,騰訊,頭條等企業的很多App都使用了Flutter,可以說,Flutter有著非常廣闊的應用與技術前景。在動態性方面,目前大家已經有了很多解決方案,如DSL等。這點上還是以自己的需求出發,稍作補足即可。

屏幕快照 2020-06-22 下午5.08.41.png

Flutter在餓了麼的應⽤

從2018年下半年到現在,餓了麼很多App上都已經使用了Flutter。其中至冠配送大概80%的頁面都使用了Flutter。

屏幕快照 2020-06-22 下午5.10.47.png

下圖展示了餓了麼已經上線的Flutter頁面,涵蓋了大部分常見的場景,包括殘疾騎士驗證頁面、單量提升頁面、優惠券頁面、運單簽收頁面等。他們的頁面加載速度和滑動流暢度都基本與Native頁面相當。

屏幕快照 2020-06-22 下午5.11.30.png

混合開發(舊)

Flutter代碼主要以Module的形式關聯到主工程,自然涉及到混合棧管理問題。Flutter頁面與Native頁面的切換主要有以下三種情況,Native切換Flutter、Flutter切換Flutter、Flutter切換Native。從2018年到2019年間,餓了麼也實現了自己的混合棧管理方案,Android和iOS的理念相似,這裡以iOS為例,關鍵在於Native側公用多個FlutterVC的FlutterView,解決FlutterVC不釋放問題。餓了麼在Flutter側做了一個LPDFRouter,記錄所有與Flutter頁面有關的URL。當從Native切換Flutter時,先打開Flutter Container VC,Flutter側的Navigator會push URL。當連續打開Flutter頁面時,Native容器不動,Flutter的Navigator直接push。當Flutter切換Native時,Native容器返回按鍵綁定Dart方法,當上一個頁面是Flutter時,使用Native直接pop即可,如果上一個頁面是Native頁面,除了把當前URL pop掉,還需要將容器pop掉。下圖的混合棧管理方案基本上滿足了餓了麼的需求,但是方案的一半在Native,另一半在Flutter中,不太適合Tab非常複雜的情況。
屏幕快照 2020-06-22 下午5.11.59.png

混合開發(新 - boost)

後來餓了麼使用了閒魚開發的Flutter boost作為混合棧管理方案,無論從Native還是Flutter打開一個頁面,都會先打開一個原生的容器,容器的生命週期ID會通過Channel傳到Flutter側,Flutter容器管理器會打開一個Flutter容器。Flutter容器管理器管理了多個Navigator,每個Navigator只有一個Flutter頁面,一個Widget。Flutter容器與Native容器ID是一致的。最新的Flutter boost使用了多個Flutter View的組合,不再複用單個Flutter View+截圖。可以發現Flutter boost對混合棧的管理是非常純粹的,都在Native側。在使用Flutter時,只需要關注Native容器的生命週期即可,適合多Tab的Flutter頁面情況。

屏幕快照 2020-06-22 下午5.12.21.png

研發/集成模式

Flutter工程與Native工程是如何組織的,下圖中的虛線框代表需要有Flutter環境。第一種是至冠模式Teemo,餓了麼最初就希望將至冠配送100% Flutter化。因此將Flutter工程與Native工程放在了同一個倉庫裡,Native工程通過ruby腳本關聯到Flutter工程。Native工程編譯時先編譯Flutter工程,將產物引進來,因此工程的開發、調試、測試、打包等都需要Flutter環境。下圖右側是蜂鳥模式,蜂鳥業務非常複雜,餓了麼考慮可能只有部分的同學會開發Flutter業務,因此將Flutter環境與其它業務環境隔離開來。餓了麼做了一個Runner工程,同步了蜂鳥主工程的很多依賴,如登錄、用戶管理、安全等。在Runner工程中把Flutter業務開發完之後,通過本地和遠端把Flutter產物抽取出來進行發佈,如App Framework、Flutter Framework、以及對應的原生代碼。發佈之後,蜂鳥工程會版本tag的形式依賴Flutter的原生產物,那麼只開發Native業務的同學不需要Flutter環境。不同的團隊有不要的研發和集成模式,並不存在最佳實踐,適合自己的才是最關鍵的。

屏幕快照 2020-06-22 下午5.12.48.png

質量&效率結果

從2018年到現在,餓了麼已經上線了很多Flutter頁面,頁面佔比在不同團隊都是不同的。Crash方面,穩定下來後在0.01%級別。在流暢度方面,使用Flutter工具或使用線上回調函數計算,都可以達到50幀以上。而最大的驚喜是在提高研發效率上,在過去一年多的時間裡節省了100多開發人日。前期不是很熟練,1個人可以頂1.5個人,後期便熟練後,1個人可以頂1.8個人。

基礎建設與沉澱

控件庫

隨著Flutter頁面的開發越來越多,餓了麼聯合UED做了基礎控件的規範和封裝,包括按鈕、TextFeild等。當基礎的控件創建完後,後續的Flutter頁面可以複用控件,進一步提高開發效率,同時使得不同頁面間的一致性更好。

屏幕快照 2020-06-22 下午5.13.27.png

基礎插件

Flutter是一個UI的開發框架,因此也不能免於使用Native能力,如定位、持久化。餓了麼在開發過程中也積累了很多基礎的插件,如Crash上報、推送處理、社交分享,還橋接了Native網絡庫發出Flutter請求,使得性能與體驗都保持一致。

屏幕快照 2020-06-22 下午5.13.46.png

dna - 背景與目標

隨著Channel越來越多,小到獲取Native參數,大到獲取Native執行功能,使得Channel使用越來越不便,其痛點主要有以下三點,一是雙邊硬編碼。在Dart和Native兩邊針對Channel的名字,如方法名等參數做硬編碼,一旦出錯就會調用錯誤。二是隻能單次調用,Flutter調Native時只能通過方法名匹配到一個代碼塊。三是創建成本高,不單要編寫Channel代碼,有時還需要創建Plugin。為了解決以上問題,餓了麼也做了一些探索和實踐,即dna Plugin。在設計和實現dna Plugin時也有幾個對應的目標,首先是直接調用Native方法,不再使用Channel名或方法名等,同時Native側不再使用硬編碼。二是支持上下文調用,Dart可以直接調用Native代碼。三是無需創建Channel和Plugin。

屏幕快照 2020-06-22 下午5.14.14.png

dna - 使用

下圖展示了dna快捷方法的使用,獲取了Android,iOS對應的版本號,以及系統平臺的字符串。可以發現,使用了NativeObject作為Native變量,NativeObject的生命週期與Native的生命週期是一致的。上一個方法的返回值可以作為下一個方法的參數,即上下文調用。原生調用上與Native很像,支持鏈式語法。返回值默認是最後一次context的的返回值。下圖下層框裡是假想的代碼,dna執行Native方法時相當於執行了框中的方法。

屏幕快照 2020-06-22 下午5.14.48.png

實際使用的dna代碼相對更簡單,下圖展示的是拆箱解碼的操作。iOS側調用了一個類來獲取實例,Android是直接調用了實例。iOS包含一個類一個方法,Android還需要一個dna method註解。

屏幕快照 2020-06-22 下午5.15.12.png

dna - 原理

當上面的context中的Native Object不斷的Invoke 方法時,會形成一個個的InvocationNode,InvocationNode定義了方法名,參數以及返回值。當context調用時,會轉換成一個context JSON,傳給Native。Native解析JSON,轉換成對應的一個個Invocation,被陸續調用。每個Native Object都對應一個ID。一個Invocation裡不但有調用值的ID,還調用了返回值的ID,他們都會放在一個map裡,實現上下文關聯。

屏幕快照 2020-06-22 下午5.15.29.png

dna在調用到一個Java方法時,需要一個dna method註解,主要是因為Android的release中做了代碼的混淆,無法通過類名和方法名定位。當一個方法加了Method註解時,可以掃描這個註解,生成代理類和代理方法。APT生成代理文件中包含掃描註解、生成的代理方法、存儲的方法名和參數信息。dna調用時先通過運行時註解匹配到代理方法,然後通過owner調用到真正的對象方法。

屏幕快照 2020-06-22 下午5.15.55.png

Channel VS. dna

dna不需要新建Plugin和Channel,而且由於dna是直接調用Native,所以所有硬編碼都沒有了,dna和Channel都不支持C函數,也不支持Native對象內存管理。目前dna傳遞JSON時還是使用Channel傳遞。很多情況下,只需要調用小部分Native代碼,dna也無需創建Plugin及Channel,而且Native也不需要硬編碼。在蜂鳥團隊中,非常基礎的功能是使用Channel做的,稍微涉及業務的功能都使用dna。

屏幕快照 2020-06-22 下午5.16.18.png

其他實踐

在餓了麼自身的實踐中,也做了其他的探索,如熱修復,雖然還沒有上線,但為Flutter boost提供了一些支持。

屏幕快照 2020-06-22 下午5.16.43.png

參與共建 - 背景

AliFlutter的目標是共同打造基礎設施,制定標準,複用技術。通過培育集團各個BU Flutter生態,沉澱業務與技術。順便可以聯合對外產生凝聚的影響力。

屏幕快照 2020-06-22 下午5.17.04.png

共建 - Pub Server

在Pub Server中,通過get和publish可以下載一些庫。AliFlutter為了Pub庫的保密性,以及外部庫的下載速度,也做了自己的Pub Server。與官方方案最大的不同是將Google Cloud存儲換成了阿里雲OSS存儲,當查詢外部的庫時先看內部是否有緩存,沒有才去下載外部的庫。

屏幕快照 2020-06-22 下午5.17.27.png

共建 - Pub Dev(前端檢索)

Pub Dev是大家所使用的前端檢索頁面,可以查到已發佈的Plugin或Dart庫。既然有了內部的Pub Server,還需要做Pub Dev。Pub Server提供的檢索功能非常有限。餓了麼做了自己的元信息數據庫,有一個定時任務可以定時讀取和解析Pub Server的產物信息。如此Pub Dev才可以滿足各類檢索需求。

屏幕快照 2020-06-22 下午5.17.49.png

共建 - 產物服務器

AliFlutter也做了自己的產物服務器,最初主要也是為了提高產物查詢速度。與Pub Server類似,依然是先檢查是否有內部的緩存,無需再去查詢外部產物,從而後面同學查詢的速度也更快了。

屏幕快照 2020-06-22 下午5.18.13.png

共建 - 引擎工作流

同時,產物服務器也可以作為引擎工作流。AliFlutter也對引擎做了一些修改,如圖片優化和機器優化,再將Android和iOS產物,如編譯debug產物,上傳到產物服務器中。整個引擎工作流是通過CI實現的。

屏幕快照 2020-06-22 下午5.18.39.png

共建 - CI/CD

AliFlutter CI/CD的主要目標是打出包含Flutter代碼的App,以及Flutter Module下的產物。在標準的App構建流程pipeline之上,餓了麼加了很多Flutter編譯邏輯和產物的特化邏輯。先把App Framework和Flutter Framework都打出來,還需要把Flutter Plugin的代碼單獨打成Framework或aar,生成postsec,最後上傳併發布。

屏幕快照 2020-06-22 下午5.18.57.png

展望與規劃

Flutter給餓了麼帶來的最大的價值是業務落地的提速。在未來,餓了麼也會推進更多的業務使用Flutter進行開發,包括跨業務組件的開發,以及Flutter代碼的分包。圍繞業務,還需保證質量,目前對Flutter性能監控和優化都是不夠的,所以計劃在性能監控和優化上再進一步擴展。另外,在業務的狂奔過程中,控制Crash數量,做好包體積優化。此外,餓了麼也希望做進一步提高研發效率的工作,如擴大UI組件範圍,抽取基礎插件,通過與AliFlutter共建,做好基礎設施及效率工具,如掃碼開發等。最後,有剩餘時間的話還可以做一些探索性的工作,如動態化UI,Flutter Web,三端一體化開發,引擎定製,同時可以結合前端做一些思考和遐想。總結起來,餓了麼對Flutter的期待是保質提效,賦能業務。

屏幕快照 2020-06-22 下午5.19.22.png

關注「淘系技術」微信公眾號,一個有溫度有內容的技術社區~

image.png

Leave a Reply

Your email address will not be published. Required fields are marked *