Flutter 性能优化系列之打造高性能 widget

Stella981
• 阅读 707

本文是 Flutter 性能优化系列文章之一,记录了 Flutter 团队优化 Flutter Gallery 的实践。本文主要介绍了如何打造高性能的 widget。原文链接:https://medium.com/flutter/building-performant-flutter-widgets-3b2558aa08fa

所有无状态和有状态 widget 都会实现 build() 方法,这个方法决定了它们是如何渲染的。app 中的一屏就可能有成百上千个部件,这些部件可能只会构建一次,或者在有动画或者某种特定的交互情况下,也有可能构建多次。如果想构建快速的 widget,你一定要很谨慎地选择构建哪些 widget,以及在什么时候构建。

这篇文章主要讨论只构建必要的和只在必要时构建,然后会分享我们是如何使用这个办法来显著提高 Flutter Gallery 的性能。我们还会分享一些高级技巧用于诊断你的 web app 中类似的问题。

只在必要时构建—

一个重要的优化方法是,只在绝对必要时才构建 widget。

谨慎地调用 setState()

调用 setState 方法会引起 build() 方法调用。如果调用太多次,会使性能变慢。

看一下下面的动画,显示在前面的黑色 widget 向下滑动,露出后面类似棋盘的面板,类似于 bottom sheet[1] 的行为。前面黑色 widget 很简单,但是后面的 widget 很忙碌。

Flutter 性能优化系列之打造高性能 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(),     )   ], ),

你可能会像以下这样写父 widget,但在这个场景下,这样是错误的:

// BAD CODE@overridevoid initState() {  super.initState();  _animationController = AnimationController(    duration: Duration(seconds: 3),    vsync: this,  );  _animationController.addListener(() {    setState(() {      // Rebuild when animation ticks    });  });}

这样性能并不好。为什么?因为动画在做不必要的工作。

Flutter 性能优化系列之打造高性能 widget

以下是有问题的代码:

// BAD CODE_animationController.addListener(() {  setState(() {    // Rebuild when animation ticks.  });});
  • 这种类型的动画只在你需要让整个 widget 动起来时才推荐使用,但这并不是我们在这种布局中需要的。

  • 在动画监听器中调用 setState() 会引起整个 Stack 重新构建,这是完全没必要的

  • PositionedTransition 部件已经一个 AnimatedWidget 了,所以它会在动画开始的时候自动重新构建

  • 不需要在这里调用 setState()

Flutter 性能优化系列之打造高性能 widget

即使后面的组件是很忙碌的,前面的组件动画也可以达到 60 FPS。更多有关合理地调用 setState 方法的内容,请看 Flutter 卡顿的动画:你不该这样 setState[2]

只构建必要的部分—

除了只在必要的时候进行构建,你还需要只构建 UI 中变化的部分。接下来的章节主要关注如何创建一个高性能的 list。

优先使用 ListView.builder()

首先,让我们简单地看看显示 list 的基础:

  • 竖 list 使用 Column

  • 如果 list 需要滚动,使用 ListView

  • 如果 list 有很多 item,使用 ListView.builder,这个方法会在 item 滚动进入屏幕的时候才创建 item,而不是一次性创建所有的 item。这在 list 很复杂和 widget 嵌套很深的情况下,有明显的性能优势。

为了解释多 item 情况下 ListView.builder 相较于 ListView 的优势,我们来看几个例子。

