天天看點

Flutter的原理及美團的實踐(上)

阿裡P7移動網際網路架構師進階視訊(每日更新中)免費學習請點選: https://space.bilibili.com/474380680

導讀

Flutter是Google開發的一套全新的跨平台、開源UI架構,支援iOS、Android系統開發,并且是未來新作業系統Fuchsia的預設開發套件。自從2017年5月釋出第一個版本以來,目前Flutter已經釋出了近60個版本,并且在2018年5月釋出了第一個“Ready for Production Apps”的Beta 3版本,6月20日釋出了第一個“Release Preview”版本。

初識Flutter

Flutter的目标是使同一套代碼同時運作在Android和iOS系統上,并且擁有媲美原生應用的性能,Flutter甚至提供了兩套控件來适配Android和iOS(滾動效果、字型和控件圖示等等),為了讓App在細節處看起來更像原生應用。

在Flutter誕生之前,已經有許多跨平台UI架構的方案,比如基于WebView的Cordova、AppCan等,還有使用HTML+JavaScript渲染成原生控件的React Native、Weex等。

基于WebView的架構優點很明顯,它們幾乎可以完全繼承現代Web開發的所有成果(豐富得多的控件庫、滿足各種需求的頁面架構、完全的動态化、自動化測試工具等等),當然也包括Web開發人員,不需要太多的學習和遷移成本就可以開發一個App。同時WebView架構也有一個緻命(在對體驗&性能有較高要求的情況下)的缺點,那就是WebView的渲染效率和JavaScript執行性能太差。再加上Android各個系統版本和裝置廠商的定制,很難保證所在所有裝置上都能提供一緻的體驗。

為了解決WebView性能差的問題,以React Native為代表的一類架構将最終渲染工作交還給了系統,雖然同樣使用類HTML+JS的UI建構邏輯,但是最終會生成對應的自定義原生控件,以充分利用原生控件相對于WebView的較高的繪制效率。與此同時這種政策也将架構本身和App開發者綁在了系統的控件系統上,不僅架構本身需要處理大量平台相關的邏輯,随着系統版本變化和API的變化,開發者可能也需要處理不同平台的差異,甚至有些特性隻能在部分平台上實作,這樣架構的跨平台特性就會大打折扣。

Flutter則開辟了一種全新的思路,從頭到尾重寫一套跨平台的UI架構,包括UI控件、渲染邏輯甚至開發語言。渲染引擎依靠跨平台的Skia圖形庫來實作,依賴系統的隻有圖形繪制相關的接口,可以在最大程度上保證不同平台、不同裝置的體驗一緻性,邏輯處理使用支援AOT的Dart語言,執行效率也比JavaScript高得多。

Flutter同時支援Windows、Linux和macOS作業系統作為開發環境,并且在Android Studio和VS Code兩個IDE上都提供了全功能的支援。Flutter所使用的Dart語言同時支援AOT和JIT運作方式,JIT模式下還有一個備受歡迎的開發利器“熱重新整理”(Hot Reload),即在Android Studio中編輯Dart代碼後,隻需要點選儲存或者“Hot Reload”按鈕,就可以立即更新到正在運作的裝置上,不需要重新編譯App,甚至不需要重新開機App,立即就可以看到更新後的樣式。

在Flutter中,所有功能都可以通過組合多個Widget來實作,包括對齊方式、按行排列、按列排列、網格排列甚至事件處理等等。Flutter控件主要分為兩大類,StatelessWidget和StatefulWidget,StatelessWidget用來展示靜态的文本或者圖檔,如果控件需要根據外部資料或者使用者操作來改變的話,就需要使用StatefulWidget。State的概念也是來源于Facebook的流行Web架構

React

,React風格的架構中使用控件樹和各自的狀态來建構界面,當某個控件的狀态發生變化時由架構負責對比前後狀态差異并且采取最小代價來更新渲染結果。

Hot Reload

在Dart代碼檔案中修改字元串“Hello, World”,添加一個驚歎号,點選儲存或者熱重新整理按鈕就可以立即更新到界面上,僅需幾百毫秒:

Flutter通過将新的代碼注入到正在運作的DartVM中,來實作Hot Reload這種神奇的效果,在DartVM将程式中的類結構更新完成後,Flutter會立即重建整個控件樹,進而更新界面。但是熱重新整理也有一些限制,并不是所有的代碼改動都可以通過熱重新整理來更新:

  1. 編譯錯誤,如果修改後的Dart代碼無法通過編譯,Flutter會在控制台報錯,這時需要修改對應的代碼。
  2. 控件類型從

    StatelessWidget

    StatefulWidget

    的轉換,因為Flutter在執行熱重新整理時會保留程式原來的state,而某個控件從stageless→stateful後會導緻Flutter重新建立控件時報錯“myWidget is not a subtype of StatelessWidget”,而從stateful→stateless會報錯“type 'myWidget' is not a subtype of type 'StatefulWidget' of 'newWidget'”。
  3. 全局變量和靜态成員變量,這些變量不會在熱重新整理時更新。
  4. 修改了main函數中建立的根控件節點,Flutter在熱重新整理後隻會根據原來的根節點重新建立控件樹,不會修改根節點。
  5. 某個類從普通類型轉換成枚舉類型,或者類型的泛型參數清單變化,都會使熱重新整理失敗。

熱重新整理無法實作更新時,執行一次熱重新開機(Hot Restart)就可以全量更新所有代碼,同樣不需要重新開機App,差別是restart會将所有Dart代碼打包同步到裝置上,并且所有狀态都會重置。

Flutter插件

Flutter使用的Dart語言無法直接調用Android系統提供的Java接口,這時就需要使用插件來實作中轉。Flutter官方提供了豐富的原生接口封裝:

  • android_alarm_manager,通路Android系統的

    AlertManager

  • android_intent,構造Android的Intent對象。
  • battery,擷取和監聽系統電量變化。
  • connectivity,擷取和監聽系統網絡連接配接狀态。

    *device info,擷取裝置型号等資訊。

  • image_picker,從裝置中選取或者拍攝照片。
  • package_info,擷取App安裝包的版本等資訊。
  • path_provider,擷取常用檔案路徑。
  • quick_actions,App圖示添加快捷方式,iOS的eponymous concept和Android的App Shortcuts。
  • sensors,通路裝置的加速度和陀螺儀傳感器。
  • shared_preferences,App KV存儲功能。
  • url_launcher,啟動URL,包括打電話、發短信和浏覽網頁等功能。
  • video_player,播放視訊檔案或者網絡流的控件。

在Flutter中,依賴包由

Pub

倉庫管理,項目依賴配置在pubspec.yaml檔案中聲明即可(類似于NPM的版本聲明

Pub Versioning Philosophy

),對于未釋出在Pub倉庫的插件可以使用git倉庫位址或檔案路徑:

dependencies: 
  url_launcher: ">=0.1.2 <0.2.0"
  collection: "^0.1.2"
  plugin1: 
    git: 
      url: "git://github.com/flutter/plugin1.git"
  plugin2: 
    path: ../plugin2/
           

以shared_preferences為例,在pubspec中添加代碼:

dependencies:
  flutter:
    sdk: flutter

  shared_preferences: "^0.4.1"
           

脫字号“^”開頭的版本表示

和目前版本接口保持相容

的最新版,

^1.2.3

等效于

>=1.2.3 <2.0.0

^0.1.2

>=0.1.2 <0.2.0

,添加依賴後點選“Packages get”按鈕即可下載下傳插件到本地,在代碼中添加import語句就可以使用插件提供的接口:

import 'package:shared_preferences/shared_preferences.Dart';

class _MyAppState extends State<MyAppCounter> {
  int _count = 0;
  static const String COUNTER_KEY = 'counter';

  _MyAppState() {
    init();
  }

  init() async {
    var pref = await SharedPreferences.getInstance();
    _count = pref.getInt(COUNTER_KEY) ?? 0;
    setState(() {});
  }

  increaseCounter() async {
    SharedPreferences pref = await SharedPreferences.getInstance();
    pref.setInt(COUNTER_KEY, ++_count);
    setState(() {});
  }
...
           

Dart

Dart是一種強類型、跨平台的用戶端開發語言。具有專門為用戶端優化、高生産力、快速高效、可移植(相容ARM/x86)、易學的OO程式設計風格和原生支援響應式程式設計(Stream & Future)等優秀特性。Dart主要由Google負責開發和維護,在

2011年10啟動項目

,2017年9月釋出第一個2.0-dev版本。

Dart本身提供了三種運作方式:

