Flutter:移動端跨平臺技術演進之路

vimerzhao發表於2019-11-21

1. 導讀

本文約4688字,閱讀可能需要15分鐘。

最早的跨平臺開發(摘自《Apache Cordova移動應用開發實戰》王亞飛,王洪飛編著)

Snipaste 2019 11 08 20 13 54

從廣義上來說,跨平臺技術早於移動端的出現。因此,本文標題前面也加上了一個定語:“移動端”。而由上圖也可窺見一二:移動端跨平臺技術幾乎和移動端本身的歷史一樣長。跨平臺技術之所以生命力如此強大,個人認為有以下幾個原因:

  1. 開發效率 : 這也是跨平臺技術出現的初衷,理想狀態下,一次開發,多端執行,元件複用,提升效率。

    • 對於管理者,跨平臺可以降低用人成本,避免了同時養兩個(Android/iOS)開發團隊的現狀

    • 對於開發者,跨平臺可以降低學習成本,只需要瞭解一套框架,就可以實現雙端開發,提升了自我價值

  2. 業務價值 : 跨平臺開發成本更低,適合產品的快速驗證,待功能穩定後再進行效能體驗上的優化

  3. 二級生態 : 舉一個例子,JVM在作業系統上建立了自己的二級生態,所有的Java開發者只需要面向JVM程式設計和優化,可以忽視作業系統的存在。在原生、底層的平臺上做一層封裝,以很小的效能損失為代價,為開發者帶來巨大的效率提升,這在軟體工業是屢見不鮮的。二級生態有重要的戰略意義,掌握了二級生態,就掌握了話語權和影響力。所以Facebook和Google都在發力。

  4. 平臺能力 : 乘上第三點,跨平臺還有一個好處就是擁有了自己的生態就可以開放自己的能力,制定遊戲規則讓其他開發者參與,典型有小程式(微信/QQ/支付寶)、快應用等

跨平臺好處頗多,但挑戰也不少,主要集中在以下四個方面:

  1. 研發效率

    • 工具支援程度:補全、提示、構建管理等

    • Debug是否方便,錯誤日誌是否詳細

    • 文件完備、專案活躍

    • 隱藏平臺差異,如React Native需要大量橋接工作

    • 開發語言生態,js生態龐大,開發者眾,Dart則名不見經傳

    • 等等

  2. 動態化

    • iOS禁止,但國內平臺普遍需要

  3. 多端一致性

    • Web方案無法還原體驗

  4. 效能

    • Web方案UI繪製效率低,網路流量消耗高

    • 遊戲引擎耗電嚴重,不能應用在普通應用開發中

    • SDK引入導致的安裝包增量

2. 歷史行程

在過去的十多年間,主流的(不考慮一些小眾、沒有取得成功的方案)移動端跨平臺技術經歷了三次變革:

  • Hybrid,代表有:Ionic/Cordova

  • OEM Wrapper,代表有:React Native/Weex

  • 自渲染,代表有:Flutter

從時間上看,這三種方案不是孤立的,既有對前人不足之處的改進(如UI繪製策略),也有對優秀思想的繼承(如React的思想)。如果站在更高的高度上,我們會看到這些方案並不是在移動端獨立演進的。在移動端普及之前,PC端已經積累了很多成熟的方案,對於移動端的探索起到了指導作用,仔細比較一下會發現每一種方案都能找到已有方案的影子,只不過結合了移動端的特點做了定製。 無論哪種跨平臺方案,都要回答兩個問題:

  1. UI如何繪製

  2. 邏輯(包括使用者互動的邏輯和與宿主系統通訊的邏輯)如何響應

對於這兩個問題,Hybrid給出的答案是webview+js,OEM Wrapper給出的是VirtualDOM轉Native元件+js,自渲染(Flutter)給出的答案是Skia+Dart。下文開始詳述。

2.1. Hybrid

texingfenxi

架構圖

hybrid

