Flutter-你還在濫用StatefulWidget嗎

暴打小女孩發表於2019-02-03

前言

對於萬物皆Widget的Fultter,同樣的事情一般都有多種控制元件可以實現,太多的選擇總是會讓人陷入或多或少的選擇糾結症和對效能的憂慮上。

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

這樣的解釋正確,但過於模糊,似乎StatelessWidget出現的地方均可以用StatefulWidget來代替,於是為了後期可能的變化、為了coding簡便,StatefulWidget被濫用變成了很容易發生的事情。

所以今天我們就詳細聊一下StatefulWidget和StatelessWidget的區別和使用。

StatefulWidget與StatelessWidget區別

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

我個人對StatefulWidget與StatelessWidget理解:

StatelessWidget初始化之後就無法改變,如果想改變,那便需要重新建立,new另一個StatelessWidget進行替換。但StatelessWidget因為是靜態的,他沒有辦法重新建立自己。所以StatefulWidget便提供了這樣的機制,通過呼叫setState((){})標記自身為dirty狀態,以等待下一次系統的重繪檢查。

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方法可能會執行多次,做的事情應該儘可能的少

Flutter-你還在濫用StatefulWidget嗎

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

Flutter-你還在濫用StatefulWidget嗎

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

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

Flutter-你還在濫用StatefulWidget嗎

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的檢視世界,有三棵樹!具體怎麼回事?我們且聽下回分解!!

相關文章