  1. 使用Dart2js編譯成JavaScript代碼,運作在正常浏覽器中( Dart Web )。
  2. 使用DartVM直接在指令行中運作Dart代碼( DartVM
  3. AOT方式編譯成機器碼,例如Flutter App架構( Flutter

Flutter在篩選了20多種語言後,最終選擇Dart作為開發語言主要有幾個原因:

  1. 健全的類型系統,同時支援靜态類型檢查和運作時類型檢查。
  2. 代碼體積優化(Tree Shaking),編譯時隻保留運作時需要調用的代碼(不允許反射這樣的隐式引用),是以龐大的Widgets庫不會造成釋出體積過大。
  3. 豐富的底層庫,Dart自身提供了非常多的庫。
  4. 多生代無鎖垃圾回收器,專門為UI架構中常見的大量Widgets對象建立和銷毀優化。
  5. 跨平台,iOS和Android共用一套代碼。
  6. JIT & AOT運作模式,支援開發時的快速疊代和正式釋出後最大程度發揮硬體性能。

在Dart中,有一些重要的基本概念需要了解:

  • 所有變量的值都是對象,也就是類的執行個體。甚至數字、函數和

    null

    也都是對象,都繼承自 Object 類。
  • 雖然Dart是強類型語言,但是顯式變量類型聲明是可選的,Dart支援類型推斷。如果不想使用類型推斷,可以用 dynamic 類型。
  • Dart支援泛型,

    List

    表示包含int類型的清單,

    List

    則表示包含任意類型的清單。
  • Dart支援頂層(top-level)函數和類成員函數,也支援嵌套函數和本地函數。
  • Dart支援頂層變量和類成員變量。
  • Dart沒有public、protected和private這些關鍵字,使用下劃線“_”開頭的變量或者函數,表示隻在庫内可見。參考 庫和可見性

DartVM的記憶體配置設定政策非常簡單,建立對象時隻需要在現有堆上移動指針,記憶體增長始終是線形的,省去了查找可用記憶體段的過程:

Dart中類似線程的概念叫做Isolate,每個Isolate之間是無法共享記憶體的,是以這種配置設定政策可以讓Dart實作無鎖的快速配置設定。

Dart的垃圾回收也采用了多生代算法,新生代在回收記憶體時采用了“半空間”算法,觸發垃圾回收時Dart會将目前半空間中的“活躍”對象拷貝到備用空間,然後整體釋放目前空間的所有記憶體:

整個過程中Dart隻需要操作少量的“活躍”對象,大量的沒有引用的“死亡”對象則被忽略,這種算法也非常适合Flutter架構中大量Widget重建的場景。

Flutter Framework

Flutter的架構部分完全使用Dart語言實作,并且有着清晰的分層架構。分層架構使得我們可以在調用Flutter提供的便捷開發功能(預定義的一套高品質Material控件)之外,還可以直接調用甚至修改每一層實作(因為整個架構都屬于“使用者空間”的代碼),這給我們提供了最大程度的自定義能力。Framework底層是Flutter引擎,引擎主要負責圖形繪制(Skia)、文字排版(libtxt)和提供Dart運作時,引擎全部使用C++實作,Framework層使我們可以用Dart語言調用引擎的強大能力。

分層架構

Framework的最底層叫做Foundation,其中定義的大都是非常基礎的、提供給其他所有層使用的工具類和方法。繪制庫(Painting)封裝了Flutter Engine提供的繪制接口,主要是為了在繪制控件等固定樣式的圖形時提供更直覺、更友善的接口,比如繪制縮放後的位圖、繪制文本、插值生成陰影以及在盒子周圍繪制邊框等等。

Animation是動畫相關的類,提供了類似Android系統的ValueAnimator的功能,并且提供了豐富的内置插值器。Gesture提供了手勢識别相關的功能,包括觸摸事件類定義和多種内置的手勢識别器。GestureBinding類是Flutter中處理手勢的抽象服務類,繼承自BindingBase類。

Binding系列的類在Flutter中充當着類似于Android中的SystemService系列(ActivityManager、PackageManager)功能,每個Binding類都提供一個服務的單例對象,App最頂層的Binding會包含所有相關的Bingding抽象類。如果使用Flutter提供的控件進行開發,則需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接調用Render層,則需要使用RenderingFlutterBinding。

Flutter本身支援Android和iOS兩個平台,除了性能和開發語言上的“native”化之外,它還提供了兩套設計語言的控件實作Material & Cupertino,可以幫助App更好地在不同平台上提供原生的使用者體驗。

渲染庫(Rendering)

Flutter的控件樹在實際顯示時會轉換成對應的渲染對象(

RenderObject

)樹來實作布局和繪制操作。一般情況下,我們隻會在調試布局,或者需要使用自定義控件來實作某些特殊效果的時候,才需要考慮渲染對象樹的細節。渲染庫主要提供的功能類有:

abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { ... }
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
abstract class RenderBox extends RenderObject { ... }
class RenderParagraph extends RenderBox { ... }
class RenderImage extends RenderBox { ... }
class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData>,
                                        RenderBoxContainerDefaultsMixin<RenderBox, FlexParentData>,
                                        DebugOverflowIndicatorMixin { ... }
           

RendererBinding

是渲染樹和Flutter引擎的膠水層,負責管理幀重繪、視窗尺寸和渲染相關參數變化的監聽。

RenderObject

渲染樹中所有節點的基類,定義了布局、繪制和合成相關的接口。

RenderBox

和其三個常用的子類

RenderParagraph

RenderImage

RenderFlex

則是具體布局和繪制邏輯的實作類。

在Flutter界面渲染過程分為三個階段:布局、繪制、合成,布局和繪制在Flutter架構中完成,合成則交由引擎負責:

控件樹中的每個控件通過實作

RenderObjectWidget#createRenderObject(BuildContext context) → RenderObject

方法來建立對應的不同類型的

RenderObject

對象,組成渲染對象樹。因為Flutter極大地簡化了布局的邏輯,是以整個布局過程中隻需要深度周遊一次:

渲染對象樹中的每個對象都會在布局過程中接受父對象的

Constraints

參數,決定自己的大小,然後父對象就可以按照自己的邏輯決定各個子對象的位置,完成布局過程。

子對象不存儲自己在容器中的位置,是以在它的位置發生改變時并不需要重新布局或者繪制。子對象的位置資訊存儲在它自己的

parentData

字段中,但是該字段由它的父對象負責維護,自身并不關心該字段的内容。同時也因為這種簡單的布局邏輯,Flutter可以在某些節點設定布局邊界(Relayout boundary),即當邊界内的任何對象發生重新布局時,不會影響邊界外的對象,反之亦然:

布局完成後,渲染對象樹中的每個節點都有了明确的尺寸和位置,Flutter會把所有對象繪制到不同的圖層上:

因為繪制節點時也是深度周遊,可以看到第二個節點在繪制它的背景和前景不得不繪制在不同的圖層上,因為第四個節點切換了圖層(因為“4”節點是一個需要獨占一個圖層的内容,比如視訊),而第六個節點也一起繪制到了紅色圖層。這樣會導緻第二個節點的前景(也就是“5”)部分需要重繪時,和它在邏輯上毫不相幹但是處于同一圖層的第六個節點也必須重繪。為了避免這種情況,Flutter提供了另外一個“重繪邊界”的概念:

在進入和走出重繪邊界時,Flutter會強制切換新的圖層,這樣就可以避免邊界内外的互相影響。典型的應用場景就是ScrollView,當滾動内容重繪時,一般情況下其他内容是不需要重繪的。雖然重繪邊界可以在任何節點手動設定,但是一般不需要我們來實作,Flutter提供的控件預設會在需要設定的地方自動設定。

控件庫(Widgets)

Flutter的控件庫提供了非常豐富的控件,包括最基本的文本、圖檔、容器、輸入框和動畫等等。在Flutter中“一切皆是控件”,通過組合、嵌套不同類型的控件,就可以建構出任意功能、任意複雜度的界面。它包含的最主要的幾個類有:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding,
            PaintingBinding, RendererBinding, WidgetsBinding { ... }
abstract class Widget extends DiagnosticableTree { ... }
abstract class StatelessWidget extends Widget { ... }
abstract class StatefulWidget extends Widget { ... }
abstract class RenderObjectWidget extends Widget { ... }
abstract class Element extends DiagnosticableTree implements BuildContext { ... }
class StatelessElement extends ComponentElement { ... }
class StatefulElement extends ComponentElement { ... }
abstract class RenderObjectElement extends Element { ... }
...
           

基于Flutter控件系統開發的程式都需要使用

WidgetsFlutterBinding

,它是Flutter的控件架構和Flutter引擎的膠水層。

Widget

就是所有控件的基類,它本身所有的屬性都是隻讀的。

RenderObjectWidget

所有的實作類則負責提供配置資訊并建立具體的

RenderObjectElement

Element

是Flutter用來分離控件樹和真正的渲染對象的中間層,控件用來描述對應的element屬性,控件重建後可能會複用同一個element。

RenderObjectElement

持有真正負責布局、繪制和碰撞測試(hit test)的

RenderObject

對象。

StatelessWidget

StatefulWidget

并不會直接影響

RenderObject

建立,隻負責建立對應的

RenderObjectWidgetStatelessElement

StatefulElement

也是類似的功能。

它們之間的關系如下圖:

如果控件的屬性發生了變化(因為控件的屬性是隻讀的,是以變化也就意味着重新建立了新的控件樹),但是其樹上每個節點的類型沒有變化時,element樹和render樹可以完全重用原來的對象(因為element和render object的屬性都是可變的):

但是,如果控件樹種某個節點的類型發生了變化,則element樹和render樹中的對應節點也需要重新建立:

原文作者:美團技術團隊

原文連結:

https://zhuanlan.zhihu.com/p/41731412