Hybrid是客戶端跨平臺技術的第一個階段,核心原理是
將原生的介面封裝後暴露給 JavaScript,可以執行在系統自帶的 WebView中或者其他核心中。這種方案在上文提到的評價體系裡表現如下:

  1. 開發效率

    • 對前端開發者友好,背靠前端龐大的JavaScript生態

    • 涉及到Native呼叫的部分不可避免要熟悉Android/iOS

    • 能力受限於橋接層,擴充套件性弱

    • 在移動端開發,除錯和錯誤日誌並不是很友好

  2. 動態化

    • Web天生自帶動態能力

  3. 多端一致性

    • 瀏覽器核心的渲染獨立於系統元件,無法保證原生體驗

    • 涉及宿主的問題,需要開發者處理,做不到完全遮蔽

  4. 效能

    • 受限於網路環境,比Native更加消耗流量

    • 受限於瀏覽器、系統平臺特性

    • 渲染效能 ,Webview效能差

    • 特別指出:對於列表的支援差,移動端幾乎全是列表(feed流)

評價:Hybrid是矛盾的結合體,HTML/CSS 過於複雜導致效能問題,但其實這正是 Web 最大的優勢所在,因為 Web 最初的目的就是顯示文件,如果你想顯示豐富的圖文排版,雖然 iOS/Android 都有富文字元件,但比起 CSS 差太遠了,所以在很多 Native 應用中是不可避免要嵌 Web 的(比如很多運營活動的頁面,存在週期短,開發時間短,樣式豐富繁多,適合H5開發)。

既然Web強大的的繪製能力限制了其在移動端的效能,那麼能不能對此進行優化? 這是一個很重要的問題,很多看起來無關的方案都是基於這種思想發源來的:

  • React Native使用系統元件封裝,可以認為是把原來的瀏覽器核心換成了一個簡化版的核心:一個不能做物理渲染,只能轉換成有限原生元件的核心。

  • Flutter的 Engine 模組也可以認為是一個瀏覽器核心的角色!事實上Flutter的前身Sky就是打算基於一個精簡的Chromium核心來實現跨平臺。

2.2. OEM Wrapper

架構圖

oem

React Native架構圖

Snipaste 2019 11 16 16 15 28

大概到了2015年,經歷了各種Hybrid方案割據混戰長達數年後,Facebook推出了React Native,這種方案迎合了大前端的趨勢,一經推出就備受關注。核心改變是拋棄了低效的瀏覽器核心渲染,轉而使用自己的DSL生成中間格式,進而對映到對應的平臺。
其實這個方案其實也算不上什麼創舉,在PC時代,The Standard Widget Toolkit採用的就是這種方案(當然要做到React Native這種水平,非Facebook級別的公司不能為也):

From SWT官網

React Native 在評價體系表現如下:

  1. 開發效率

    • 在Web基礎上引進了React等能力,符合前端大趨勢

    • 版本變動頻繁,需要開發者自己優化,工作量大

    • 與Native互動需要開發者自己支援,維護成本高

    • 文件不完善、除錯資訊、錯誤日誌提示不夠友好

  2. 動態化

    • 可以支援

  3. 多端一致性

    • 渲染成各自平臺的元件,可以保證Native的體驗

    • 由於渲染依賴原生控制元件,不同平臺的控制元件需要單獨維護,並且當系統更新時,社群控制元件可能會滯後

    • 其控制元件系統也會受到原生UI系統限制,例如,在Android中,手勢衝突消歧規則是固定的,這在使用不同人寫的控制元件巢狀時,手勢衝突問題將會變得非常棘手

  4. 效能

    • 稍差於Native,但遠好於Hybrid

    • 渲染時需要JavaScript和原生之間通訊,在有些場景如拖動可能會因為通訊頻繁導致卡頓

    • JavaScript為指令碼語言,執行時需要JIT,執行效率和AOT程式碼仍有差距。

評價:使用類前端的語法,但又不在瀏覽器核心直接繪製,而是轉成Native控制元件,交由系統繪製。這樣既保留了前端這套開發體系,又最大限度保證了渲染的效能。咋一看,React Native解決了 Hybrid 技術的痛點:渲染效能,又充分發揮了Hybrid 的優勢:前端技術棧。但就在2018年,Airbnb和Udacity相繼宣佈棄用React Native,Facebook也宣佈要大規模重構React Native,導致其前景堪憂,比起React Native的美好願景,其在開發過程中需要踩的坑更多,長期的維護成本也很高,反而降低了開發效率,此外,庫的增量也不容忽視。

2.3. 自渲染

架構圖

flutter

