原來你是這樣的Flutter

前面咱們提到過Flutter其實就是個Dart編寫的UI庫,附帶了本身的渲染引擎。咱們經過Widget描述咱們的view,而後Flutter會用它的渲染引擎根據咱們的Widget樹來繪製咱們的界面。注意,是根據Widget樹來繪製界面,而不是直接繪製Widget樹,這是一個很重要的概念,我們接下來慢慢來探討。android

繪製的究竟是什麼?

咱們來看一張Flutter的架構圖:git

Flutter架構圖

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有一些方法諸如performLayoutpaint,這些方法負責在屏幕上繪製,咱們使用的Widget的概念爲咱們在RenderObject上提供了很好的抽象,咱們只須要聲明咱們想要什麼東西就行了。那有些同窗可能會想,其實咱們也能夠拋開Widget去直接繪製的呀?大部分人應該都不肯意直接跟底層繪製打交道,那樣就要本身計算每一個像素應該繪製的位置,工做量會大大增長,就像咱們以前開發android app不會全部的界面都用OpenGL去繪製同樣,而是使用各類View、ViewGroup,Widget跟View同樣是框架提供給咱們的編寫界面的抽象。框架

RenderObject幹了什麼?

本質上,RenderObject是沒有任何狀態的,它也不包含任何業務邏輯,它們只知道一點點關於它們父RenderObject的信息,同時還有訪問它們子RenderObject的能力。在整個app的層面上它們不會互相協做,也不能幫別人作決定,只會按照順序在屏幕上繪製。less

widget在他們的build方法裏面會返回其它Widget,致使Widget樹愈來愈龐大。在樹的最下端最底下會遇到一個或多個RenderObjectWidget,就是這個類幫整個Widget樹建立了RenderObjectdom

咱們前面提到過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。對應的,ElementWidget就有一個顯著的不一樣,它會更新,當build方法再被調用時,它會更新它的引用指向新的Widget。咱們以前說過了在屏幕繪製的不是Widget樹,如今能夠說繪製的究竟是什麼東西了,是Element樹。Element樹表明着app的實際結構,是app的骨架,是實際繪製在屏幕上的東西。Element會經過引用查詢Widget攜帶的信息,在一系列的判斷後交給RenderObject去繪製。(主要判斷有木有修改,要不要重繪)

如今就很明朗了:

最終關係圖

Element持有WidgetRenderObject的引用,RederObject負責把上層描述轉換成能夠跟底層渲染引擎通訊的東西,而Element則是WidgetRenderObject之間的膠水代碼。

爲何有三兄弟?

那到底爲何要設計出這三層呢,直接繪製很差嗎?爲何要增長這樣的複雜度呢?咱們知道Flutter是一個響應式的框架,全部的Widget也都是immutable的,任何修改都會致使從新build,也就是會從新構建它的Widget樹,一個app天天build界面幾百萬次不過度吧?而RenderObject是開銷比較大的對象,由於負責底層的繪製,比較expensive,這樣它也頻繁地銷燬重建的話確定會影響性能,大多數時候界面上僅有一小部分被修改,好比在一個動畫中,一幀可能就改變一點點,可能只改個某部分的顏色,其它的都不變,那麼隨便咱們的Widget樹怎麼變,咱們的app骨架也就是咱們的Element樹結構徹底不須要從新構建,只須要把改變的那部分從新繪製就行了。Widget只是配置文件,比較輕量,想怎麼變你就怎麼變,咱們實際繪製在屏幕上的是Element,只要想辦法判斷它指向的Widget有沒有改變就行了,變了就從新繪製,沒變就無論,這樣雖然咱們可能頻繁地經過setState之類的手段去頻繁通知重繪,Widget樹也頻繁地從新build,Flutter的性能並不會受到影響。咱們在享受了immutable帶給個人便利的同時也複用了那些個實際在屏幕上作繪製的對象。

Flutter的複用機制

以前咱們說過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

關注我,一塊兒學習Flutter吧!
相關文章
相關標籤/搜索