flutter防止widget rebuild终极解决办法

服务器

浏览数:321

2019-9-7

背景

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

在widget前面加const

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

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

参考flutter文档
就是把那你的组件都定义成叶子,树的最底层,然后你在叶子组件内部更改状态,这样叶子之间互不影响,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的方法

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

library should_rebuild_widget;

import 'package:flutter/material.dart';

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

class ShouldRebuild<T extends Widget> extends StatefulWidget {
  final T child;
  final ShouldRebuildFunction<T> shouldRebuild;
  ShouldRebuild({@required this.child, this.shouldRebuild}):assert((){
    if(child == 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.child;
    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,
                child: 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,
                child:  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,因为常量是一直不会变的

 github:shouldRebuild

如果觉得帮助到了你,请star一下吧

作者:fantasy525