7

【老孟Flutter】如何提高Flutter应用程序的性能

 3 years ago
source link: http://www.cnblogs.com/mengqd/p/14306082.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

3imYrmn.png!mobile

首先 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)),
          ),
        ),
      ],
    );
  }
}

aQ7fI3R.gif!mobile

注意看上图右边下半部分,点击切换 开关 的时候,所有的组件全部重建了,理想情况下,应该只是 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,
  ),
)

BJv2qaJ.gif!mobile

此时看到重建的组件只有 _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(() {

            });
          })
        ],
      ),
    );
  }
}

miEzIr.gif!mobile

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

const Text('老孟'),

3qiuUb3.gif!mobile

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

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

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

bool _visible = true;

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: [
        if(_visible)
          Text('可见'),
        Container(),
      ],
    ),
  );
}

可见时的组件树:

yau2Ab3.png!mobile

不可见时的组件树:

AbYjQvn.png!mobile

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

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

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

aQjUjq6.png!mobile

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

bool _showButton = true;

@override
Widget build(BuildContext context) {
  return Center(
    child: Column(
      children: [
        _showButton ? RaisedButton(onPressed: null) : Text('不显示'),
        Container(),
      ],
    ),
  );
}

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

V3uYrm.png!mobile

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

AbEf63B.png!mobile

看到左侧子节点由 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);
          },
        ))
      ],
    );
  }
}

nIjIvaQ.gif!mobile

耗时在 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,
)

aeAz22v.gif!mobile

优化后瞬间跳转到底部。

这是因为不设置 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),
      ),
    )

uY7jQfj.png!mobile

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

交流

老孟Flutter博客(330个控件用法+实战入门系列文章): http://laomengit.com

添加微信或者公众号领取 《330个控件大全》和 《Flutter 实战》PDF。

欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:

qqQjAzF.png!mobileBzuqUjR.jpg!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK