写点什么

如何提高 Flutter 应用程序的性能

用户头像
老孟Flutter
关注
发布于: 2021 年 03 月 23 日

首先 Flutter 是一个非常高性能的框架,因此大多时候不需要开发者做出特殊的处理,只需要避免常见的性能问题即可获得高性能的应用程序。


重建最小化原则


在调用 setState() 方法重建组件时,一定要最小化重建组件,没有变化的组件不要重建,看下面的 Demo,这是一个设置页面,


import 'package:flutter/material.dart';
class SettingDemo extends StatefulWidget { @override _SettingDemoState createState() => _SettingDemoState();}
class _SettingDemoState extends State<SettingDemo> {
Widget _item( {IconData iconData, Color iconColor, String title, Widget suffix}) { return Container( height: 45, child: Row( children: <Widget>[ SizedBox( width: 30, ), Icon( iconData, color: iconColor, ), SizedBox( width: 30, ), Expanded( child: Text('$title'), ), suffix, SizedBox( width: 15, ), ], ), ); }
bool _switchValue = false;
@override Widget build(BuildContext context) { return Column( children: <Widget>[ _item( iconData: Icons.notifications, iconColor: Colors.blue, title: '是否允许4G网络下载', suffix: Switch( value: _switchValue, onChanged: (value) { setState(() { _switchValue = value; }); }), ), Divider(), _item( iconData: Icons.notifications, iconColor: Colors.blue, title: '消息中心', suffix: Text( '12条', style: TextStyle(color: Colors.grey.withOpacity(.5)), ), ), Divider(), _item( iconData: Icons.thumb_up, iconColor: Colors.green, title: '我赞过的', suffix: Text( '121篇', style: TextStyle(color: Colors.grey.withOpacity(.5)), ), ), Divider(), _item( iconData: Icons.grade, iconColor: Colors.yellow, title: '收藏集', suffix: Text( '2个', style: TextStyle(color: Colors.grey.withOpacity(.5)), ), ), Divider(), _item( iconData: Icons.account_balance_wallet, iconColor: Colors.blue, title: '我的钱包', suffix: Text( '10万', style: TextStyle(color: Colors.grey.withOpacity(.5)), ), ), ], ); }}
复制代码



注意看上图右边下半部分,点击切换开关的时候,所有的组件全部重建了,理想情况下,应该只是 Switch 组件进行切换,因此将 Switch 组件进行封装:


class _SwitchWidget extends StatefulWidget {  final bool value;
const _SwitchWidget({Key key, this.value}) : super(key: key);
@override __SwitchWidgetState createState() => __SwitchWidgetState();}
class __SwitchWidgetState extends State<_SwitchWidget> { bool _value;
@override void initState() { _value = widget.value; super.initState(); }
@override Widget build(BuildContext context) { return Switch( value: _value, onChanged: (value) { setState(() { _value = value; }); }, ); }}
复制代码


使用:


_item(  iconData: Icons.notifications,  iconColor: Colors.blue,  title: '是否允许4G网络下载',  suffix: _SwitchWidget(    value: false,  ),)
复制代码



此时看到重建的组件只有 _SwitchWidgetSwitch 组件,提高了性能。


如果 Switch 组件的状态改变也会改变其它组件的状态,这是典型的组件间通信,这种情况下可以使用 InheritedWidget,但更建议使用状态管理框架(比如 Provider 等),而不是将其父组件改变为 StatefulWidget。


尽量不要将整个页面定义为 StatefulWidget 组件,因为一旦重建将重建此页面下所有的组件,尤其是 Switch 、Radio 等组件状态的改变导致的重建,强烈建议对其进行封装。


这里有一个误区,有些人认为,将组件拆分为方法可以减少重建,就比如上面的例子,将 _SwitchWidget 组件改变为方法,该方法返回 Switch 组件,这是错误的,此种方式并不能减少重建, 但是将一个组件拆分为多个小组件是可以减少重建的,就像上面的例子,将需要重建的 Switch 封装为一个单独的 StatefulWidget 组件,避免了其他不必要的重建。


强烈建议:在组件前加上 const


在组件前加上 const ,相当于对此组件进行了缓存,下面是未加 const 的代码:


class ConstDemo extends StatefulWidget {  @override  _ConstDemoState createState() => _ConstDemoState();}
class _ConstDemoState extends State<ConstDemo> { @override Widget build(BuildContext context) { return Center( child: Column( children: [ Text('老孟'), RaisedButton(onPressed: (){ setState(() {
}); }) ], ), ); }}
复制代码



