你們好,我是郭樹煜,掘金 《Flutter 完整開發實戰詳解》 系列的做者,Github GSY 系列開源項目的維護人員,系列包括 GSYVideoPlayer 、
GSYGitGithubApp
(Flutter \ ReactNative \ Kotlin \ Weex 四大版本)、GSYFlutterBook 電子書等,系列總 star 數在 25k 左右,目前 Github 中國區粉絲數暫居 67 名,主要負責移動端項目開發,大前端方向,主要涉及領域有 Android、Flutter、React Native、Weex 、小程序等等。前端
此次分享的主題主要涉及:移動端跨平臺開發的發展、Flutter Widget 的實現原理 、 Flutter 的實戰技巧 、Flutter Web的現狀 四個方面,而總體主題將圍繞 Widget 爲中心展開。git
按照慣例,咱們先介紹歷史進程,隨着用戶終端種類的百花齊放,現在跨平臺開發已然成爲移動領域的熱門話題之一,移動端跨平臺開發技術的發展,也表明着開發者對於性能、複用、高效上不斷的追求。github
移動端的跨平臺開發主要有三個階段,這些階段的表明框架主要有:Cordova
、React Native
、Flutter
等,以下圖所示,是移動端的跨平臺發展歷程:web
Cordova
做爲早期跨平臺領域應用最普遍的框架,爲前端人員所熟知,其主要原理就是:面試
將 web 代碼打包到本地,利用平臺的 WebView 進行加載,經過內部約定好的 JS 通信協議,加載和調用具有平臺原生能力的插架。canvas
Cordova
讓前端開發人員能夠快速的構建移動應用,獲取平臺入口,對早期 web 上欠缺的如攝像機、本地緩存、文件讀寫等能力進行快速支持。小程序
早期的移動開發市場除了 Android 和 iOS 以外,還有 WindowPhone、黑莓等,
Cordova
簡單又實用的理念,使得它成爲早期熱門的跨平臺框架,至今仍在更新的ionic
框架,也是在其基礎上進行了封裝發展。緩存
Cordova
雖然實用方便,可是因爲 WebView
的性能瓶頸,開發者開始追求更高性能,且具有平臺特點的跨平臺能力,這時候由 Facebook 開源的 React Native
框架開始引領新潮流。bash
React Native
讓 JS 代碼運行在框架內置的 JS 引擎(JavaScriptCore)上,利用 JS 引擎實現了跨平臺能力,同時又將 JS 控件,對應解析爲平臺原生控件進行渲染,從而實現性能的優化與提高。markdown
因爲 React
框架的盛行, React Native
也開始成爲 React
開發人員,將自身能力拓展到應用開發的最佳選擇之一。同時 React Native
也是應用開發人員,接觸前端的不錯嘗試。
後來阿里開源的
Weex
框架設計類似,利用了 V8 引擎實現跨平臺,不過使用了Vue
的設計理念,而Weex
由於種種緣由,最終仍是沒能大面積推廣開來。
事實上 JS Bridge
一樣存在性能等限制,Facebook 也在着力優化這一問題,好比 HermesJS
、底層大規模重構等 ,而 JS -> 平臺控件映射,也致使了框架和平臺耦合過多,在版本兼容和系統升級等問題上讓框架維護愈加困難。
這時候谷歌開源了 Flutter
,它另闢蹊徑,只要求平臺提供一個 Surface
和一個 Canvas
,剩下的 Flutter
說:「你能夠躺下了,咱們來本身動」。
Flutter
的跨平臺思路快速讓他成爲「新貴」,連跨平臺界的老大哥 「JS」 語言都「視而不見」,大膽的選擇 Dart
也讓 Flutter
在前期的推廣中飽受爭議。
短短兩年,不算 PR ,
Flutter
的 issue 已經有近 1.8 萬的 closed 和 8000+ open , 這表明了它的熱度,也表明着它須要面對的問題和挑戰。 不支持 Release 模式下的熱更新,也讓用戶更多徘徊於 React Native 不肯嘗試。不過有一點能夠肯定的,那就是
Flutter
的版本號上是完全打敗了React Naitve
。
總結起來,咱們能夠看到,移動端跨平臺的發展,從單純的套殼打包,到提供高性能的跨平臺控件封裝,再到如今的控件與平臺脫離的發展。 整個發展歷程,就是對 性能、複用、高效 的不斷追求。
一、開發成本
我直接學 Java
/Kotlin
、Object-C
/Swift
、JavaScript
/CSS
去寫各平臺的代碼能夠嗎?
固然能夠,這樣的性能確定最有保證,可是跨平臺的主要優點在於代碼邏輯的複用,減小各平臺同一邏輯,因人而異的開發成本。
二、學習機會
通常狀況下,各平臺開發者容易侷限在本身的領域開發,而做爲應用開發者,跨平臺是接觸另外一平臺或領域的過渡機會。
下面開始今天的主題 Flutter ,Flutter 總體涉及的內容不少,因爲篇幅問題,本篇咱們的主題總體都圍繞一個
Widget
展開。Flutter 做爲跨平臺 UI 框架,Widget
是其靈魂設定之一。
Flutter 是 UI 框架,Flutter 內一切皆 Widget
,每一個 Widget
狀態都表明了一幀,Widget
是不可變的。 那麼 Widget
是怎麼工做的呢?
以下圖能夠看到,是一個簡單的 Flutter Widget
頁面代碼,頁面包含了一個標題和容易,那在頁面 build
時,它是怎麼表繪製出來的呢?同時它是如何保證性能? 而Widget
又是怎麼樣的一個概念?後面咱們將逐步揭曉。
首先看上圖代碼,其實如圖的代碼並非真正的 View
級別代碼,它們更像是配置文件。
而要知道 Widget
是如何工做的,這就涉及到 Flutter 的三大金剛: Widget
、 Element
、RenderObject
。 事實上,這三大金剛纔能組成了 Flutter Framework 的基礎渲染閉環。
如上圖所示,當一個 Widget
被「加載「的時候,它並非立刻被繪製出來,而是會對應先建立出它的 Element
,而後經過 Element
將 Widget
的配置信息轉化爲 RenderObject
實現繪製。
因此,在 Flutter 中大部分時候咱們寫的是 Widget
,可是 Widget
的角色反而更像是「配置文件」 ,真正觸發工做的實際上是 RenderObject
。
小結一下這裏的關係就是:
Widget
是配置文件。Element
是橋樑和倉庫。RenderObject
是解析後的繪製和佈局。對應詳細的解釋就是:
Widget
,它須要轉化爲相應的 RenderObject
去工做;Element
持有 Widget
和 RenderObject
,做爲二者的橋樑,並保存着一些狀態參數,咱們在 Flutter 框架中常見到的 BuildContext
,其實就是 Element
的抽象 ;Widget
的配置信息,轉化到 RenderObject
內,告訴 Canvas
應該在哪一個 Rect
內,繪製多大 Size
的數據。因此 Widget
和咱們之前的佈局概念不同,由於 Widget
是不可變的(immutable
),且只有一幀,且不是真正工做的對象,每次畫面變化,都會致使一些 Widget
從新 build
。
那到這裏,咱們可能就會關心性能的問題,Flutter 是如何保證性能呢?
其實就是迴歸到了 Widget
的定位,做爲「配置文件」,Widget
的變化,是否也會致使 Element
和 RenderObject
也會從新建立?
答案是不必定會,Widget
只是一個 「配置文件」 的做用,是很是輕量級的,它的存在,只是起到對 RenderObject
的數據進行配置的做用。
可是 RenderObject
就不同了,它涉及到了 layout
、paint
等真實 的繪製操做,能夠認爲是一個真正的 「View」 ,若是頻繁建立就會導性能出現問題。
因此在 Flutter 中,會有一系列的判斷,來處理 Widget
到 RenderObject
轉化的性能問題 ,這部分操做一般是在 Element
中進行的 ,例如 updateChild
時,會有以下圖所示的判斷:
當 element.child.widget == widget.build()
時,就不會觸發 update
操做;
在 update
時,canUpdate(element.child.widget, newWidget)
返回 true, Element
纔會被更新;(這裏代碼中的 slot
通常爲 Element
對象,有時候會傳空)
其餘還有利用 isRelayoutBoundary
、 isRepaintBoundary
等參數,來實現局部的更新判斷,好比:當執行 markNeedsPaint() 觸發繪製時,會經過 isRepaintBoundary
是否爲 true
, 往上肯定了更新區域,經過 requestVisualUpdate
方法觸發更新往下繪製。
經過
isRepaintBoundary
參數, 對應的RenderObject
能夠組成一個Layer
。
因此這就能夠解答一些初學者的疑問,嵌套那麼多 Widget
,性能會不會有問題?
這也體現出 Flutter 在佈局上和其餘框架不一樣的地方,你寫的 Widget
只是配置文件,堆疊嵌套了一堆控件,對最終的 RenderObject
而言,可能只是多幾個 Offset
和 Size
計算而已。
結合上面的理解,能夠知道 Widget
大部分時候,其實只是輕量級的配置,對於性能問題,你更須要關心的是 Clip
、Overlay
、透明合成等行爲,由於它們會須要產生 saveLayer
的操做,由於 saveLayer
會清空GPU繪製的緩存。
最後總結個面試點:
同一個 Widget
能夠同時描述多個渲染樹中的節點,做爲配置文件是能夠複用的。 Widget
和 RenderObject
通常狀況是一對多的關係。 ( 前提是在 Widget
存在 RenderObject
的狀況。)
Element
是 Widget
的某個固定實例,與 RenderObject
一一對應。(前提是在 Element
存在 RenderObject
的狀況。)
RenderObject
內 isRepaintBoundary
標示使得它們組成了一個個 Layer
區域。
當 isRepaintBoundary
爲 true
時,該區域就是一個可更新繪製區域,而當這個區域造成時,就會新建立一個 Layer
。 但不是每一個 RenderObject
都會有 Layer
, 由於這受 isRepaintBoundary
的影響。
注意,Flutter 中常見的
BuildContext
,其實就是Element
的抽象,經過BuildContext
,咱們通常狀況就能夠對應得到Element
,也就是拿到了「倉庫的鑰匙」 ,經過context
就能夠去獲取Element
內持有的東西,好比前面所說的RenderObject
,還有後面咱們會談到State
等。
這裏咱們將 Widget
分爲以下圖所示分類:是否存在 State
、是否存在RenderObject
。
其實還能夠按照
RenderBox
和RenderSliver
分類,可是篇幅緣由之後再介紹。
Flutter 中咱們經常使用的 Widget
有: StatelessWidget
和 StatefulWidget
。
以下圖, StatelessWidget
的代碼很簡單,由於 Widget
是不可變的,傳入的 text
決定了它顯示的內容,而且 text
也算是 final
的。
注意圖中
DemoPage
有個黃色警告,這是由於咱們定義了int i = 0
不是 final 致使的,在StatelessWidget
中, 非 final 的變量起始容易產生誤解,由於Widget
本事就是不可變的。
前面咱們說過 Widget
都是不可變的,在這個基礎上, StatefulWidget
的 State
,幫咱們實現了 Widget
的跨幀繪製 ,也就是在每次 Widget
重構時,能夠經過 State
從新賦予 Widget
須要的配置信息,而這裏的 State
對象,就是存在每一個 Element
裏的。
同時,前面咱們說過,Flutter 內的
BuildContext
其實就是Element
的抽象,這說明咱們能夠經過context
去獲取Element
內的東西,好比State
、RenderObject
、Widget
。
Widget ancestorWidgetOfExactType
State ancestorStateOfType
State rootAncestorStateOfType
RenderObject ancestorRenderObjectOfType
複製代碼
以下圖所示,保存在 State
中的 text
,當咱們點擊按鍵時,setState
時它被標誌爲 "變化了"
, 它能夠主動發生改變,保存變量,再也不只是「只讀」狀態了。
在 Flutter 中還有 容器 Widget 和 渲染Widget 的區別,通常狀況下:
Text
、Slider
、ListTile
等都是屬於渲染 Widget
,其內部主要是 RenderObjectElement
,對應有 RenderObject
參數。
StatelessWidget
/ StatefulWidget
等屬於容器 Widget
,其內部使用的是 ComponentElement
, ComponentElement
自己是不存在 RenderObject
的。
因此做爲容器 Widget
, 獲取它們的 RenderObject
時,獲取到的是 build
後的樹結構裏,最上面渲染 Widget的 RenderObject
。
如上圖所示
findRenderObject
的實現,最終就是獲取renderObject
,在遇到ComponentElement
時,執行的是element.visitChildren(visit);
, 遞歸直到找到RenderObjectElement
,再返回它的renderObject
。
獲取 RenderObject
在 Flutter 裏很重要的,由於獲取控件的位置和大小等,都須要經過 RenderObject
獲取。
Flutter 中各種 RenderObject
的實現,大多都是顆粒度很細,功能很單一的存在 :
然而接觸過 Flutter 的同窗應該知道 Container
這個 Widget
,Container
的功能卻不顯單一,這是爲何呢?
以下圖,由於 Container
實際上是容器 Widget ,它只是把其餘「單一」的 Widget 作了二次封裝,而後經過配置參數來達到 「多功能的效果」 而已。
因此 Flutter 開發中,咱們常常會根據功能定義出各種如 Continer
、Scaffold
等腳手架模版,實現靈活與複用的界面開發。
迴歸到 RenderObject
,事實上 RenderObject
還屬於比較「低級」的階段,由於繪製到屏幕上咱們還須要座標體系和佈局協議等,因此 大部分 Widget
的 RenderObject
會是子類 RenderBox
(RenderSliver
例外) ,由於 RenderObject
自己只實現了基礎的 layout
和 paint
,而繪製到屏幕上,咱們須要的座標和大小等,這些內容是在 RenderBox
中開始實現。
RenderSliver
主要是在滾動控件中繼承使用。
好比控件被繪製在 x=10,y=20
的位置,而後大小由 parent
對它進行約束顯示,RenderBox
繼承了 RenderObject
,在其基礎上實現了 笛卡爾座標系
和佈局協議。
這裏咱們經過 Offstage
這個 Widget
,看下其 RenderBox
子類的實現邏輯, Offstage
是用於控制 child
是否顯示的做用,以下圖,能夠看到 RenderOffstage
對於 offstage
標誌位的內部邏輯:
那麼 Flutter 中的佈局協議是什麼呢?
簡單來講就是 child
和 parent
之間的大小應該怎麼顯示,由誰決定顯示區域。 相信從 Android 到接觸 Flutter 的同窗有這樣的疑惑, Flutter 中的 match_parent
和 wrap_content
邏輯須要怎麼設置?
就咱們從一個簡單的代碼分析,以下圖所示,Row
佈局咱們沒有設置任何大小,它是怎麼肯定自身大小的呢?
咱們翻閱源碼,能夠發現其實 Flutter 中經常使用的 Row
、Column
等其實都是 Flex
的子類,只是對 Flex
作了簡單默認配置。
那按照咱們前面的理解,看一個 Widget
的實現邏輯,就應該看它的 RenderObject
,而在 Flex
布對應的 RenderFlex
中,咱們能夠看到以下一段代碼:
能夠看到在佈局的時候,RenderFlex
首先要求 constraints != null
,Flex
佈局的上層中必須存在約束,否則確定會報錯。
以後,在佈局時,Row
佈局的 direction
是橫向的,因此 maxMainSize
爲上層佈局的最大寬度,而後根據咱們配置的 mainAxisSize
的參數:
mainAxisSize
爲 max
時,咱們 Row
的橫向佈局就是 maxMainSize
;mainAxisSize
爲 min
時,咱們 Row
的橫向佈局就是 allocatedSize
;前面 maxMainSize
咱們知道了是父佈局的最大寬度,而 allocatedSize
其實就是 child 的寬度之和。因此結果很明顯了:
對於 Row
來講, mainAxisSize
爲 max
時就是 match_parent
;mainAxisSize
爲 min
時就是 wrap_content
。
而高度 crossSize
,實際上是由 math.max(crossSize, _getCrossSize(child));
決定,也就是 child
中最高的一個做爲其高度。
最後小結一個知識點:
佈局通常都是由上層往下傳遞 Constraints
,而後由下往上返回 Size
。
那如何直接自定義 RenderObject
佈局?
拋開 Flutter 爲咱們封裝的好的,三大金剛 Widget
、Element
、RednerObject
一個很多,固然, Flutter 內置了不少封裝幫咱們節省代碼。
通常狀況下自定義 RenderObject
佈局:
MultiChildRenderObjectWidget
和 RenderBox
這兩個 abstract
類,實現本身的Widget
和 RenderObject
對象;MultiChildRenderObjectElement
關聯起它們;ContainerRenderObjectMixin
、 RenderBoxContainerDefaultsMixin
和 ContainerBoxParentData
等能夠幫你減小代碼量。總結起來, 對於 Flutter 而言,整個屏幕都是一塊畫布,咱們經過各類 Offset
和 Rect
肯定了位置,而後經過 Canvas
繪製上去,目標是整個屏幕區域,整個屏幕就是一幀,每次改變都是從新繪製。
這裏沒有介紹
RenderSliver
相關,它的輸入和輸出和Renderbox
又不大同樣,有機會咱們後面再詳細介紹。
InheritedWidget
是 Flutter 的靈魂設定之一。
InheritedWidget
共享的是 Widget
,只是這個 Widget
是一個 ProxyWidget
,它本身自己並不繪製什麼,但共享這個 Widget
內保存有的數據,從而到了共享狀態的目的。
以下圖所示,是 Flutter 中常見的 Theme
,其內部就是使用了 _InheritedTheme
這個 InheritedWidget
來實現主題的全局共享的。那麼 InheritedWidget
是如何實現全局共享的呢?
其實在 Element
的內部有一個 Map<Type, InheritedElement> _inheritedWidgets;
參數,_inheritedWidgets
通常狀況下是空的,只有當父控件是 InheritedWidget
或者自己是 InheritedWidget
時,它纔會被初始化,而當父控件是 InheritedWidget
時,這個 Map
會被一級一級往下傳遞與合併。
因此當咱們經過 context
調用 inheritFromWidgetOfExactType
時,就能夠經過這個 Map
往上查找,從而找到這個上級的 InheritedWidget
。(畢竟 context
is Element
)
如咱們的 Theme
/ThemeData
、Text
/DefaultTextStyle
、Slider
/ SliderTheme
等,以下代碼所示,咱們能夠定義全局的 ThemeData
或者局部的 DefaultTextStyle
,從而實現全局的自定義和局部的自定義共享等。
其實,Flutter 中大部分的狀態管理控件,其狀態共享方法,也是基於
InheritedWidget
去實現的。
前面咱們說過, Flutter 既然不依賴於原生控件,那麼如何集成一些平臺已有的控件呢?好比 WebView
和 Map
?
咱們這裏以 WebView
爲例子:
在官方 WebView
控件支持出來以前 ,第三方是直接在 FlutterView 上覆蓋了一個新的原生控件,利用 Dart 中的佔位控件傳遞位置和大小。
以下圖,在 Flutter 端 push
出一個 設定好位置和大小 的 SingleChildRenderObjectWidget
,從而獲得須要顯示的大小和位置,將這些信息經過 MethodChannel
傳遞到原生層,在原生層 addContentView
一個指定大小和位置的 WebView
。
這時候 WebView
和 SingleChildRenderObjectWidget
處於同樣的大小和位置,而空白部分則用 FLutter 的 Appbar
顯示。
這樣看起來就像是在 Flutter 中添加了 WebView
,但實際這脫離了 Flutter 的渲染樹,其中一個問題就是,當你跳轉 Flutter 其餘頁面的時候,會被 WebView
擋住;而且打開頁面的動畫,Appbar
和 WebView
難以保持一致。
後面 官方 WebView
控件支持出來後,這時候官方是利用 PlatformView
的設計,完成了不脫離 Flutter 渲染堆棧,也能集成平臺原生控件的功能。
以 Android 爲例,Android 上是利用了副屏顯示的底層邏輯,使用 VirtualDisplay
類,建立一個虛擬顯示器,須要調用 DisplayManager
的 createVirtualDisplay()
方法,將虛擬顯示器的內容渲染在一個內存的 Surface
上 ,生成一個惟一的 textureId
。
以下圖,以後渲染時將 textureId
傳遞給 Dart
層,渲染引擎會根據 textureId
, 獲取到內存裏已渲染數據,繪製到 AndroidView
上進行顯示。
Flutter 中比較有趣的狀況是,在 Dart 中的一些錯誤,並不會致使應用閃退,而是經過以下的紅色堆棧 UI ,錯誤區域不一樣,多是全屏紅,也可能局部紅,這種狀態就和傳統 APP 的「崩潰」狀態不大同樣了。
在開發過程當中這樣的顯示沒太大問題,但事實發佈線上版本就不合適了,因此咱們通常會選擇自定義錯誤顯示。
以下圖所示,通常咱們能夠經過以下處理,自定義咱們的錯誤頁面,而且收集錯誤信息。
重寫 ErrorWidget
的 builder
方法,而後將信息收集到 Zone
中,返回本身的自定義錯誤顯示,最後在 Zone
內利用 onError
統一處理錯誤。
ps 圖中的
Zone
等概念這裏就不展開了,有興趣的能夠去之前的文章詳細查看。
最後簡單說下 Flutter Web ,Flutter 在支持 Web 平臺上的優點在於 Flutter UI 與平臺的耦合度很低,而 Dart 起初就是爲了 Web 而生,一拍即合下 Flutter 支持 Web 並非什麼意外。
可是 Web 平臺就繞不過 JS ,在 Web 平臺,實際上 Image
控件最後會經過 dart2js 轉化爲 <img>
標籤並經過 src
賦值顯示。
同時,多了一個平臺就多了須要兼容的,目前 Flutter 的 issue 仍然很多,而 Web 支持雖然已經合併到主項目中,可是在兼容、性能等問題上還須要繼續優化,好比 Flutter Web 中 canvas.drawColor(Colors.black, BlendMode.clear);
是會出現運行錯誤的,由於不支持 BlendMode.clear
。
《全網最全Flutter與React Native深刻對比分析》