MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter Widget性能优化:减少不必要的重建

2022-12-277.3k 阅读

理解 Flutter 中的重建机制

在 Flutter 开发中,Widget 树的重建是一个核心概念。每当状态发生变化时,Flutter 会通过重建 Widget 树来反映这些变化。这意味着 Flutter 会从根 Widget 开始,自上而下地重新构建整个树或者树的一部分。例如,考虑一个简单的计数器应用:

import 'package:flutter/material.dart';

class CounterApp extends StatefulWidget {
  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _count = 0;

  void _incrementCounter() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_count',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

在这个例子中,每次点击 FloatingActionButton_count 状态改变,setState 方法被调用,导致 build 方法再次执行,进而整个 Scaffold 及其子 Widget 都会被重建。虽然在这个简单的应用中,重建的开销并不明显,但在复杂的应用中,频繁且不必要的重建会显著影响性能。

识别不必要的重建

  1. 父 Widget 状态改变导致子 Widget 重建 假设我们有一个包含多个子 Widget 的父 Widget,并且父 Widget 的状态改变会触发 setState,即使子 Widget 的状态并没有改变,它们也会被重建。例如:
import 'package:flutter/material.dart';

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  int _parentCounter = 0;

  void _incrementParentCounter() {
    setState(() {
      _parentCounter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('Parent Counter: $_parentCounter'),
        ChildWidget(),
        FloatingActionButton(
          onPressed: _incrementParentCounter,
          tooltip: 'Increment Parent',
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}

class ChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('This is a child widget');
  }
}

在这个例子中,每次点击 FloatingActionButton 增加 _parentCounterChildWidget 也会被重建,尽管它本身的状态并没有改变。这就是不必要的重建,因为 ChildWidget 的渲染结果并没有因为父 Widget 的状态改变而改变。

  1. 不必要的 StatefulWidget 使用 有时候,开发者可能会过度使用 StatefulWidget,导致不必要的重建。例如,一个仅仅显示静态文本的 Widget,使用 StatefulWidget 就显得多余。如下:
import 'package:flutter/material.dart';

class UnnecessaryStateful extends StatefulWidget {
  @override
  _UnnecessaryStatefulState createState() => _UnnecessaryStatefulState();
}

class _UnnecessaryStatefulState extends State<UnnecessaryStateful> {
  @override
  Widget build(BuildContext context) {
    return Text('This could be a StatelessWidget');
  }
}

在这种情况下,使用 StatelessWidget 就可以避免不必要的状态管理和可能的重建。

减少不必要重建的策略

  1. 使用 constfinal 在 Flutter 中,constfinal 关键字可以帮助我们优化性能。const Widgets 在编译时就被创建,并且在整个应用生命周期中是不可变的。例如:
import 'package:flutter/material.dart';

class ConstWidgetExample extends StatelessWidget {
  const ConstWidgetExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Text('This is a const text');
  }
}

在这个例子中,const Text Widget 只会被创建一次,无论 ConstWidgetExample 被重建多少次。final 变量也是不可变的,但它是在运行时初始化。例如:

import 'package:flutter/material.dart';

class FinalWidgetExample extends StatelessWidget {
  final String text;

  const FinalWidgetExample({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(text);
  }
}

这里的 textfinal,一旦初始化就不能改变,这有助于 Flutter 优化渲染过程,因为它知道这个值不会变化。

  1. InheritedWidget 的合理使用 InheritedWidget 是 Flutter 中用于在 Widget 树中向下传递数据的一种机制。它允许子 Widget 在不需要重建整个树的情况下访问父 Widget 的数据。例如,假设我们有一个应用主题设置,希望在整个应用中传递主题数据:
import 'package:flutter/material.dart';

class ThemeDataProvider extends InheritedWidget {
  final ThemeData themeData;

  const ThemeDataProvider({
    Key? key,
    required this.themeData,
    required Widget child,
  }) : super(key: key, child: child);

  static ThemeDataProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeDataProvider>();
  }

  @override
  bool updateShouldNotify(ThemeDataProvider oldWidget) {
    return themeData != oldWidget.themeData;
  }
}

class ThemeConsumerWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = ThemeDataProvider.of(context);
    if (themeProvider == null) {
      return Container();
    }
    return Text(
      'Theme Color: ${themeProvider.themeData.primaryColor}',
      style: TextStyle(color: themeProvider.themeData.primaryColor),
    );
  }
}