Text('老孟') 组件加上 const


const Text('老孟'),
复制代码



对比两次 Text 组件的重建情况,加上 const 后,未重建。


避免更改组件树的结构和组件的类型


有如下场景,有一个 Text 组件有可见和不可见两种状态,代码如下:


bool _visible = true;
@overrideWidget build(BuildContext context) { return Center( child: Column( children: [ if(_visible) Text('可见'), Container(), ], ), );}
复制代码


可见时的组件树:



不可见时的组件树:



两种状态组件树结构发生变化,应该避免发生此种情况,优化如下:


Center(  child: Column(    children: [      Visibility(        visible: _visible,        child: Text('可见'),      ),      Container(),    ],  ),)
复制代码


此时不管是可见还是不可见状态,组件树都不会发生变化,如下:



还有一种情况是根据不同的条件构建不同的组件,如下:


bool _showButton = true;
@overrideWidget build(BuildContext context) { return Center( child: Column( children: [ _showButton ? RaisedButton(onPressed: null) : Text('不显示'), Container(), ], ), );}
复制代码


设置为 true 时的组件树结构:



设置为 false 时的组件树结构:



看到左侧子节点由 RaisedButton 变为了 Text


上面的情况组件树发生了更改,不管是类型发生更改,还是深度发生更改,如果无法避免,那么就将变化的组件树封装为一个 StatefulWidget 组件,且设置 GlobalKey,如下:


封装变化的部分:


class ChildWidget extends StatefulWidget {  const ChildWidget({Key key}) : super(key: key);
@override _ChildWidgetState createState() => _ChildWidgetState();}
class _ChildWidgetState extends State<ChildWidget> { bool _showButton = true;
@override Widget build(BuildContext context) { return _showButton ? RaisedButton(onPressed: null) : Text('不显示'); }}
复制代码


构建:


class ConstDemo extends StatefulWidget {  @override  _ConstDemoState createState() => _ConstDemoState();}
class _ConstDemoState extends State<ConstDemo> { @override Widget build(BuildContext context) { return Center( child: Column( children: [ ChildWidget(key: GlobalKey(),), Container(), ], ), ); }}
复制代码


虽然通过 GlobalKey 提高了上面案例的性能,但我们千万不要乱用 GlobalKey,因为管理 GlobalKey 的成本很高,所以其他需要使用 Key 的地方建议考虑使用 Key, ValueKey, ObjectKey, 和 UniqueKey


关于 GlobalKey 的相关说明参考:https://api.flutter.dev/flutter/widgets/GlobalKey-class.html


关于 ListView 的优化


ListView 是我们最常用的组件之一,用于展示大量数据的列表。如果展示大量数据请使用 ListView.builder 或者 ListView.separated,千万不要直接使用如下方式:


ListView(  children: <Widget>[    item,item1,item2,...  ],)
复制代码


这种方式一次加载所有的组件,没有“懒加载”,消耗极大的性能。


ListView 中 itemExtent 属性对动态滚动到性能提升非常大,比如,有 2000 条数据展示,点击按钮滚动到最后,代码如下:


class ListViewDemo extends StatefulWidget {  @override  _ListViewDemoState createState() => _ListViewDemoState();}
class _ListViewDemoState extends State<ListViewDemo> { ScrollController _controller;
@override void initState() { super.initState(); _controller = ScrollController(); }
@override Widget build(BuildContext context) { return Stack( children: [ ListView.builder( controller: _controller, itemBuilder: (context, index) { return Container( height: 80, alignment: Alignment.center, color: Colors.primaries[index % Colors.primaries.length], child: Text('$index',style: TextStyle(color: Colors.white,fontSize: 20),), ); }, itemCount: 2000, ), Positioned( child: RaisedButton( child: Text('滚动到最后'), onPressed: () { _controller.jumpTo(_controller.position.maxScrollExtent); }, )) ], ); }}
复制代码



耗时在 2 秒左右,加上 itemExtent 属性,修改如下:


ListView.builder(  controller: _controller,  itemBuilder: (context, index) {    return Container(      height: 80,      alignment: Alignment.center,      color: Colors.primaries[index % Colors.primaries.length],      child: Text('$index',style: TextStyle(color: Colors.white,fontSize: 20),),    );  },  itemExtent: 80,  itemCount: 2000,)
复制代码



