Flutter-你還在濫用StatefulWidget嗎

前言

對於萬物皆Widget的Fultter,一樣的事情通常都有多種控件能夠實現,太多的選擇老是會讓人陷入或多或少的選擇糾結症和對性能的憂慮上。java

初次接觸Flutter,首先必然要面對的兩座大山:StatelessWidget & StatefulWidget。 而在這兩個控件的選擇上,大部分人給出的解釋就是:"就像他們的名字同樣,無狀態靜態的視圖展現使用StatelessWidget,而有交互,須要動態變化的使用StatefulWidget."緩存

這樣的解釋正確,但過於模糊,彷佛StatelessWidget出現的地方都可以用StatefulWidget來代替,因而爲了後期可能的變化、爲了coding簡便,StatefulWidget被濫用變成了很容易發生的事情。性能優化

因此今天咱們就詳細聊一下StatefulWidget和StatelessWidget的區別和使用。bash

StatefulWidget與StatelessWidget區別

對於廣泛存在的模糊解釋,想吐槽又不能說它是錯的,但它確實產生了一些無解。框架

我我的對StatefulWidget與StatelessWidget理解:less

StatelessWidget初始化以後就沒法改變,若是想改變,那便須要從新建立,new另外一個StatelessWidget進行替換。但StatelessWidget由於是靜態的,他沒有辦法從新建立本身。因此StatefulWidget便提供了這樣的機制,經過調用setState((){})標記自身爲dirty狀態,以等待下一次系統的重繪檢查。ide

StatefulWidget 動態化代價

經過定義,StatefulWidget怎麼看都是一個萬金油的存在,可是,我指望你能對StatefulWidget動態化所付出的代價有所瞭解:佈局

在State類中的調用setState((){})更新視圖,將會觸發State.build! 也將間接的觸發其每一個子Widget的構造方法以及build方法。性能

這意味這什麼呢? 若是你的根佈局是一個StatefulWidget,那麼每在根State中調用一次setState((){}),都將是一次整頁全部Widget的rebuild!!! 舉個栗子:優化

class MyStatefulWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return CustomerState();
  }
}

class CustomerState extends State<MyStatefulWidget> {
  int _num = 0;

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Row(children: <Widget>[
      GestureDetector(
        onTap: () {
          setState(() {
            _num++;
          });
        },
        child: Text("Click My"),
      ),
      Text("1:AAAAA"),
      Text("2:BBBBB"),
      Text("3:C:" + _num.toString()),
      CustomerContainer()
    ]);
  }
}

class CustomerContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  	for (int i = 0; i < 1000000; i++) {
  		print("我是一個耗時操做 for:" + i.toString());
  	}
    return Container(
      child: Text("4:DDDD"),
    );
  }
}
複製代碼

對於上面的代碼,每一次點擊 」My Click「,CustomerState build方法,以及Row、Text、CustomerContainer等子Widget都將重建,暫時還不太肯定在繪製上Flutter是否會有緩存優化,但大量的對象建立與方法執行是跑不了的。若是某個子Widget的構造或build進行了較爲耗時的操做,那更是災難!!!

因此,你也應該能理解新建一個Flutter工程根佈局爲何是一個StatelessWidget了。

StatefulWidget是如何實現界面更新的?

setState(() {
	_num++;
});
複製代碼

在接觸一門新的技術時,舊技術所帶來的慣性思惟是很可怕的。

初次接觸像上面這樣setState的方法時,想固然的認爲State.setState((){})實現原理應該相似於 Android 的 DataBinding 或者 Vue 的數據劫持,實現觀察者模式並作定向更新,只局部更新綁定了 _num 的 Widget。

也正是由於抱着這樣的想法,對於大量使用StatefulWidget並無什麼心理負擔。

但上面的case已經很直白的告訴咱們,事實並非這樣!!!

咱們先看一下State.setState((){})源碼:

@protected
  void setState(VoidCallback fn) {
  	...
  	_element.markNeedsBuild();
  }
複製代碼

省略了全部的assert效驗,實際有意義的只有這一句,標記 element 爲須要 build 狀態。再往下看:

void markNeedsBuild() {
	...
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
}
複製代碼

標記 element 爲 dirty 狀態,並執行 owner 的 scheduleBuildFor 方法。owner 是 BuildOwner,看名字就知道是負責build的。

