前面咱們提到過Flutter其實就是個Dart編寫的UI庫,附帶了本身的渲染引擎。咱們經過Widget
來描述
咱們的view,而後Flutter會用它的渲染引擎根據咱們的Widget
樹來繪製咱們的界面。注意,是根據Widget
樹來繪製界面,而不是直接繪製Widget
樹,這是一個很重要的概念,我們接下來慢慢來探討。android
咱們來看一張Flutter的架構圖:git
Flutter在咱們跟渲染引擎之間提供了好幾層抽象,咱們平常開發主要接觸到的就是那些個Widget
庫了,Rendering
作了一些渲染相關的抽象,而dart:ui
則是用Dart編寫的最後一層代碼,它實現了一些與底層的引擎交互的膠水代碼,咱們使用到的canvas API也是在這裏定義的。github
當咱們組合好咱們Widget
樹後,Flutter會從根節點向葉節點傳遞他們的約束或者說叫配置,約束限制了minHeight
,minWidth
,maxHeight
,maxWidth
等等。 好比Center
就向它的子Widget
傳遞居中的約束,當訪問到葉節點的時候,這時候Widget
樹上全部的Widget
都知道了它們的約束,這時候他們就能夠根據已有的約束本身肯定它們實際要佔有的大小跟位置,再一層層往上傳遞,只須要線性的時間複雜度,整個界面的上的元素繪製在哪一個像素上就都肯定下來了。canvas
這是我從谷歌找到的一張圖: 架構
那屏幕上繪製的既然不是咱們代碼裏寫的Widget
樹,那究竟是什麼呢?我以前也說過了Flutter裏面其實不僅有Widget
,還有其餘的對象類型,只不過咱們做爲開發者平常開發任務中關心的只有Widget
而已,因此Everything is Widget
這句話也不能算錯。咱們這裏要提到的其餘對象類型就是RenderObject
,這個類雖然也暴露給咱們了,可是基本上只在Flutter框架內部使用,咱們日常開發大多數不會碰到的。從名字能夠猜到它們跟渲染相關,確實,RenderObject
在Flutter裏面負責實際在屏幕上的繪製,而且每個Widget
都有一個對應的RenderObject
,也就是說,除了Widget
樹,咱們還會有一個RenderObject
樹。app
咱們平時編寫的Dart代碼,組合的那些Widget
,其實就是給RenderObject
提供了草圖,提供了對UI的描述信息,而後RenderObject
根據這些信息去繪製咱們的界面。RenderObject
有一些方法諸如performLayout
,paint
,這些方法負責在屏幕上繪製,咱們使用的Widget
的概念爲咱們在RenderObject
上提供了很好的抽象,咱們只須要聲明咱們想要什麼東西就行了。那有些同窗可能會想,其實咱們也能夠拋開Widget
去直接繪製的呀?大部分人應該都不肯意直接跟底層繪製打交道,那樣就要本身計算每一個像素應該繪製的位置,工做量會大大增長,就像咱們以前開發android app不會全部的界面都用OpenGL去繪製同樣,而是使用各類View、ViewGroup,Widget
跟View同樣是框架提供給咱們的編寫界面的抽象。框架
RenderObject
幹了什麼?本質上,RenderObject
是沒有任何狀態的,它也不包含任何業務邏輯,它們只知道一點點關於它們父RenderObject
的信息,同時還有訪問它們子RenderObject
的能力。在整個app的層面上它們不會互相協做,也不能幫別人作決定,只會按照順序在屏幕上繪製。less
widget
在他們的build
方法裏面會返回其它Widget
,致使Widget
樹愈來愈龐大。在樹的最下端最底下會遇到一個或多個RenderObjectWidget
,就是這個類幫整個Widget
樹建立了RenderObject
。dom
咱們前面提到過Widget
拿到本身的約束後會決定本身的大小,其實這些約束拿到了以後是給了本身對應的RenderObject
,它們會根據約束決定Widget
在屏幕上的真實的物理大小。不一樣的RenderObject
決定大小的方式也不一樣,主要就三大類:ide
Center
對應的RenderObject
Widget
保持同樣大,好比Opacity
對應的RenderObject
Image
對應的RenderObject
關於Flutter自帶的RenderObject
就這三點比較重要,通常咱們也不會去自定義RenderObject
。
Element
再來看看咱們開頭那張Flutter架構圖。咱們Widget
層抽象出了一個Widget
樹,咱們dart:ui
負責實際繪製,抽象出了一個RenderObject
樹,中間的一層Rendering
幹了啥?它其實也抽象出來了一個樹:Element
樹。
當一個Widget
地build方法被調用時,Flutter會調用Widget.createElement(this)
建立一個Element
,這個Widget
就是此Element
一開始的配置,這個Element
會持有它的引用。值得一提的是咱們的StatefulWidget
關聯的State
對象其實也是由Element
管理的,由於State
通常都存活的比較長,widget
卻可能頻繁build
。對應的,Element
跟Widget
就有一個顯著的不一樣,它會更新,當build
方法再被調用時,它會更新它的引用指向新的Widget
。咱們以前說過了在屏幕繪製的不是Widget
樹,如今能夠說繪製的究竟是什麼東西了,是Element
樹。Element
樹表明着app的實際結構,是app的骨架,是實際繪製在屏幕上的東西。Element
會經過引用查詢Widget
攜帶的信息,在一系列的判斷後交給RenderObject
去繪製。(主要判斷有木有修改,要不要重繪)
如今就很明朗了:
Element
持有Widget
跟RenderObject
的引用,RederObject
負責把上層描述轉換成能夠跟底層渲染引擎通訊的東西,而Element
則是Widget
跟RenderObject
之間的膠水代碼。
那到底爲何要設計出這三層呢,直接繪製很差嗎?爲何要增長這樣的複雜度呢?咱們知道Flutter是一個響應式的框架,全部的Widget
也都是immutable的,任何修改都會致使從新build
,也就是會從新構建它的Widget
樹,一個app天天build
界面幾百萬次不過度吧?而RenderObject
是開銷比較大的對象,由於負責底層的繪製,比較expensive,這樣它也頻繁地銷燬重建的話確定會影響性能,大多數時候界面上僅有一小部分被修改,好比在一個動畫中,一幀可能就改變一點點,可能只改個某部分的顏色,其它的都不變,那麼隨便咱們的Widget
樹怎麼變,咱們的app骨架也就是咱們的Element
樹結構徹底不須要從新構建,只須要把改變的那部分從新繪製就行了。Widget
只是配置文件,比較輕量,想怎麼變你就怎麼變,咱們實際繪製在屏幕上的是Element
,只要想辦法判斷它指向的Widget
有沒有改變就行了,變了就從新繪製,沒變就無論,這樣雖然咱們可能頻繁地經過setState
之類的手段去頻繁通知重繪,Widget
樹也頻繁地從新build
,Flutter的性能並不會受到影響。咱們在享受了immutable帶給個人便利的同時也複用了那些個實際在屏幕上作繪製的對象。
以前咱們說過build
方法被調用後Element
會更新引用,而後判斷要不要重繪。具體的判斷標準就是運行時類型有木有改變,或者說若是一個Widget
有key的話,key有木有變等等。這麼說聽起來也有點抽象,咱們就來實際寫一點代碼來感覺一下Flutter的這個機制。
仍是用昨天的那個app爲例,此次咱們但願咱們點擊重置那個FAB的時候,能夠交換加減兩個按鈕的位置。可能你們沒看我以前的文章,有的人還不熟悉Flutter開發,我這裏先帶你們定義一個按鈕叫作FancyButton
,看完你們就知道Flutter代碼怎麼寫了:
class FancyButton extends StatefulWidget {
final Widget child;
final VoidCallback callback;
const FancyButton({Key key, this.child, this.callback}) : super(key: key);
@override
FancyButtonState createState() {
return FancyButtonState();
}
}
複製代碼
由於它是一個StatefulWidget
,它的核心邏輯都在它對應的State
裏面,StatelessWidget
更簡單,它包含了一個相似的build
方法,這裏就不帶你們寫了,後面直接看源代碼就行了:
class FancyButtonState extends State<FancyButton> {
@override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
color: _getColors(),
child: widget.child,
onPressed: widget.callback,
),
);
}
Color _getColors() {
return _buttonColors.putIfAbsent(this, () => colors[next(0, 5)]);
}
}
Map<FancyButtonState, Color> _buttonColors = {};
final _random = Random();
int next(int min, int max) => min + _random.nextInt(max - min);
List<Color> colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.amber,
Colors.lightBlue,
];
複製代碼
其實咱們也只是包裝了RaisedButton
並提供了顏色而已,其它的仍是要上游去配置的。
接下來,咱們就能夠把這按鈕添加到主頁面去了:
@override Widget build(BuildContext context) {
final incrementButton =
FancyButton(child: Text("增長"), callback: _incrementCounter);
final decrementButton =
FancyButton(child: Text("減小"), callback: _decrementCounter);
List<Widget> _buttons = [incrementButton, decrementButton];
if (_reversed) {
_buttons = _buttons.reversed.toList();
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.0),
color: Colors.green.withOpacity(0.3)),
child: Image.asset("qrcode.jpg"),
margin: EdgeInsets.all(4.0),
padding: EdgeInsets.only(bottom: 4.0),
),
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buttons),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _resetCounter,
tooltip: 'Increment',
child: Icon(Icons.refresh),
),
);
}
複製代碼
其中交換按鈕位置的邏輯就很簡單了:
void _swap() {
setState(() {
_reversed = !_reversed;
});
}
複製代碼
好,能夠運行代碼了。
一切都如咱們指望的那樣,按鈕交換過來了而且點擊事件也都正常...等等!怎麼按鈕的顏色沒動!
這就是咱們前面提到的判斷邏輯,複用機制了!原來,當從新build
的時候,Element
仍是指向它原來位置對應的Widget
,咱們的Widget
並無key,那它只根據運行時類型來判斷是否有改變,咱們這兒倆個類型都是同樣的,都是FancyButton
,咱們原本指望Flutter能發現兩個按鈕的顏色不同從而去從新繪製。可是顏色是在State
裏面定義的,State
並無被銷燬,所以只根據運行時類型Element
最終會認爲沒有修改,因此咱們看到顏色沒有更新,那爲何文字跟點擊事件變了呢,那是由於這倆是從外部傳遞過來的,外部從新建立了呀。解決這個問題也很簡單,咱們只要根據規則給這兩個按鈕加上key就行了,這樣Flutter根據key就知道咱們的Widget
不同了:
List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()];
...
final incrementButton = FancyButton(
key: _buttonKeys.first, child: Text("增長"), callback: _incrementCounter);
final decrementButton = FancyButton(
key: _buttonKeys.last, child: Text("減小"), callback: _decrementCounter);
複製代碼
Key
的類型有好幾種,不過不是今天的重點咱們暫且不討論。這下Flutter不再會認爲沒有改變啦,再次運行項目,這下按鈕切換的同時背景色也會跟着改變了。
好啦,到了這兒,Flutter的基本工做流程咱們算是搞明白了,怪不得它頻繁build卻不卡頓!想深刻了解的朋友們也能夠看看Flutter團隊的這個視頻:Flutter渲染過程。今天的信息量確實很大,好在咱們平常開發不用直接跟它們打交道。你們也不用強迫本身一會兒明白,尤爲是剛入門的朋友們,不要急,雖然懂得原理會幫助咱們處理一些問題,目前知道有這麼個東西有個印象就好,時間長了天然就懂啦。
代碼地址:counter