Flutter 在評價體系表現如下:

  1. 開發效率

    • 開發工具完備,提供了VS Code(最流行的編輯器),Intellij IDEA 外掛

    • Google背書,文件完備,社群較完備

    • Dart語言本身有上手成本,沒有前端的生態,但Dart語言本身是及其優秀的

  2. 動態化

    • 動態性不足,為了保證UI繪製效能,自繪UI系統一般都會採用AOT模式編譯其釋出包

    • 可能涉及安全政策,Flutter Release目前不支援

    • 國內開發者正在做積極探索

  3. 多端一致性

    • 自繪製UI,提供了Material Design和Cupertino兩種風格的Widget

  4. 效能

    • 效能和Native繪製一樣

評價:Flutter站在前人的肩膀上,取長(React的狀態管理、Web的自繪製UI、React Native的HotReload等)補短(與Native通訊的Channel機制、自渲染、完備的開發工具鏈),並且有Google作為作為支撐,在跨平臺領域後發制人,是目前最被看好的方案。

關於Dart,在開發者踩了十幾年坑之後,Google和Microsoft兩大巨頭似乎看清了需要一種新的、更現代、更適合UI開發的程式語言來重新建立秩序。

Snipaste 2019 11 16 20 22 00

谷歌要推Dart,微軟要推Rust,這兩門語言的年齡比很多開發者的從業年齡都要小,大概是要以犧牲一代開發者為代價換取一個沒有歷史包袱(做夢)的新生態吧(手動狗頭)。

3. Flutter初探

3.1. Flutter技術架構

FlutterSystemArchitecture01

在Framework層,有以下內容

  • Foundation 底層庫

  • 負責UI繪製和互動處理的 AnimationPaint 等模組

  • 基於上面的介面實現的基礎元件 Widgets

  • 基於元件實現的 Material 風格元件(Google推薦)和 Cupertino 風格元件(iOS風格)

在Engine層,有以下內容

  • Skia 圖形處理庫

  • Dart 執行時

  • Text 文字排版引擎

這張圖和Android自身的架構很像

android stack 2x

這種設計符合Unix哲學的 正交性 原則(計算機網路中的OSI模型也是),可以很好地遮蔽每一個層的細節,接下來我們基於這個架構圖討論幾個問題

  • Flutter如何繪製UI

  • 為什麼選擇Dart

  • 如何和Native互動

  • HotReload與動態化

3.2. GUI Framework

在開始下文之前,需要介紹一下現代GUI框架的模型,無論是Web、PC、移動端、遊戲開發,都是基於這種框架工作

Snipaste 2019 11 17 13 51 11

  • Widgets(控制元件,也叫 View Tree),它是用於描述使用者介面原始資料的樹狀結構。通常這一層根本不關心繪製,它只關心使用者對資料的操作。

  • Render Tree,它是一種更為抽象的樹狀資料結構,一般來說它是和上一步的 View Tree結構相同,並且它不關心原始資料,只關心控制元件的佈局和大小。通過這一步計算出控制元件佈局後才能真正地確定控制元件的外觀。

  • Layer Tree 跟 Render Tree是相對應的,這一步會主動觸發 Render Tree中每個元素的外觀渲染,在已知控制元件大小和位置的情況下決定每個控制元件的真正外觀。但 Layer Tree的樹狀結構不是和 Render Tree一一對應的,Layer Tree有可能因為 Layer合併優化導致一層的 Render Tree葉子節點最終只對應一個 Layer。

  • 在已經決定好控制元件的大小位置以及長相後,剩下的工作就需要把這些東西組合起來顯示到螢幕上。這一步原理比較簡單,就是將前一步的 Layer合併成一張 Bitmap,這是一種最簡單的影象儲存形式。將 Bitmap光柵化後便可以提交給 GPU渲染。

比如Android開發都很熟悉的 measure layout draw 就和前三層吻合,有了原始資料還不夠,螢幕只關心每個畫素點的值,所以最後要進行一次光柵化,歷史已經證明這種模型是目前來說相對高效的GUI方案。

3.3. Flutter如何繪製UI

有了以上背景,我們來看Flutter的UI繪製過程。

Flutter繪製流程1

FlutterSystemArchitecture02

Flutter繪製流程2

FlutterSystemArchitecture03

  • GPU發出 Vsync 訊號,Dart捕獲後就開始一幀的繪製。

  • Throttle: 用來做節流,防止短時間內重複呼叫,提高效能。

  • Compositor: 這一步進行 Layer合成,決定某一塊具體顯示哪一個 Layer的資料,可以額外的計算開支。

  • GL or Vulkan: 這一階段過後得到的將是一份向量圖資料,在進行光柵化後提交給 GPU執行渲染即可。

3.4. 為什麼選擇Dart

官方解釋:

  • Developer productivity

    • JIT + AOT

    • Dart開發團隊對於Flutter支援粒度很大

  • Object-orientation

  • Predictable, high performance

  • Fast allocation.

其他聲音:

  • Dart 的效能更好,對高幀率下的檢視資料計算很有幫助。

  • 多生代無鎖垃圾回收器,專門為UI框架中常見的大量Widgets物件建立和銷燬優化

  • Native Binding,在 Android上,v8的 Native Binding可以很好地實現,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基礎框架的程式碼模式就很難統一了。而 Dart的 Native Binding可以很好地通過 Dart Lib實現。

  • Fuchsia OS(谷歌的野心:5G + IOT) 

  • Dart是型別安全的語言,擁有完善的包管理和諸多特性。

關於IOT,國內已經有開發者嘗試在樹莓派上使用Flutter了。

總的來說,想要挑戰JavaScript的地位還是很難的,JavaScript雖然有很多缺陷,但龐大的生態已經為自己建立了穩固的堡壘。

有一個關於Lisp的笑話,我覺得也可以改個Dart版本的:某小偷偷了美國國防部機密軟體的原始碼的最後幾頁打算拿回去好好研究,但等他真的開啟時發現是這樣

Snipaste 2019 11 16 20 32 51

3.5. 如何和Native互動

Flutter Platform Channel Demo

通過 Platform Channel機制進行通訊,常用的 Platform Channel主要有三種

  • BasicMessageChannel:傳遞字串和半結構化資料

  • MethodChannel:方法呼叫

  • EventChannel:資料流的通訊

每種Channel具有三個重要的成員變數:

  • name: String型別,代表Channel的名字,也是其唯一識別符號。

  • messager:BinaryMessenger型別,代表訊息信使,是訊息的傳送與接收的工具。

  • codec: MessageCodec型別或MethodCodec型別,代表訊息的編解碼器。

對常見的Channel進一步封裝成Plugin

Snipaste 2019 11 17 18 23 39

3.6. HotReload與動態化

Flutter Hot Demo

Flutter的HotReload確實讓人耳目一新,但這東西可能只是對於客戶端開發比較稀奇,從Instant Run到freeline,開發者一直希望能提高專案構建速度,更快地看到程式碼改動的結果,從原理上來說,只要這門語言及其所在平臺支援解釋執行(or JIT)和增量編譯就可以做到很好的HotReload效果,flutter的這一個特性算是填上了之前Android開發的一個坑,創舉肯定是算不上的。 至於動態化,可以算是國內開發者的剛需了,Flutter之前移除了對動態化的支援,可能是擔心iOS的政策原因,目前不少開發者也對Flutter展開了研究,後面應該有一批魔改方案。
Flutter的HotReload過程:

  1. 首先會掃描程式碼,找到上次編譯之後有變化的Dart程式碼;

  2. 將這些變化的Dart程式碼轉化為增量的Dart Kernel檔案;

  3. 將增量的Dart Kernel檔案傳送到正在移動裝置上執行的Dart VM;

  4. 觸發widgets樹的重新建立、重新佈局、重新繪製

不能使用的場景(所以對這個功能不能過於樂觀,業務複雜之後能用的場景就不會太多):

  1. 程式碼出現編譯錯誤的不能使用 Hot Reload

  2. Widget的狀態更改不能使用 Hot Reload

  3. 在 Flutter 中,全域性變數和靜態欄位被視為狀態,因此在 Hot Reload 期間不會重新初始化。

  4. 修改通用型別宣告時

4. 評價

從Hybrid到Flutter,突破性的創新是不會有的,每一個特性、功能都是紮紮實實演進過來的,這也是標題的本意,Flutter能否成為大前端的答案尚未可知,谷歌布的局又有多大也不清楚,核心還是要把握住發展脈絡,對新事物保持警惕和清醒。浪潮退去才知道誰在裸泳,有一天Flutter的替代者來了,才知道誰是API boy(哭泣臉)。

5. 參考


相關文章