[Flutter翻译]构建高性能的Flutter widget

1,286 阅读7分钟

原文地址:medium.com/flutter/bui…

原文作者:medium.com/@guidez

发布时间:2020年7月23日 - 7分钟阅读

本文是 Flutter Material 团队致力于使 Flutter Gallery 应用在网络上的性能更强之后开发的系列文章的一部分。然而,本文中的提示适用于所有Flutter应用程序。跳到结尾找到本系列的其他文章。

作者:Anthony Robledo & Pierre-Louis Guidez

所有无状态和有状态的widget都实现了build()方法,定义了它们的渲染方式。一个应用程序上的一个屏幕可以有数百甚至数千个widget,这些widget可能只被构建一次,或者如果有动画或某种交互,则被构建多次。这些widgets可能只被构建一次,如果有动画或某种交互,则会被多次构建。虽然在Flutter中构建widget的速度相对较快,但你必须对何时以及选择构建什么保持警惕。

这篇文章讲的是只构建你需要的东西,并且只在你需要的时候构建。然后,我们将分享我们如何使用这种方法来实现Flutter Gallery Web应用的性能显著提升。我们还将分享专业的技巧,告诉你如何诊断你的web应用中的类似问题。

只有在必要的时候才进行构建

一个重要的优化是只有在绝对必要的时候才构建widget。

谨慎地调用setState()

调用setState会安排调用build()方法。太频繁地这样做会减慢屏幕的性能。

考虑下面的动画,其中正面(黑屏)的显示被动画化为向下滑动以显示背面(棋盘),类似于bottom sheet的行为。前面的widget很简单,但后面的widget很忙。

前面的在繁忙的widget前平滑地播放动画

流畅的动画

Stack(
   children: [
     Back(),
     PositionedTransition(
       rect: RelativeRectTween(
         begin: RelativeRect.fromLTRB(0, 0, 0, 0),
         end: RelativeRect.fromLTRB(0, MediaQuery.of(context).size.height, 0, 0),
       ).animate(_animationController),
       child: Front(),
     )
   ],
 ),

你可能会想把父部件设置成如下的样子,但在这种情况下,这是错误的!

// 错误的代码
@override
void initState() {
 super.initState();
 _animationController = AnimationController(
   duration: Duration(seconds: 3),
   vsync: this,
 );
 _animationController.addListener(() {
   setState(() {
     // 当动画点击时重建。
   });
 });
}

这样性能差!为什么?

因为动画在做不必要的工作。

前面的widget在繁忙的widget上用jank动画化

抖动的动画

以下是有问题的代码。

// 错误的代码
_animationController.addListener(() {
 setState(() {
   // 当动画点击时重建。
 });
});
  • 当你需要对整个widget进行动画时,建议使用这种动画风格,但我们在这里做的不是这个。
  • 在动画监听器中调用setState()会导致整个Stack被重建,而这是没有必要的。
  • PositionedTransitionwidget已经是一个AnimatedWidget,所以当动画点击时,它会自动重建。
  • 调用setState()在这里其实是不需要的!

即使后面的小组件很忙,它也能以60 FPS的速度流畅地制作动画。关于明智地调用setState的更多信息,请参见Flutter Laggy Animations: How Not To setState


只构建必要的东西

除了只在需要的时候构建,你还想只构建UI中实际变化的部分。下面一节主要介绍如何创建执行力强的列表。

优先使用ListView.builder()

首先,让我们简单介绍一下显示列表的基础知识。

  1. 要垂直布局列表项,使用Column
  2. 如果列表应该是可滚动的,则使用ListView来代替。
  3. 如果列表中包含许多项目,使用ListView.builder构造函数,它可以在项目滚动到屏幕上时创建项目,而不是一次全部创建。这对于复杂的列表项和深层的小组件子树有明显的性能优势。 为了说明当你有大量列表项时,ListView.builderListView的好处,我们来看几个例子。 在DartPad中运行以下ListView示例。观察所有8个项目都被创建了。(点击左下角的Console显示控制台,然后点击Run。输出窗口没有滚动条,但你可以滚动内容并观察控制台,看看什么是创建的,什么时候建立的)。)
ListView(
  children: [
    _ListItem(index: 0),
    _ListItem(index: 1),
    _ListItem(index: 2),
    _ListItem(index: 3),
    _ListItem(index: 4),
    _ListItem(index: 5),
    _ListItem(index: 6),
    _ListItem(index: 7),
  ],
);

接下来,在DartPad中运行ListView.builder示例。请注意,只有可见的项目被创建。当你滚动时,它会创建(并构建)新的行。

ListView.builder(
  itemBuilder: (context, index) {
    return _ListItem(index: index);
  },
  itemCount: 8,
);