在这个例子中,ThemeDataProvider 继承自 InheritedWidgetThemeConsumerWidget 通过 ThemeDataProvider.of(context) 获取主题数据。只有当 themeData 发生变化时,依赖它的 ThemeConsumerWidget 才会重建,而不是每次父 Widget 重建时都重建。

  1. ValueListenableBuilderStreamBuilder 的优化使用 ValueListenableBuilderStreamBuilder 是 Flutter 中用于响应数据变化的 Widget。它们可以帮助我们控制重建的范围。例如,使用 ValueListenableBuilder 来监听一个 ValueNotifier
import 'package:flutter/material.dart';

class ValueListenableExample extends StatefulWidget {
  @override
  _ValueListenableExampleState createState() => _ValueListenableExampleState();
}

class _ValueListenableExampleState extends State<ValueListenableExample> {
  final ValueNotifier<int> _counterNotifier = ValueNotifier(0);

  void _incrementCounter() {
    _counterNotifier.value++;
  }

  @override
  void dispose() {
    _counterNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        ValueListenableBuilder<int>(
          valueListenable: _counterNotifier,
          builder: (context, value, child) {
            return Text('Counter: $value');
          },
        ),
        FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}

在这个例子中,只有 ValueListenableBuilder 内部的 Text Widget 会因为 _counterNotifier 的变化而重建,而不是整个 ColumnStreamBuilder 的原理类似,它根据 Stream 的数据变化来重建 Widget。例如,监听一个网络请求的 Stream

import 'dart:async';
import 'package:flutter/material.dart';

class StreamExample extends StatefulWidget {
  @override
  _StreamExampleState createState() => _StreamExampleState();
}

class _StreamExampleState extends State<StreamExample> {
  late StreamController<int> _streamController;

  @override
  void initState() {
    super.initState();
    _streamController = StreamController<int>.broadcast();
    Timer.periodic(const Duration(seconds: 1), (timer) {
      _streamController.add(timer.tick);
    });
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: _streamController.stream,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Text('Stream Data: ${snapshot.data}');
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

这里 StreamBuilder 根据 _streamController.stream 的数据变化来重建内部的 Widget,从而控制了重建范围。

  1. IndexedStackOffstage 的使用 IndexedStackOffstage 可以帮助我们在不重建 Widget 的情况下隐藏或显示 Widget。IndexedStack 允许我们在多个子 Widget 中切换显示,只有当前显示的子 Widget 会被构建和渲染。例如:
import 'package:flutter/material.dart';

class IndexedStackExample extends StatefulWidget {
  @override
  _IndexedStackExampleState createState() => _IndexedStackExampleState();
}

class _IndexedStackExampleState extends State<IndexedStackExample> {
  int _currentIndex = 0;

  void _changeIndex() {
    setState(() {
      _currentIndex = _currentIndex == 0? 1 : 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        IndexedStack(
          index: _currentIndex,
          children: const <Widget>[
            Text('First Widget'),
            Text('Second Widget'),
          ],
        ),
        FloatingActionButton(
          onPressed: _changeIndex,
          tooltip: 'Change Index',
          child: Icon(Icons.switch_account),
        ),
      ],
    );
  }
}

在这个例子中,当点击 FloatingActionButton 时,IndexedStack 会切换显示的子 Widget,但不会重建未显示的子 Widget。Offstage 则是通过设置 offstage 属性来隐藏或显示 Widget,隐藏的 Widget 不会被渲染,但仍然存在于 Widget 树中。例如:

import 'package:flutter/material.dart';

class OffstageExample extends StatefulWidget {
  @override
  _OffstageExampleState createState() => _OffstageExampleState();
}

class _OffstageExampleState extends State<OffstageExample> {
  bool _isVisible = true;

  void _toggleVisibility() {
    setState(() {
      _isVisible =!_isVisible;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Offstage(
          offstage:!_isVisible,
          child: const Text('This is an offstage widget'),
        ),
        FloatingActionButton(
          onPressed: _toggleVisibility,
          tooltip: 'Toggle Visibility',
          child: Icon(_isVisible? Icons.visibility_off : Icons.visibility),
        ),
      ],
    );
  }
}

这里通过 Offstageoffstage 属性控制 Text Widget 的显示和隐藏,避免了不必要的重建。

  1. AutomaticKeepAliveClientMixin 的使用 在使用 TabBarViewPageView 等多页面切换组件时,AutomaticKeepAliveClientMixin 可以帮助我们保持页面的状态,避免每次切换页面时重建。例如,在一个 TabBarView 中有多个页面:
import 'package:flutter/material.dart';

class KeepAlivePage extends StatefulWidget {
  @override
  _KeepAlivePageState createState() => _KeepAlivePageState();
}

class _KeepAlivePageState extends State<KeepAlivePage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return const Text('This page keeps its state');
  }
}

class TabBarExample extends StatefulWidget {
  @override
  _TabBarExampleState createState() => _TabBarExampleState();
}

class _TabBarExampleState extends State<TabBarExample> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabController,
          tabs: const <Tab>[
            Tab(text: 'Page 1'),
            Tab(text: 'Page 2'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: const <Widget>[
          KeepAlivePage(),
          KeepAlivePage(),
        ],
      ),
    );
  }
}