void scheduleBuildFor(Element element) {
	...
	if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    ...
}
複製代碼

onBuildScheduled() 中又調用了 ensureVisualUpdate() 而後 scheduleFrame(),直接看下 scheduleFrame

void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
	ui.window.scheduleFrame();
    _hasScheduledFrame = true;
  }
複製代碼

調用了Window類的scheduleFrame()方法,scheduleFrame()是一個native方法,實現真正的界面繪製,到這裏咱們就基本清楚咱們要知道的東西了。

Flutter並無實現數據雙向綁定,你在State.setState((){})中寫什麼代碼都不重要,它僅用來標記這個State對象須要從新Build,從新build後根據已變動的數據來建立新的Widget。

setState(() {
	_num++;
});
複製代碼
_num++;
setState(() {});
複製代碼

因此這兩種寫法均可以實現依賴_num的Widget更新。

開發中如何選擇StatefulWidget和StatelessWidget?

經過上面三個小結,你應該大體瞭解了StatefulWidget的視圖更新是如何簡單粗暴、且代價較高。

對比Vue(Vue經過雙向數據綁定實現局部DOM更新以提升效率),Flutter將本來由框架負責的一些性能優化轉嫁在了開發者身上。有一點相似於C++和java的內存回收。

既然反抗不了,就躺下來享受「自由」的快感吧。下面咱們聊聊如何在開發中選擇StatefulWidget和StatelessWidget來提升視圖更新性能。

先列一些決策點:

  • 優先使用 StatelessWidget
  • 含有大量子 Widget(如根佈局、次根佈局)慎用 StatefulWidget
  • 儘可能在葉子節點使用 StatefulWidget
  • 將會調用到setState((){}) 的代碼儘量的和要更新的視圖封裝在一個儘量小的模塊裏。
  • 若是一個Widget須要reBuild,那麼它的子節點、兄弟節點、兄弟節點的子節點應該儘量少

另外其餘須要注意的點

  • 相較Android的View,Flutter Widget的構造方法可能被會執行不少次,作的事情應該儘量的少
  • Flutter Widget build方法可能會執行屢次,作的事情應該儘量的少

假設你有如上一個Widget樹,紅色表示的是一個將會被改變的Widget。若是按照這樣的佈局結構,那麼每一次紅色的 leaf 節點發生變化並重建,它的四個兄弟節點也會從新建立,對於這樣的結構,你應該作這樣的優化:

將變化的節點下放封裝到一個更小的分支當中,使得它的兄弟節點儘量的少。

咱們用簡單的demo來講明:BBB是靜態文案、每點擊一次Click My, AAA後面的數字都會加1

class CustomerStatefulWidget extends StatefulWidget {
  final String _name;

  CustomerStatefulWidget(this._name);

  @override
  State<StatefulWidget> createState() {
    print("TAG, CustomerStatefulWidget:" + _name + " build");
    return CustomerState("CustomerStateA");
  }
}

class CustomerState extends State<CustomerStatefulWidget> {
  String _name;

  CustomerState(this._name) {
    print("TAG, CustomerState:" + _name + " 構造");
  }

  int _customerStatelessText = 0;

  @override
  Widget build(BuildContext context) {
    print("TAG, " + _name + " build");
    return Container(
      margin: EdgeInsets.only(top: 100),
      color: Colors.yellow,
      child: Column(
        children: <Widget>[
          CustomerStatelessWidget("BBB", "BBB"),
          CustomerStatelessWidget(
              "AAA", "AAA:" + _customerStatelessText.toString()),
          GestureDetector(
            onTap: () {
              print("Click My");
              setState(() {
                _customerStatelessText++;
              });
            },
            child: Text("Click My"),
          )
        ],
      ),
    );
  }
}

class CustomerStatelessWidget extends StatelessWidget {
  final String _text;
  final String _name;

  CustomerStatelessWidget(this._name, this._text) {
    print("TAG, CustomerStatelessWidget:" + _name + " 構造");
  }

  @override
  Widget build(BuildContext context) {
    print("TAG, CustomerStatelessWidget:" + _name + " build");
    if (_name == "BBB") {
//      for (int i = 0; i < 10000000; i++) {
//        print("for:" + i.toString());
//      }
      print("我是一個耗時方法,耗時2s");
    }
    return Text(_text);
  }
}

