【问题标题】:How to prevent unnecessary renders in Flutter?如何防止 Flutter 中不必要的渲染?
【发布时间】:2021-07-25 06:00:29
【问题描述】:

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

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。虽然这可以阻止一些即使不是大多数无用的渲染,但它并不能阻止所有无用的渲染,如我的示例所示。

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

【问题讨论】:

  • 使用提供者或集团
  • 检查 ValueListenableBuilder 文档(并参见 youtube.com/watch?v=s-ZG-jS5QHQ&t=6s)但老实说,我认为您对性能问题有点夸大了
  • 是的,到目前为止还没有遇到性能问题,但没有过分担心,@pskink,考虑到我考虑从 React Native 切换到 Flutter 的主要原因是性能。
  • ok 多少次(例如每秒)MyList 重建?另外,ListView.builder 中有多少可见项目?
  • MyList 的可见项(其中大约 14 个)只会在每次更改数据时重建一次。在 React Native 中,鉴于 js 桥,这是一个问题。很高兴确认这在 Flutter 中不是一个问题。

标签: flutter


【解决方案1】:

我会争辩说,当您看到性能问题/卡顿时进行优化,但这很可能。在这样做之前不值得这样做。 Flutter 团队做了一个short video about this

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

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

Image taken from Architectural Overview.

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

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

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

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

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

这是一个使用AnimatedSwitcher的人为示例:

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

AnimatedSwitcher 应该在其子项更改时交叉淡化。 (在这种情况下说从蓝色到绿色。)但是如果没有提供keyAnimatedSwitcher 将不会被重建并且不会做交叉淡入淡出动画。

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

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

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

  • 首次加载时,框从绿色变为蓝色,没有动画(键未更改)
  • 按下 AppBar 中的刷新按钮重置页面,但键已更新为蓝色 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
  }
}

意见附录

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

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

如果新旧子节点不同/不同,它会针对 X 帧/帧数执行过渡动画。

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

【讨论】:

  • 我知道除非键不同,否则它不会做动画,但如果它不重建,那么颜色为什么/如何从绿色切换到蓝色?
  • 是否有调试/分析方法可以让您知道元素渲染的频率?
  • @BenjaminLee 在 Android Studio 中,您可以点击 Flutter Performance/Widget Rebuild stats 并查看哪些 Widget 的重建频率。
  • @BenjaminLee 我将您的问题作为上述评论的附录回复了。
  • 是的,我意识到这是一个非常糟糕的例子,现在我查看了AnimatedSwitcher 工作原理的详细信息。我认为它仍然会定期重建,而不是昂贵的动画过渡。
【解决方案2】:

这不是一个正确的答案,更多的是我分享我在回答答案时发现的东西。

正如@Baker 所指出的,仅仅因为不必要地调用了构建方法并不一定意味着您会损失很多性能。

我使用示例运行 repaint rainbow,就像在 React (https://felixgerschau.com/react-rerender-components/) 中一样,仅仅因为组件/小部件被渲染/构建并不意味着它被重新绘制。

虽然 build 方法中的所有代码都会执行,但它确实必须经过 diffing 算法。

使用 BLOC 我能够防止不必要的渲染(左侧为 setState,右侧为 BLOC):

这是问题中关于 BLOC 实现的简单示例:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider<ListBloc>(
        create: (context) => ListBloc(),
        child: MyHomePage(),
      ),
    );
  }
}

class MyList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(itemBuilder: (context, index) {
      print("item $index built");
      return MyListTile(index);
    });
  }
}

class MyListTile extends StatelessWidget {
  final int index;
  MyListTile(this.index) : super(key: ValueKey(index));

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ListBloc, List<int>>(
      buildWhen: (previous, current) {
        return previous[index] != current[index];
      },
      builder: (context, state) {
        return ListTile(title: Text('${state[index]}'));
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  @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(), Expanded(child: MyList())],
        ),
      ),
    );
  }
}

class MyButton extends StatelessWidget {
  MyButton();
  @override
  Widget build(BuildContext context) {
    void handlePress() {
      BlocProvider.of<ListBloc>(context).add(ListEvent.incrementItem2);
    }

    return TextButton(
      style: ButtonStyle(
        foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
      ),
      onPressed: handlePress,
      child: Text('increment item 2'),
    );
  }
}

//BLOC
enum ListEvent { incrementItem2 }

class ListBloc extends Bloc<ListEvent, List<int>> {
  ListBloc() : super(List<int>.generate(10000, (i) => i));

  @override
  Stream<List<int>> mapEventToState(ListEvent event) async* {
    switch (event) {
      case ListEvent.incrementItem2:
        List<int> newState = [...state];
        newState[1]++;
        yield newState;
        break;
      default:
        break;
    }
  }
}

无法使用 Provider 来阻止每个项目的重建,因为notifyListeners() 将触发对所有消费者及其子项的重建。这是我的尝试:

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

void main() {
  runApp(
      ChangeNotifierProvider(create: (context) => ItemsNotifier(), child: MyApp()));
}

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

  @override
  Widget build(BuildContext context) {
    var items = Provider.of<ItemsNotifier>(context, listen: false);
    final title = 'Long List';

    void _incrementItem2() {
      items.incrementItem(2);
    }

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

class MyList extends StatelessWidget {
  final ItemsNotifier items;
  MyList(this.items);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          print("item $index built");
          return MyListItem(items[index],
              key: ValueKey(items[index].id)
          );
        });
  }
}

class MyListItem extends StatelessWidget {
  final MyItem item;
  MyListItem(this.item, {Key key}) : super(key: key);
  Widget build(BuildContext context) {
    print('item ${item.value} built');
    return Consumer<ItemsNotifier>(builder: (context, model, child) {
      return ListTile(title: Text('Item ${item.value}'));
    });
  }
}

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'),
    );
  }
}

class MyItem {
  int id;
  int value;
  MyItem({@required this.id, @required this.value});
  void incrementValue() {
    this.value = value + 1;
  }
}

class ItemsNotifier extends ChangeNotifier {
  final List<MyItem> _myItems =
      List<MyItem>.generate(1000, (i) => MyItem(id: i, value: i));

  List<MyItem> get all {
    return (List.unmodifiable(_myItems));
  }

  int get length => _myItems.length;

  MyItem operator [](int i) => _myItems[i];

  void incrementItem(int itemId) {
    MyItem item = _myItems.firstWhere((item) => item.id == itemId);
    item.incrementValue();
    notifyListeners();
  }
}

【讨论】:

    【解决方案3】:

    Flutter 的一个关键特性是每个 Widget 都是它自己的单元,并且可以在需要时决定重新构建。

    Bloc 库可以很好地控制要重建的内容。如果您的 ListTile 中的文本发生变化,您将拥有一个 MyListTile 类:

    class MyListTile extends StatelessWidget {
      final int index;
      MyListTile(this.index):super(key:ValueKey(index));
    
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<ItemBloc, ItemState>(
          buildWhen: (previous, current) {
            return previous.items[index] != current.items[index];
          },
          builder: (context, state) {
            return ListTile(title: Text('${state.items[index]}'));
          },
        );
      }
    }
    

    BlocBuilder 的 buildWhen 就像您习惯的 shouldComponentUpdate 一样,它为您提供了很多控制权。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-24
    • 1970-01-01
    • 2018-12-02
    • 2021-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-25
    相关资源
    最近更新 更多