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的视图世界,有三棵树!具体怎么回事?咱们且听下回分解!!

相关文章
相关标签/搜索