首页 > 解决方案 > 如何防止 Flutter 中不必要的渲染?

问题描述

下面是一个展示我关注的最小应用程序。如果你运行它,你会看到每个可见项目的构建方法都会运行,即使只有一个项目的数据发生了变化。

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

void main() {
  runApp(MyApp());
}

class MyList extends StatelessWidget {
  final List<int> items;
  MyList(this.items);
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          print("item $index built");
          return ListTile(
            title: Text('${items[index]}'),
          );
        });
  }
}

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
   List<int> items = List<int>.generate(10000, (i) => i);

  void _incrementItem2() {
    setState(() {
      this.items[1]++;
    });
  }

  @override
  Widget build(BuildContext context) {
    final title = 'Long List';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Column(
          children: <Widget>[
            MyButton(_incrementItem2),
            Expanded(child: MyList(this.items))
          ],
        ),
      ),
    );
  }
}

class MyButton extends StatelessWidget {
  final Function incrementItem2;
  void handlePress() {
    incrementItem2();
  }

  MyButton(this.incrementItem2);
  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: ButtonStyle(
        foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
      ),
      onPressed: handlePress,
      child: Text('increment item 2'),
    );
  }
}

在 React / React Native 中,我会使用 shouldComponentUpdate 或 UseMemo 来防止不必要的渲染。我想在 Flutter 中做同样的事情。

这是 Flutter github 上关于同一主题的已关闭问题的链接:(我将在此处评论该问题的链接) https://github.com/flutter/flutter/issues/19421

在 Flutter 中避免无用的“重新渲染”或调用 build 方法的最佳实践是什么,或者考虑到 Flutter 和 React 之间的结构差异,这些额外的构建是否不是一个严重的问题?

这或多或少类似于几年前在堆栈溢出中提出的这个问题:How to prevent rerender of a widget based on custom logic in flutter?

一个赞成的答案是使用 ListView.builder。虽然这可以阻止一些即使不是大多数无用的渲染,但它并不能阻止所有无用的渲染,如我的示例所示。

提前感谢您的知识和洞察力。

标签: flutter

解决方案


我会争辩说,当您看到性能问题/ jankiness 时进行优化,但这是有可能的。在这样做之前不值得这样做。Flutter 团队为此做了一个简短的视频

请记住,虽然您可能会看到您可以使用print语句检测到的小部件上发生重建,但您并没有看到 Flutter 优化渲染是单独发生的。

Flutter 有一个小部件树、一个元素树和一个渲染对象树。

图片取自建筑概览

颤振构建流程

小部件树是我们想要构建的手动编码的 Dart 蓝图。(左侧树上只有蓝色项目。白色项目是底层:我们不直接写。)

元素树是 Flutter 对我们蓝图的解释,更详细地说明了实际使用的组件以及布局和渲染所需的内容。

renderObject 树将生成一个项目列表,其中包含其尺寸、层深度等需要光栅化到屏幕上的详细信息。

蓝图可能会被多次擦除和重写,但这并不意味着所有(甚至许多)底层组件都被销毁/重新创建/重新渲染/重新定位。未更改的组件将被重新使用。

假设我们正在建造一个包含水槽和浴缸的浴室。业主决定交换水槽和浴缸的位置。承包商不用大锤粉碎陶瓷,购买/运输并安装全新的水槽和浴缸。他只是分离每个并交换它们的位置。

这是一个人为的例子,使用AnimatedSwitcher

        child: AnimatedSwitcher(
          duration: Duration(milliseconds: 500),
            child: Container(
                key: key,
                color: color,
                padding: EdgeInsets.all(30),
                child: Text('Billy'))),

AnimatedSwitcher当他们改变时,应该交叉淡化它的孩子。(在这种情况下说从蓝色到绿色。)但如果key没有提供,AnimatedSwitcher则不会重建,也不会做交叉淡入淡出动画。

当 Flutter 遍历元素树并到达 时AnimatedSwitcher,它会检查它的子元素是否发生了明显的变化,即不同的树结构、不同的元素类型等,但它不会进行深度检查(我认为成本太高了)。它在相同的树位置(直接子级)中看到相同的对象 Type ( Container) 并保留AnimatedSwitcher而不重建它。结果是Container(当它重新构建时,因为它的颜色已经改变)从绿色翻转到蓝色,没有动画。

添加唯一的keytoContainer有助于 Flutter 在分析AnimatedSwitcher子树以进行优化时将它们识别为不同的。现在 Flutter 将AnimatedSwitcher在其 500 毫秒的持续时间内重建大约 30 次以直观地显示动画。(Flutter 的目标是在大多数设备上达到 60fps。)

AnimatedSwitcher这是复制/粘贴示例中使用的上述内容:

  • 在第一次加载时,盒子从绿色翻转到蓝色,没有动画(键没有改变)
  • 按下 AppBar 中的刷新按钮以重置页面,但键已更新为 blue Container,从而允许 Flutter 发现差异并为AnimatedSwitcher
import 'package:flutter/material.dart';

class AnimatedSwitcherPage extends StatefulWidget {
  @override
  _AnimatedSwitcherPageState createState() => _AnimatedSwitcherPageState();
}

class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
  Color color = Colors.lightGreenAccent;
  ValueKey key;

  @override
  void initState() {
    super.initState();
    key = ValueKey(color.value);

    Future.delayed(Duration(milliseconds: 1500), () => _update(useKey: false));
    // this initial color change happens WITHOUT key changing
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation Switcher'),
        actions: [
          IconButton(icon: Icon(Icons.refresh), onPressed: _restartWithKey)
        ],
      ),
      body: Center(
        child: AnimatedSwitcher(
          duration: Duration(milliseconds: 500),
            child: Container(
                key: key,
                color: color,
                padding: EdgeInsets.all(30),
                child: Text('Billy'))),
      ),
    );
  }

  void _update({bool useKey}) {
    setState(() {
      color = Colors.lightBlueAccent;
      if (useKey) key = ValueKey(color.value);
    });
  }

  void _restartWithKey() {
    setState(() {
      color = Colors.lightGreenAccent; // reset color to green
    });
    Future.delayed(Duration(milliseconds: 1500), () => _update(useKey: true));
    // this refresh button color change happens WITH a key change
  }
}

意见附录

问题
我知道除非键不同,否则它不会做动画,但如果它没有重建,那么颜色为什么/如何从绿色切换到蓝色?

回复
AnimatedSwitcher是一个StatefulWidget。它在构建之间保存前一个子小部件。当一个新的构建发生时,它会比较旧的孩子和新的孩子。(参见Widget.canUpdate()比较方法。)

如果新旧孩子不同/不同,它会执行一个过渡动画,用于 X 帧/滴答声。

如果旧孩子和新孩子不是不同的/不同的(根据.canUpdate()),AnimatedSwitcher基本上是一个不需要的包装器:它不会做任何特别的事情,但会进行重建以维护其最新的孩子(?)。我不得不承认这是一个糟糕的例子选择。


推荐阅读