[Flutter][性能优化] 篇一: 布局优化

691 阅读6分钟

概要

使用合适的布局Widget和布局算法来避免不必要的布局计算和重绘,以提高应用的性能和响应速度。

flutter三棵树

Flutter的界面渲染离不开三棵树:Widget、Element、RenderObject。当Widget被加载的时候,它并不是马上就会被绘制出来,而是会先创建出它对应的Element,然后Element再根据Widget的配置信息在对应位置生成一个RenderObject,从而进行绘制的

  1. Widget树:Widget树是由各种Widget组成的层次结构,用于描述应用程序的用户界面。Widget是Flutter中构建用户界面的基本单元,它们可以是文本、按钮、图片等各种元素。Widget树描述了应用程序的界面结构和布局。

  2. Element树:Element树是Widget树的实例化对象,在渲染过程中,Widget树会被实例化为Element树。Element是Flutter中的一个重要概念,它负责维护Widget的状态和生命周期管理。Element树与Widget树一一对应,每个Widget都会对应一个Element。

  3. RenderObject树:RenderObject树是Element树的底层实现,它负责将Element树转化为最终的渲染对象。RenderObject是Flutter中的一个核心概念,它负责处理布局、绘制和事件响应等底层操作。RenderObject树是Flutter渲染引擎的基础,它将Element树中的布局和样式信息转换为最终的屏幕显示。 这三棵树在Flutter中协同工作,实现了高效的UI渲染和响应。Widget树描述了界面结构,Element树负责管理状态和生命周期,RenderObject树负责最终的渲染和绘制。通过这种层次结构的组织,Flutter可以实现高性能的跨平台UI开发。

优化布局方法

1.使用具有固定大小的Widget

在Flutter中,布局计算(build)和重绘(setState)是一种相对昂贵的操作。

布局计算是指当Widget树中的某个节点需要进行布局计算时,Flutter会调用该节点对应的build方法来创建该节点的Widget,并将其添加到渲染树中。

重绘是指当Widget树中的某个节点需要进行界面更新时,Flutter会调用该节点的setState方法来通知Flutter框架进行重绘。setState方法是StatefulWidget的一个方法,用于更新该节点的状态,并触发界面重绘的过程。

当Widget的大小不会改变时,可以使用具有固定大小的Widget,例如Container、SizedBox等。这样可以避免不必要的布局计算和重绘。

2.使用Flex布局

flex布局可以根据可用空间自动调整子Widget的布局。当父Widget的大小发生变化时,子Widget会自动适应新的布局,而不需要进行额外的布局计算和重绘。

Flex布局可以根据可用空间自动调整子Widget的布局的过程中难道不是进行了布局计算和重绘吗,为什么说不需要进行额外的布局计算和重绘?

使用Flex布局的过程中确实是进行了布局计算和重绘,但是相对于传统的布局方式,Flex布局减少了额外的布局计算和重绘的工作量。

在传统的布局方式中,当父Widget的大小发生变化时,需要重新计算和调整所有子Widget的位置和大小,然后再进行重绘。

而在Flex布局中,子Widget会根据父Widget的大小自动调整布局,这个调整过程仅仅是对子Widget的位置和大小进行简单的计算,而不需要重新计算所有子Widget的位置和大小。这是因为Flex布局中的子Widget的布局是相对于父Widget的,所以只需要根据父Widget的大小进行简单的计算和调整,而不需要对所有子Widget进行全局的布局计算。 所以说Flex布局相对于传统的布局方式来说,减少了额外的布局计算和重绘的工作量。

3.避免过度使用嵌套

过度使用嵌套的Widget会导致布局计算和重绘的性能损失。尽量减少嵌套的层次,可以提高应用的性能和响应速度。

4.使用RepaintBoundary

如果某个Widget的子树很复杂,但是它本身不会发生变化,可以将其包装在RepaintBoundary中。 RepaintBoundary会将其子树绘制到一个位图中,并在需要时直接使用该位图进行重绘,避免不必要的布局计算和重绘。

使用RepaintBoundary组件可以将其子组件转换为一张图片,并将其展示在界面上。 这样做的好处是可以避免不必要的重绘,提高界面的渲染效率。同时,使用RepaintBoundary组件还可以将其子组件的绘制结果缓存起来,当需要重新绘制时,可以直接使用缓存的结果,减少绘制的时间和资源消耗。

5.使用ListView.builder代替ListView

/// This constructor is appropriate for list views with a large number of
/// item and separator children because the builders are only called for
/// the children that are actually visible.
///
/// The `itemBuilder` callback will be called with indices greater than
/// or equal to zero and less than `itemCount`.

当需要显示大量列表项时,使用ListView.builder可以提高性能。ListView.builder会根据需要动态加载和回收子组件,而不是一次性加载所有列表项。

6.使用ValueKey

/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s. /// /// A new widget will only be used to update an existing element if its key is /// the same as the key of the current widget associated with the element.

Key是用于标识WidgetElementSemanticsNode的标识符。 只有当新的Widgetkey与与Element关联的当前Widgetkey相同时,才会使用新的Widget来更新现有的Element

使用StatefulWidget的build方法进行局部刷新:如果只需要更新部分子组件的内容,可以在StatefulWidget的build方法中只更新需要改变的部分,避免重新构建整个布局。 可以通过在需要刷新的子组件上使用ValueKey来实现

示例代码

import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Counter: $_counter',
          style: TextStyle(fontSize: 20),
        ),
        SizedBox(height: 10),
        Container(
          key: ValueKey(_counter), // 使用ValueKey来标识需要刷新的子组件
          color: Colors.blue,
          width: 200,
          height: 200,
          child: Center(
            child: Text(
              'Child Widget',
              style: TextStyle(fontSize: 20, color: Colors.white),
            ),
          ),
        ),
        SizedBox(height: 10),
        RaisedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

在上面的示例中,我们使用ValueKey来标识Container组件,当_counter发生变化时,只有带有相同ValueKeyContainer组件会被重新构建,从而实现局部刷新。其他子组件不受影响,避免了重新构建整个布局。

示例代码

import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Counter: $_counter',
          style: TextStyle(fontSize: 20),
        ),
        SizedBox(height: 10),
        Container(
          key: ValueKey(_counter), // 使用ValueKey来标识需要刷新的子组件
          color: Colors.blue,
          width: 200,
          height: 200,
          child: Center(
            child: Text(
              'Child Widget',
              style: TextStyle(fontSize: 20, color: Colors.white),
            ),
          ),
        ),
        SizedBox(height: 10),
        RaisedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}

7.避免在build方法中计算或执行耗时操作

参考文档

Flutter几个优化技巧,减少rebuild影响

Flutter中Key的详解