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

Flutter 性能优化:减少不必要重绘的实战技巧

2021-08-304.4k 阅读

理解 Flutter 中的重绘机制

在 Flutter 应用开发中,重绘(Repainting)是一个关键概念。当 Flutter 检测到 UI 状态变化时,会重新绘制部分或整个 UI。这一过程涉及到多个层面的操作,理解其底层原理对于性能优化至关重要。

渲染树与重绘

Flutter 使用渲染树(Render Tree)来管理 UI 的布局和绘制。渲染树中的每个节点代表一个渲染对象(RenderObject),这些对象负责计算自身的大小、位置,并最终绘制到屏幕上。当某个渲染对象的状态发生变化,如颜色、大小改变,它会标记自身为“需要重绘”。

例如,假设我们有一个简单的 Flutter 应用,包含一个文本组件和一个按钮,点击按钮会改变文本的内容:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String text = '初始文本';

  void _updateText() {
    setState(() {
      text = '更新后的文本';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('重绘示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(text),
            RaisedButton(
              child: Text('点击更新文本'),
              onPressed: _updateText,
            ),
          ],
        ),
      ),
    );
  }
}

在这个例子中,当按钮被点击,setState 方法会触发 _MyHomePageStatebuild 方法重新执行。这会导致整个 Column 及其子组件(TextRaisedButton)对应的渲染对象被标记为需要重绘。

重绘的触发条件

  1. 数据变化:像上述例子中 text 变量的改变,会导致依赖该数据的组件重绘。如果一个组件依赖多个数据,只要其中任何一个数据发生变化,都可能触发重绘。
  2. 父组件重绘:如果父组件重绘,通常其子组件也会重绘。例如,一个 Container 组件的大小发生变化,它内部的所有子组件可能都需要重新计算布局和重绘。
  3. 动画:在 Flutter 中,动画的每一帧更新通常都会触发相关组件的重绘。比如,一个 AnimatedContainer 正在进行动画,它会不断改变自身的属性(如大小、颜色),这会持续触发重绘。

不必要重绘的常见场景及危害

常见场景

  1. 频繁调用 setState:如果在短时间内多次调用 setState,会导致不必要的重绘。例如,在一个循环中调用 setState,每次调用都会触发 build 方法,即使数据的改变并不影响所有组件的显示。
class UnnecessarySetStateExample extends StatefulWidget {
  @override
  _UnnecessarySetStateExampleState createState() => _UnnecessarySetStateExampleState();
}

class _UnnecessarySetStateExampleState extends State<UnnecessarySetStateExample> {
  int count = 0;

  void _incrementCount() {
    for (int i = 0; i < 10; i++) {
      setState(() {
        count++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('不必要的 setState 示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('计数: $count'),
            RaisedButton(
              child: Text('增加计数'),
              onPressed: _incrementCount,
            ),
          ],
        ),
      ),
    );
  }
}

在这个例子中,_incrementCount 方法在循环中调用 setState,这会导致 build 方法被调用 10 次,而实际上只需要更新一次 UI 即可。 2. 无状态组件内数据变化:虽然无状态组件(StatelessWidget)不应该有状态,但如果在构建函数中使用了会发生变化的数据,也可能导致不必要重绘。比如,在 StatelessWidgetbuild 方法中使用了一个全局变量,而这个全局变量会在其他地方被修改。

int globalValue = 0;

class StatelessWidgetWithChangingData extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('无状态组件数据变化示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('全局值: $globalValue'),
            RaisedButton(
              child: Text('改变全局值'),
              onPressed: () {
                globalValue++;
              },
            ),
          ],
        ),
      ),
    );
  }
}

这里,当点击按钮改变 globalValue 时,由于 StatelessWidgetbuild 方法依赖于这个全局变量,整个组件会被重新构建,尽管它本身不应该有状态变化。 3. 父组件构建时子组件不必要重绘:当父组件的 build 方法执行时,即使子组件的数据没有变化,默认情况下子组件也会重新构建和重绘。例如,一个包含多个子组件的 Row 组件,父组件的某个属性变化导致 Row 重绘,但其内部的 Text 组件数据并未改变,却也会跟着重绘。