优化后瞬间跳转到底部。


这是因为不设置 itemExtent 属性,将会由子组件自己决定大小,大量的计算导致 UI 堵塞。


关于 AnimatedBuilder TweenAnimationBuilder 的优化


这里说的是向 AnimatedBuilder 、TweenAnimationBuilder 等一类的组件的问题,这些组件都有一个共同点,带有 builder 且其参数重有 child


以 AnimatedBuilder 为例,如果 builder 中构建的树中包含与动画无关的组件,将这些无关的组件当作 child 传递到 builder 中比直接在 builder 中构建更加有效。


比如下面的代码,直接在 builder 中构建子组件:


AnimatedBuilder(    animation: animation,    builder: (BuildContext context, Widget child) {      return Transform.rotate(        angle: animation.value,        child: FlutterLogo(size: 60,),      );    },  )
复制代码


优化后的代码:


AnimatedBuilder(    animation: animation,    builder: (BuildContext context, Widget child) {      return Transform.rotate(        angle: animation.value,        child: child,      );    },    child: FlutterLogo(size: 60,),  )
复制代码


谨慎的使用一些组件


部分组件一定要谨慎使用,因为这些组件包含一些昂贵的操作,比如 saveLayer() 方法。


调用 saveLayer()会分配一个屏幕外缓冲区。 将内容绘制到屏幕外缓冲区中可能会触发渲染目标切换,这在较早的 GPU 中特别慢。


另外虽然下面这些组件比较消耗性能,但并不是禁止大家使用,而是谨慎使用,如果有替代方案,考虑使用替代方法。


尤其注意,如果这些组件频繁重建(比如动画的过程),要重点优化。


Clip 类组件


Clip 类组件是常用的裁剪类组件,比如:ClipOval、ClipPath、ClipRRect、ClipRect、CustomClipper。这些组件中都有 clipBehavior 属性,不同的值性能是不同的,


///  * [hardEdge], which is the fastest clipping, but with lower fidelity.///  * [antiAlias], which is a little slower than [hardEdge], but with smoothed edges.///  * [antiAliasWithSaveLayer], which is much slower than [antiAlias], and should///    rarely be used.
复制代码


越往下,速度越慢。


一些简单的圆角组件的设置可以使用 Container 实现:


Container(      height: 200,      width: 200,      decoration: BoxDecoration(        image:  DecorationImage(          image: NetworkImage(              'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),          fit: BoxFit.cover,        ),        border: Border.all(          color: Colors.blue,          width: 2,        ),        borderRadius: BorderRadius.circular(12),      ),    )
复制代码



Opacity


Opacity 组件的功能是使子组件透明。此类将其子级绘制到中间缓冲区中,然后将子级混合回到部分透明的场景中。


对于除 0.0 和 1.0 之外的不透明度值,此类相对昂贵,因为它需要将子级绘制到中间缓冲区中。 对于值 0.0,根本不绘制子级。 对于值 1.0,将立即绘制没有中间缓冲区的子对象。


如果仅仅是对单个 Image 或者 Color 增加透明度,直接使用比 Opacity 组件更快:


 Container(color: Color.fromRGBO(255, 0, 0, 0.5))
复制代码


比使用 Opacity 组件更快:


Opacity(opacity: 0.5, child: Container(color: Colors.red))
复制代码


如果对组件的透明度进行动画操作,建议使用 AnimatedOpacity


还有一些组件也要慎重使用,比如:


  • ShaderMask

  • ColorFilter

  • BackdropFilter


文中如果有不完善或者不正确的地方欢迎提出意见,后面如果优化的补充将会在我的博客中进行补充,地址:


http://laomengit.com/blog/20201227/improve_performance.html


参考链接:


https://flutter.dev/docs/perf/rendering/best-practices


https://api.flutter.dev/flutter/widgets/Opacity-class.html#transparent-image


https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html#performance-considerations


交流


博客中领取 《330 个控件大全》和 《Flutter 实战》PDF。地址:http://laomengit.com


发布于: 2021 年 03 月 23 日阅读数: 19
用户头像

老孟Flutter

关注

公众号老孟Flutter 《Flutter 实战入门》。 2021.01.11 加入

还未添加个人简介

评论

发布
暂无评论
如何提高Flutter应用程序的性能