现在,在DartPad中运行这个例子,其中ListView的子代是提前创建的,在创建ListView本身的时候一次性创建完毕。在这种情况下,使用ListView构造函数更有效率。

final listItems = [
  _ListItem(index: 0),
  _ListItem(index: 1),
  _ListItem(index: 2),
  _ListItem(index: 3),
  _ListItem(index: 4),
  _ListItem(index: 5),
  _ListItem(index: 6),
  _ListItem(index: 7),
];
@override
Widget build(BuildContext context) {
  // This offers no benefit, it is actually more efficient to use the ListView constructor instead.
  return ListView.builder(
    itemBuilder: (context, index) {
      return listItems[index];
    },
    itemCount: 8,
  );
}

关于懒惰地构建列表的更多信息,请参见Slivers, Demystified


我们如何用一行代码将Flutter Gallery的网页渲染时间提高了2倍以上。

Flutter Gallery支持超过100个locales;这些locales使用--你猜对了--ListView.builder来列出。通过获取widget重建信息,我们注意到这些列表项在启动时被不必要地构建。这些项目的罪魁祸首并不明显,因为它们在两个级别的折叠菜单中:设置面板本身,以及locale扩展磁贴(事实证明,设置面板使用ScaleTransition被渲染为 "不可见",这意味着它非常被构建)。

Flutter图库设置面板与locale选项的扩展

Flutter图库设置面板,扩展了地域选项。

通过简单地将ListView.builderitemCount设置为0,对于非展开的设置类别,我们确保列表项只为展开的、可见的类别构建。解决了这个问题的单行PR将网络上的渲染时间提高了2倍以上。关键在于识别过度的小部件构建。

查看您的应用程序的小部件构建计数

虽然Flutter的构建效率很高,但有些情况下,过度构建会导致性能问题。有几种方法可以识别过度的widget重构。

通过使用Android Studio/IntelliJ

Android Studio和IntelliJ开发者可以使用内置的工具来显示Widget重建信息

通过修改框架

如果你使用不同的编辑器,或者想知道web的widget重建信息,你可以通过在框架中添加一些代码来实现。

输出示例:

RaisedButton 1
RawMaterialButton 2
ExpensiveWidget 538
Header 5

找到<Flutter path>/packages/flutter/lib/src/widgets/framework.dart。添加下面的代码,它在启动时统计widgets的构建次数,并在一定的持续时间(这里是10秒)后输出结果。

bool _outputScheduled = false;
Map<String, int> _outputMap = <String, int>{};
void _output(Widget widget) { 
  final String typeName = widget.runtimeType.toString();
  if (_outputMap.containsKey(typeName)) {
    _outputMap[typeName] = _outputMap[typeName] + 1;
  } else {
    _outputMap[typeName] = 1;
  }
  if (_outputScheduled) {
    return;
  }
  _outputScheduled = true;
  Timer(const Duration(seconds: 10), () {
    _outputMap.forEach((String key, int value) {
      switch (widget.runtimeType.toString()) {
        // Filter out widgets whose build counts we don't care about
        case 'InkWell':
        case 'RawGestureDetector':
        case 'FocusScope':
          break;
        default:
          print('$key $value');
      }
    });
  });
}

然后,修改StatelessElementStatefulElementbuild方法,调用_output(widget)

class StatelessElement extends ComponentElement {
  ...
@override
  Widget build() {
    final Widget w = widget.build(this);
    _output(w);
    return w;
   }
class StatefulElement extends ComponentElement {
...
@override
  Widget build() {
    final Widget w = _state.build(this);
    _output(w);
    return w;
  }

请看生成的framework.dart文件。

需要注意的是,多次重建并不一定说明有问题,但它可以通过验证不可见的小组件没有被构建来帮助调试性能问题。然而,它可以帮助调试性能问题,例如,通过验证不可见的部件没有被构建。

仅适用于web的提示:你可以添加一个resetOutput函数(可以从浏览器的开发工具中调用)来获取任何时间点的widget build counts。

import 'dart:js' as js;
 
void resetOutput() {
 _outputScheduled = false;
 _outputMap = <String, int>{};
}
void _output(Widget widget) {
  // Add this line
  js.context['resetOutput'] = resetOutput;
  ...

请看产生的framework.dart文件。

结束语

有效的性能调试需要了解引擎盖下发生了什么。这些提示可以帮助决定如何实现你的下一个构建方法,以便你的应用程序在所有场景下保持性能。

这篇文章是关于我们在提高Flutter Gallery的性能时学到的系列文章的一部分。创建高性能的Flutter web应用系列中的文章:

如果想了解更多的一般信息,关于UI性能的Flutter文档是所有级别的开发人员开始的好地方。

感谢Shams Zakhour编辑本文,感谢Ferhat Buyukkokten提供的调试技巧。


通过www.DeepL.com/Translator(免费版)翻译