危害

不必要的重绘会消耗大量的 CPU 和 GPU 资源。每次重绘都需要计算布局、绘制图形等操作,这会导致应用的性能下降,表现为卡顿、掉帧等现象。特别是在复杂的 UI 界面或者动画频繁的场景下,不必要重绘带来的性能问题会更加明显,严重影响用户体验。

减少不必要重绘的实战技巧

使用 const 关键字

  1. 常量组件:在 Flutter 中,如果一个组件在整个应用生命周期内不会发生变化,就可以将其声明为 const。例如,一个固定的图标、文本等。
class ConstWidgetExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('const 组件示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Icon(Icons.home, size: 50),
            const Text('这是一个常量文本'),
          ],
        ),
      ),
    );
  }
}

Flutter 会对 const 组件进行优化,在构建过程中会复用这些组件,而不会因为父组件的重绘或者其他无关因素导致它们重绘。 2. 常量数据:对于一些不会改变的数据,也可以声明为 const。比如,一个固定的颜色值、尺寸等。

const Color myColor = Colors.blue;

class ConstDataExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: myColor,
      child: Text('使用常量颜色'),
    );
  }
}

这样,当其他部分的代码发生变化导致组件重绘时,只要 myColor 不变,Container 的颜色设置部分就不会重新计算和重绘。

合理使用 StatefulWidget 和 StatelessWidget

  1. 区分有状态和无状态:确保将具有不变状态的组件定义为 StatelessWidget,将状态会发生变化的组件定义为 StatefulWidget。例如,一个展示静态图片的组件应该是 StatelessWidget,而一个可以切换开关状态的组件应该是 StatefulWidget
class StaticImage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Image.asset('assets/images/static_image.jpg');
  }
}

class SwitchWidget extends StatefulWidget {
  @override
  _SwitchWidgetState createState() => _SwitchWidgetState();
}

class _SwitchWidgetState extends State<SwitchWidget> {
  bool isSwitched = false;

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: isSwitched,
      onChanged: (value) {
        setState(() {
          isSwitched = value;
        });
      },
    );
  }
}
  1. 避免 StatefulWidget 过度使用:不要因为某个组件可能有微小的状态变化就将其定义为 StatefulWidget。如果状态变化很少且对整体 UI 影响不大,可以考虑其他方式,比如使用全局状态管理(如 Provider)来处理,而不是将其变为 StatefulWidget 导致不必要的重绘。

局部更新组件

  1. 使用 AnimatedBuilderAnimatedBuilder 允许我们在动画过程中局部更新组件,而不是整个父组件。例如,我们有一个包含多个组件的 Column,其中一个 Container 进行动画,我们可以使用 AnimatedBuilder 来只更新这个 Container
class AnimatedBuilderExample extends StatefulWidget {
  @override
  _AnimatedBuilderExampleState createState() => _AnimatedBuilderExampleState();
}

class _AnimatedBuilderExampleState extends State<AnimatedBuilderExample> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedBuilder 示例'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('这是不变的文本'),
          AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              return Container(
                width: _animation.value * 200,
                height: 100,
                color: Colors.red,
              );
            },
          ),
          Text('这也是不变的文本'),
        ],
      ),
    );
  }
}

在这个例子中,只有 AnimatedBuilder 包裹的 Container 会随着动画的变化而重绘,其他文本组件不会受到影响。 2. 使用 ValueListenableBuilderValueListenableBuilder 用于监听 ValueListenable 的变化,并在变化时局部更新组件。例如,我们有一个 ValueNotifier 来管理一个计数器,只希望在计数器变化时更新相关的文本组件。

class ValueListenableBuilderExample extends StatefulWidget {
  @override
  _ValueListenableBuilderExampleState createState() => _ValueListenableBuilderExampleState();
}