複製代碼

在咱們點擊Click My以後,看一下日誌:

I/flutter (31310): Click My
I/flutter (31310): TAG, CustomerStateA  build
I/flutter (31310): TAG, CustomerStatelessWidget:BBB  構造
I/flutter (31310): TAG, CustomerStatelessWidget:AAA  構造
I/flutter (31310): TAG, CustomerStatelessWidget:BBB  build
I/flutter (31310): 我是一個耗時方法,耗時2s
I/flutter (31310): TAG, CustomerStatelessWidget:AAA  build

複製代碼

本來靜態無需Rebuild的BBB,由於和AAA屬於兄弟節點,在AAA發生改變時被動重繪,更糟糕的是BBB還有一個很是耗時的build方法。那麼如何優化呢?

將ClickMy與AAA控件封裝在一個更小的StatefulWidget當中,BBB上提至StatelessWidget

class WrapStatelessWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("TAG, WrapStatelessWidget: build");
    return Container(
      margin: EdgeInsets.only(top: 100),
      color: Colors.yellow,
      child: Column(
        children: <Widget>[
          CustomerStatelessWidget("BBB", "BBB"),
          CustomerStatefulWidget("AAA")
        ],
      ),
    );
  }
}

class CustomerStatefulWidget extends StatefulWidget {
  final String _name;

  CustomerStatefulWidget(this._name);

  @override
  State<StatefulWidget> createState() {
    print("TAG, CustomerStatefulWidget:" + _name + " build");
    return CustomerState("CustomerStateA");
  }
}

class CustomerState extends State<CustomerStatefulWidget> {
  String _name;

  CustomerState(this._name) {
    print("TAG, " + _name + " 構造");
  }
  int _customerStatelessText = 0;

  @override
  Widget build(BuildContext context) {
    print("TAG, CustomerState:" + _name + " build");
    return Container(
      child: Column(
        children: <Widget>[
          CustomerStatelessWidget(
              "AAA", "AAA:" + _customerStatelessText.toString()),
          GestureDetector(
            onTap: () {
              print("Click My");
              _customerStatelessText++;
              setState(() {});
            },
            child: Text("Click My"),
          )
        ],
      ),
    );
  }
}

class CustomerStatelessWidget extends StatelessWidget {
  final String _text;
  final String _name;

  CustomerStatelessWidget(this._name, this._text) {
    print("TAG, CustomerStatelessWidget:" + _name + " 構造");
  }

  @override
  Widget build(BuildContext context) {
    print("TAG, CustomerStatelessWidget:" + _name + " build");
    if (_name == "BBB") {
//      for (int i = 0; i < 1000000; i++) {
//        print("for:" + i.toString());
//      }
      print("我是一個耗時方法,耗時2s");
    }
    return Text(_text);
  }
複製代碼

咱們再點一下ClickMy看下日誌:

I/flutter (31310): Click My
I/flutter (31310): TAG,CustomerStateA  build
I/flutter (31310): TAG, CustomerStatelessWidget:AAA  構造
I/flutter (31310): TAG, CustomerStatelessWidget:AAA  build
複製代碼

AAA的重繪不會再使得BBB被迫重繪!

結論

重申一下StatefulWidget使用的決策點:

  • 優先使用 StatelessWidget
  • 含有大量子 Widget(如根佈局、次根佈局)慎用 StatefulWidget
  • 儘可能在葉子節點使用 StatefulWidget
  • 將會調用到setState((){}) 的代碼儘量的和要更新的視圖封裝在一個儘量小的模塊裏。
  • 若是一個Widget須要reBuild,那麼它的子節點、兄弟節點、兄弟節點的子節點應該儘量少

另外其餘須要注意的點

  • 相較Android的View,Flutter Widget的構造方法可能被會執行不少次,作的事情應該儘量的少
  • Flutter Widget build方法可能會執行屢次,作的事情應該儘量的少

若是你的代碼存在大量的StatefulWidget,快去重構啦~

最後再補充一下:

Flutter固然不會放着大的漏洞無論。因此即便你的代碼真的形成了整顆WidgetTree在不停重建,有性能問題!但不致命。爲何呢?由於Flutter的視圖世界,有三棵樹!具體怎麼回事?咱們且聽下回分解!!

相關文章
相關標籤/搜索