在这个例子中,KeepAlivePage 实现了 AutomaticKeepAliveClientMixin 并设置 wantKeepAlivetrue,这样当切换 TabBarView 的页面时,页面的状态会被保持,不会被重建。

  1. RepaintBoundary 的使用 RepaintBoundary 可以限制重绘的区域。当一个 Widget 频繁重绘,并且这种重绘影响到了其他 Widget 的性能时,使用 RepaintBoundary 可以将重绘限制在一个特定的区域内。例如,一个包含动画的 Widget:
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';

class AnimatedWidgetExample extends StatefulWidget {
  @override
  _AnimatedWidgetExampleState createState() => _AnimatedWidgetExampleState();
}

class _AnimatedWidgetExampleState extends State<AnimatedWidgetExample> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0, end: 1).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });
    _animationController.repeat();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        RepaintBoundary(
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue.withOpacity(_animation.value),
          ),
        ),
        const Text('This text is not affected by the animation repaint'),
      ],
    );
  }
}

在这个例子中,RepaintBoundary 包裹了 Container,使得 Container 的重绘(由于动画导致的颜色变化)不会影响到下面的 Text Widget,从而优化了性能。

  1. 分析和优化 build 方法 仔细分析 build 方法中的代码,避免在 build 方法中进行复杂的计算或创建新的对象。例如,不要在 build 方法中初始化一个新的 List 或执行复杂的数学运算。如果有需要,可以将这些操作移到 initState 或其他生命周期方法中。例如:
import 'package:flutter/material.dart';

class BuildMethodOptimization extends StatefulWidget {
  @override
  _BuildMethodOptimizationState createState() => _BuildMethodOptimizationState();
}

class _BuildMethodOptimizationState extends State<BuildMethodOptimization> {
  List<int> _numbers = [];

  @override
  void initState() {
    super.initState();
    _numbers = List.generate(1000, (index) => index + 1);
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _numbers.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text('Number: ${_numbers[index]}'),
        );
      },
    );
  }
}

在这个例子中,_numbers 列表在 initState 中初始化,而不是在 build 方法中,这样避免了每次 build 时都重新创建列表,提高了性能。

性能分析工具

  1. Flutter DevTools Flutter DevTools 是一套用于分析和调试 Flutter 应用的工具集。其中的性能面板可以帮助我们分析 Widget 的重建情况。通过在终端中运行 flutter pub global run devtools 启动 DevTools,然后在应用中启用性能监控(通常通过 flutter run --profileflutter run --release),我们可以在 DevTools 中查看重建的次数、持续时间等信息。例如,在 DevTools 的性能面板中,我们可以看到每个 Widget 的重建时间线,从而找出重建频繁的 Widget 并进行优化。

  2. Widgets Flare Widgets Flare 是一个可以集成到 Flutter 应用中的调试工具,它可以在应用运行时实时显示 Widget 的边界、重建次数等信息。通过在应用中添加 WidgetsFlare 依赖并初始化,我们可以在应用界面上直观地看到哪些 Widget 正在频繁重建。例如:

import 'package:flutter/material.dart';
import 'package:widgets_flare/widgets_flare.dart';

void main() {
  runApp(WidgetsFlare(
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Widgets Flare Example'),
        ),
        body: Center(
          child: Text('Check Widget rebuilds'),
        ),
      ),
    );
  }
}

运行这个应用后,我们可以在界面上看到 Widget 的重建次数等信息,方便我们进行性能优化。

通过上述的策略和工具,开发者可以有效地减少 Flutter Widget 的不必要重建,提升应用的性能和用户体验。在实际开发中,需要根据应用的具体场景和需求,灵活运用这些方法来达到最佳的性能优化效果。