class _ValueListenableBuilderExampleState extends State<ValueListenableBuilderExample> {
  final ValueNotifier<int> _counter = ValueNotifier(0);

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ValueListenableBuilder 示例'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('这是不变的文本'),
          ValueListenableBuilder<int>(
            valueListenable: _counter,
            builder: (context, value, child) {
              return Text('计数器: $value');
            },
          ),
          RaisedButton(
            child: Text('增加计数器'),
            onPressed: _incrementCounter,
          ),
          Text('这也是不变的文本'),
        ],
      ),
    );
  }
}

这里,只有 ValueListenableBuilder 包裹的 Text 组件会在 _counter 变化时重绘,其他组件不受影响。

优化数据监听与更新

  1. 减少不必要的依赖:在 StatefulWidget 中,确保 setState 只在真正影响 UI 的数据变化时调用。例如,如果一个组件有多个数据变量,只有当影响 UI 显示的变量变化时才调用 setState
class OptimizedStateExample extends StatefulWidget {
  @override
  _OptimizedStateExampleState createState() => _OptimizedStateExampleState();
}

class _OptimizedStateExampleState extends State<OptimizedStateExample> {
  int importantData = 0;
  int unimportantData = 0;

  void _updateImportantData() {
    setState(() {
      importantData++;
    });
  }

  void _updateUnimportantData() {
    // 这里不调用 setState,因为这个数据变化不影响 UI 显示
    unimportantData++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('优化数据监听示例'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('重要数据: $importantData'),
            RaisedButton(
              child: Text('更新重要数据'),
              onPressed: _updateImportantData,
            ),
            RaisedButton(
              child: Text('更新不重要数据'),
              onPressed: _updateUnimportantData,
            ),
          ],
        ),
      ),
    );
  }
}
  1. 使用 StreamBuilder 与 StreamController:当处理异步数据更新时,StreamBuilderStreamController 可以帮助我们更好地控制数据变化对 UI 的影响。例如,一个从网络获取数据的场景,我们可以通过 Stream 来发送数据更新,StreamBuilder 只在有新数据时更新相关组件。
class StreamBuilderExample extends StatefulWidget {
  @override
  _StreamBuilderExampleState createState() => _StreamBuilderExampleState();
}

class _StreamBuilderExampleState extends State<StreamBuilderExample> {
  final StreamController<String> _streamController = StreamController();

  void _fetchData() {
    // 模拟网络请求
    Future.delayed(Duration(seconds: 2), () {
      _streamController.add('新数据');
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('StreamBuilder 示例'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          StreamBuilder<String>(
            stream: _streamController.stream,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text('数据: ${snapshot.data}');
              } else {
                return Text('等待数据...');
              }
            },
          ),
          RaisedButton(
            child: Text('获取数据'),
            onPressed: _fetchData,
          ),
        ],
      ),
    );
  }
}

在这个例子中,只有当 Stream 有新数据时,StreamBuilder 包裹的 Text 组件才会重绘。

性能分析工具辅助优化

  1. Flutter DevTools:Flutter DevTools 提供了丰富的性能分析功能,包括 CPU 性能分析、内存分析等。在性能分析中,我们可以查看重绘的频率和耗时。例如,通过 DevTools 的性能面板,我们可以看到哪些组件在频繁重绘,从而针对性地进行优化。
  2. 使用 Timeline:Timeline 可以记录 Flutter 应用在一段时间内的各种事件,包括重绘事件。通过分析 Timeline 数据,我们可以了解重绘发生的时间点、持续时间等信息,帮助我们找出不必要重绘的源头。例如,我们可以看到某个动画过程中是否有过多的重绘,或者某个按钮点击操作是否导致了不必要的大面积重绘。

通过以上这些实战技巧,可以有效地减少 Flutter 应用中的不必要重绘,提升应用的性能和用户体验。在实际开发中,需要根据具体的应用场景和需求,综合运用这些技巧进行优化。