在这个 DartPad 例子[3]中运行以下 ListView。你可以看到 8 个 item 都创建好了。(点击左下角的 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 例子[4]中运行 ListView.builder。你可以看只有可见的 item 被创建了,当你滚动时,新的 item 才被创建。

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

现在,运行这个例子[5]。在这里例子中,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),];@overrideWidget build(BuildContext context) {  // 这种情况下 ListView.builder 并不会有性能上的好处  return ListView.builder(    itemBuilder: (context, index) {      return listItems[index];    },    itemCount: 8,  );}

更多有关延迟构建 list 的内容,请看 Slivers, Demystified[6]。

怎样通过一行代码,提升超过两倍的性能—

Flutter Gallery[7] 支持超过 100 个地区;这些地区,可能你也猜到了,是通过 ListView.builder() 来展示的。通过查看 widget 重新构建的次数,我们注意到这些 item 会在启动时进行不必要的构建。这个情况有点难发现,因为这些 item 藏在折叠了两层的菜单下:设置面板和地区列表。(后来我们发现,因为使用了 ScaleTransitioin ,设置面板在不可见状态下也会进行渲染,意味着它会不断地被构建)。

Flutter 性能优化系列之打造高性能 widget

通过简单地将 ListView.builderitemCount 在未展开状态下设置为 0,我们确保了 item 只会在展开的、可见的设置面板中才进行构建。这一行改动提高了在 web 环境下渲染时间将近两倍,其中的关键是定位过度的 widget 构建。

如何查看 widget 的构建次数—

虽然 Flutter 的构建是很高效的,但是也会出现过度构建导致性能问题的情况。有几种方法可以帮助定位过度的 widget 构建:

使用 Android Studio/IntelliJ

Android Studio 和 IntelliJ 开发者可以使用自带的工具来查看 widget 重新构建信息[8]。

修改 Flutter 框架本身

如果使用的不是以上的编辑器,或者希望可以知道 web 环境下 widget 的重新构建次数,你可以在 Flutter 框架中加入几行简单的代码。

先看一下输出效果:

RaisedButton 1RawMaterialButton 2ExpensiveWidget 538Header 5

先定位到文件:<Flutter path>/packages/flutter/lib/src/widgets/framework.dart ,然后加入以下代码。这些代码会在启动时统计 widget 的构建次数,并在一段时间(这里设置的是 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');      }    });  });}

然后,修改 StatelessElementStatelessElementbuild 方法来调用 _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 文件[9]。

需要注意的是,几次重新构建不一定会引起问题,但是这个办法可以通过验证不可见的 widget 是否在构建来帮你 debug 性能问题。

web 专用 tips:你可以添加一个 resetOutput 函数(可以在浏览器的控制台中调用)来获取随时获取 widget 的构建次数。

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 文件[10]。

结语—

高效的性能调优需要我们明白底层的工作原理。文章里的 tips 可以帮助你决定什么时候构建 widget 来使你的 app 在所有场景都保持高性能。

这篇文章是我们在提高 Flutter Gallery[11] 性能中学习到的系列内容之一。希望对你有所帮助,能让你学到可以在你的 Flutter app 中用上的内容。系列文章如下:

  • Flutter 性能优化系列之 tree shaking 和延迟加载 [12]

  • Flutter 性能优化系列之图片占位符、预缓存和禁用导航过渡动画 [13]

  • Flutter 性能优化系列之打造高性能 widget(本文)

你还可以查看适用所有水平开发者的 Flutter UI 性能文档[14]。

参考资料

[1]

bottom sheet: https://material.io/components/sheets-bottom

[2]

Flutter 卡顿的动画:你不该这样 setState: https://medium.com/flutter-community/flutter-laggy-animations-how-not-to-setstate-f2dd9873b8fc

[3]

DartPad 例子: https://dartpad.dev/e41ed2678b9b9d7347880c20ec49f3f2

[4]

DartPad 例子: https://dartpad.dev/1ae687f1c0d17eb80c8e28a70fb5b8d1

[5]

这个例子: https://dartpad.dev/a338a69afea04f746015861cd55782db

[6]

Slivers, Demystified: https://medium.com/flutter/slivers-demystified-6ff68ab0296f

[7]

Flutter Gallery: https://gallery.flutter.dev/#/

[8]

查看 widget 重新构建信息: https://flutter.dev/docs/development/tools/android-studio#show-performance-data

[9]

framework.dart 文件: https://gist.github.com/guidezpl/54f9a03b0adbf207153178dba0bf214c

[10]

framework.dart 文件: https://gist.github.com/guidezpl/32518a6d22596393fa368c28e8f0ece4

[11]

Flutter Gallery: https://gallery.flutter.dev/#/

[12]

Flutter 性能优化系列之 tree shaking 和延迟加载: https://github.com/zsjie/o2team.github.io/blob/v2/source/\_posts/2020-10-13-optimizing-performance-in-flutter-web-apps-with-tree-shaking-and-deferred-loading.md

[13]

Flutter 性能优化系列之图片占位符、预缓存和禁用导航过渡动画: https://github.com/zsjie/o2team.github.io/blob/v2/source/\_posts/2020-10-13-improving-perceived-performance-with-image-placeholders-precaching-and-disabled-navigation.md

[14]

Flutter UI 性能文档: https://flutter.dev/docs/perf/rendering/ui-performance

本文分享自微信公众号 - 凹凸实验室(AOTULabs)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
5个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这