乐趣区

flutter防止widget-reebuild终极解决办法

背景

众所周知,flutter 是借鉴了前端框架 React 的思想而开发的框架,有很多相似之处,也有看不到的不一样,我目前感受最深的就是 flutter 无所不在的 rebuild,那么有办法阻止 rebuild 吗?

在 widget 前面加 const

这个办法确实可以,一劳永逸,但是你一旦加了 const, 你这个 widget 就永远不会更新了,除非你是在写静态页面,否则你最好不要用它

把你的组价写成“叶子 ” 组件

就是把那你的组件都定义成叶子,树的最底层,然后你在叶子组件内部更改状态,这样叶子之间互不影响,emm, 在我看来这样子跟 react 的状态提升的思想相反了,因为你为了互不影响,你不能把状态放到根节点,放到根节点,一调用 setState 那全部自组价就 rebuild 了,我一开始一直是用这个思路来解决 rebuild 的问题的,
比如使用 StreamBuilder 这个可以包裹你的组件,然后用流来触发 StreamBuilder 内部 rebuild, 通过 StreamBuilder 来隔绝外面的组件,这样写有个小缺点,我要额外写个流,还要关闭流,很啰嗦。

使用其他的库,比如 Provider

这些库的实现方法跟 StreamBuilder 差不多,都是通过一个 Widget 来隔绝其他 Widget, 让更新限制在内部,但是都有一个共同点,你要配合额外的外部变量去触发内部的更新

终极办法

用过 react 的人都知道,react 的类组件有个很重要的生命周期叫shouldComponentUpdate ,我们可以在组件内部重写这个声明周期来进行性能优化。

如何优化呢,就是对比组件的新旧 props 的属性的值是否一致,如果一致那组件就没必要更新.
那 flutter 有没有类似的生命周期呢?没有!

flutter 团队认为 flutter 的渲染速度已经够快了,并且 flutter 实际也有类似 react 的 diff 算法来对比 element 是否需要更新,他们做了优化和缓存,因为更新 flutter 的 element 是很昂贵的操作,而 rebuild Widget 只是重新 new 了一个 widget 的实例,就像只是执行了一段 dart 代码一样,没涉及到任何 ui 层的更改,而且他们也对新旧 widget 做了 diff, 通过 diff widget 来减少对 element 层的更改,不管怎样,只要没有导致 element 销毁,重建,一般不会影响什么性能。

但是通过谷歌和百度你还是能发现有人在搜索如何防止 rebuild, 这说明了市场还是有需求的。我个人认为,这个不叫过度优化,其实是有这个场景需要优化的,比如谷歌推荐的状态管理库 Provider 就提供了如何减少不必要的 rebuild 的方法

话 (我) 不(想)多 (吐) 说(槽)了:

import 'package:flutter/material.dart';

typedef WidgetBuilder<T> = T Function();

typedef ShouldRebuildFunction<T> = bool Function(T oldWidget, T newWidget);

class ShouldRebuild<T extends Widget> extends StatefulWidget {
  final WidgetBuilder<T> builder;
  final ShouldRebuildFunction<T> shouldRebuild;
  ShouldRebuild({@required this.builder, this.shouldRebuild}):assert((){if(builder == null){
      throw FlutterError.fromParts(
      <DiagnosticsNode>[ErrorSummary('ShouldRebuild widget: builder must be not  null')]
      );
    }
    return true;
  }());
  @override
  _ShouldRebuildState createState() => _ShouldRebuildState<T>();
}

class _ShouldRebuildState<T extends Widget> extends State<ShouldRebuild> {
  @override
  ShouldRebuild<T> get widget => super.widget;
  T oldWidget;
  @override
  Widget build(BuildContext context) {final T newWidget = widget.builder();
    if (this.oldWidget == null || (widget.shouldRebuild == null ? true : widget.shouldRebuild(oldWidget, newWidget))) {this.oldWidget = newWidget;}
    return oldWidget;
  }
}

就是这几行代码,不到 40 行代码
来看测试代码:

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:should_rebuild_widget/should_rebuild_widget.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue,),
      home: Test(),);
  }
}

class Test extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<Test> {
  int productNum = 0;
  int counter = 0;

  _incrementCounter(){setState(() {++counter;});
  }
  _incrementProduct(){setState(() {++productNum;});
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(constraints: BoxConstraints.expand(),
          child: Column(
            children: <Widget>[
              ShouldRebuild<Counter>(shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                builder: () => Counter(counter: counter,onClick: _incrementCounter,title: '我是优化过的 Counter',),
              ),
              Counter(counter: counter,onClick: _incrementCounter,title: '我是未优化过的 Counter',),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'),
              )
            ],
          ),
        ),
      ),
    );
  }
}



class Counter extends StatelessWidget {
  final VoidCallback onClick;
  final int counter;
  final String title;
  Counter({this.counter,this.onClick,this.title});
  @override
  Widget build(BuildContext context) {Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
    return AnimatedContainer(duration: Duration(milliseconds: 500),
      color:color,
      height: 150,
      child:Column(
        children: <Widget>[Text(title,style: TextStyle(fontSize: 30),),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[Text('counter = ${this.counter}',style: TextStyle(fontSize: 43,color: Colors.white),),
            ],
          ),
          RaisedButton(
            color: color,
            textColor: Colors.white,
            elevation: 20,
            onPressed: onClick,
            child: Text('increment Counter'),
          ),
        ],
      ),
    );
  }
}


布局效果图:

  • 我们定义了一个 Counter 组件,Counter 在 build 的过程中会改变自己的背景色,每次执行 build 都会随机生成背景色,以便我们观察组件是否 build。另外 Counter 接收父组件传过来的值 counter, 并展示,还接收一个 title, 来区分不同的 Counter 名字
  • 看这里的代码
           Column(
            children: <Widget>[
              ShouldRebuild<Counter>(shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                builder: () => Counter(counter: counter,onClick: _incrementCounter,title: '我是优化过的 Counter',),
              ),
              Counter(counter: counter,onClick: _incrementCounter,title: '我是未优化过的 Counter',),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'),
              )
            ],
          )

我们上面的 Counter 被 ShouldRebuild 包裹,同时 shouldRebuild 参数传入了自定义的条件当这个 Counter 接收的 counter 不一致时才 rebuild, 如果新老 Counter 对比发现 counter 一致那就不 rebuild,
而下面的 Counter 则没有做优化。

  • 我们点击增加 Product 的按钮 increment Product ,会触发增加 productNum, 而此时没有增加 counter, 所以被 ShouldRebuild 包裹的 Counter 并没有 rebuild, 而下面没有包裹的 Counter 就 rebuild 了

来看下 gif:

原理揭秘

其实原理跟用 const 声明的 widget 一致,来看下 flutter 源码

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
      if (child.widget == newWidget) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }

...
}

摘抄其中一部分,
第一个

if (child.widget == newWidget) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
   }

这里是关键,flutter 发现 child.widget 也就是老的 widget 和新的 widget 是同一个,引用一致的话就直接返回了 child

如果发现不一致就走了这里

if (Widget.canUpdate(child.widget, newWidget)) {if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        return child;
      }

这里如果可以更新,就会走 child.update(), 这个方法一旦走了,那 build 方法肯定会执行了。
请看它做了什么事

@override
  void update(StatelessWidget newWidget) {super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();}

看到 rebuild()就知道一定去执行 build 了。

其实看到 if (child.widget == newWidget) 我们也知道为什么 const Text()会让 Text 不会重复 build, 因为常量是一直不会变的

退出移动版