跳转到正文
YGJoon 技术博客

Flutter 渲染优化详解

发布于:
·1.6k 字 · 约 4 分钟·作者:
用 DevTools 定位 Flutter 帧耗时,理解列表默认重绘边界,并按需优化图层与图片尺寸。
Flutter 渲染优化详解文章封面
目录

简介

渲染性能直接影响应用的流畅度。本文介绍如何优化 Flutter 应用的渲染性能。

基本概念

渲染流程

  1. Layout - 计算大小和位置
  2. Paint - 绘制界面
  3. Composite - 合成图层

性能指标

  1. 帧率(FPS)
  2. 帧时间
  3. 掉帧数量

优化方法

1. 减少重建

// 使用 const 构造函数(和下面定义的 MyWidget 对应)
const MyWidget(
  key: Key('my_widget'),
);

// 使用 StatelessWidget
class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('Hello'),
    );
  }
}

2. 按测量结果使用 RepaintBoundary

SliverChildBuilderDelegate 的 addRepaintBoundaries 默认是 true,会为列表子项添加重绘边界。ListView.builder 也通过对应的 sliver delegate 构建子项,所以通常不需要在 builder 中再手动给每个列表项套一层。

当 DevTools 或重绘可视化确认某个子树与周围内容的重绘时机明显不同时,再考虑增加边界。RepaintBoundary 会创建独立 Layer;它可能减少重复绘制,也会增加图层和内存开销,效果要在目标场景中测量。

3. 图层优化

// Opacity 会作用于整个 child 子树。
Opacity(
  opacity: 0.5,
  child: ExpensiveWidget(),
);

// 如果只需要半透明背景,只给背景颜色设置透明度;child 本身不会变透明。
Container(
  color: const Color.fromRGBO(0, 0, 0, 0.5),
  child: ExpensiveWidget(),
);

// 使用 Transform 代替 Positioned
// 不推荐
Stack(
  children: [
    Positioned(
      left: _animation.value,
      child: ExpensiveWidget(),
    ),
  ],
);

// 推荐
Transform.translate(
  offset: Offset(_animation.value, 0),
  child: ExpensiveWidget(),
)

完整示例

class RenderOptimizedApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: OptimizedList(),
      ),
    );
  }
}

class OptimizedList extends StatefulWidget {
  @override
  _OptimizedListState createState() => _OptimizedListState();
}

class _OptimizedListState extends State<OptimizedList> {
  final ScrollController _controller = ScrollController();
  final List<String> _items = List.generate(1000, (i) => 'Item $i');
  
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      controller: _controller,
      cacheExtent: 100, // 缓存范围
      physics: const BouncingScrollPhysics(),
      slivers: [
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) => _buildItem(index),
            childCount: _items.length,
          ),
        ),
      ],
    );
  }
  
  Widget _buildItem(int index) {
    // 使用 const 构造函数
    return const ListItemWidget(
      height: 100,
      margin: EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 8,
      ),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class ListItemWidget extends StatelessWidget {
  final double height;
  final EdgeInsets margin;
  
  const ListItemWidget({
    Key? key,
    required this.height,
    required this.margin,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Container(
      height: height,
      margin: margin,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: const [
          BoxShadow(
            color: Colors.black12,
            offset: Offset(0, 2),
            blurRadius: 4,
          ),
        ],
      ),
      child: const _ItemContent(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Row(
      children: const [
        _ItemImage(),
        SizedBox(width: 16),
        Expanded(child: _ItemText()),
      ],
    );
  }
}

// 注意:原稿引用了 _ItemImage/_ItemText 却没定义,已补上
class _ItemImage extends StatelessWidget {
  const _ItemImage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Icon(Icons.image, size: 48);
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Text('Item');
  }
}

性能监控

1. 性能叠加层

void main() {
  runApp(
    Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: [
          MyApp(),
          const PerformanceOverlay.allEnabled(),
        ],
      ),
    ),
  );
}

2. 自定义性能监控

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';

class FrameTimingLogger {
  bool _started = false;

  void start() {
    if (_started) return;
    SchedulerBinding.instance.addTimingsCallback(_onTimings);
    _started = true;
  }

  void stop() {
    if (!_started) return;
    SchedulerBinding.instance.removeTimingsCallback(_onTimings);
    _started = false;
  }

  void _onTimings(List<FrameTiming> timings) {
    for (final timing in timings) {
      debugPrint(
        'build=${timing.buildDuration.inMilliseconds}ms, '
        'raster=${timing.rasterDuration.inMilliseconds}ms, '
        'total=${timing.totalSpan.inMilliseconds}ms',
      );
    }
  }
}

在页面或服务销毁时调用 stop() 移除回调。这个示例记录引擎上报的帧耗时,不用递归的 post-frame 回调近似计算 FPS;完整的采集与生命周期说明见Flutter 性能监控。

最佳实践

  1. 记住帧预算:60fps 下 UI 线程和 Raster 线程单帧各 16.6ms(120Hz 设备是 8.3ms)。DevTools Performance 视图里标红的帧就是要修的——先看标红再动手,别凭感觉优化。
  2. 固定高度的列表给 itemExtent:省掉滚动时每一项的布局测量,长列表滚动提升明显。高度不固定的千万别硬给,错位比卡顿更难查。
  3. build 方法只做”拼 widget”:排序、正则、耗时计算全挪到 initState 里算好存起来,或做 memoize。build 每帧都可能跑,在里面干重活等于每帧交税。
  4. RepaintBoundary 按需添加:列表 delegate 默认会添加子项边界;额外边界只用于重绘时机不同且已通过 profile 观察到收益的子树。

注意事项

  1. 避免重复包列表项:SliverChildBuilderDelegate 默认 addRepaintBoundaries: true。手动再包一层会形成嵌套边界;关闭默认边界后再自行管理时,也应通过 DevTools 验证,而不是预设图层越多越快。
  2. 别过度优化:没测出瓶颈就加缓存、加隔离层,等于给代码加债。流程永远是:DevTools 定位标红帧 → 修 → 再测。
  3. 验收用低端机:开发机上 60fps 不代表千元机也 60fps。渲染优化的验收机至少要有一台 3 年前的中低端 Android,否则你优化的全是幻觉。
  4. 检查列表图片尺寸:过大的源图会增加解码与纹理内存开销。优先提供接近显示尺寸的资源;无法调整资源时,可按实际布局设置 cacheWidth / cacheHeight,再用 Flutter Inspector 检查 oversized images。不要笼统地把图片解码归因于 UI 线程,也不要把它当作可以直接挪进应用 isolate 的普通 Dart 工作。

实战案例

旅行类应用 是个列表密集型 App(旅行目的地、失物招领都是长列表),渲染优化是刚需。我的三板斧:第一,列表图片全用 cached_network_image,占位图+淡入,避免滑动白闪;第二,pull_to_refresh(^2.0.0)做下拉刷新,它的 footer/header 是按需构建的,不会一次性 build 全量;第三,轮播图用 carousel_slider(^5.1.1)而不是自己写 PageView——自己写的容易忘记 viewportFraction 和预加载,carousel 把坑都踩完了。还有个反直觉的:flutter_screenutil 的 .w/.h 适配在 build 里频繁调用没问题,别在循环里做 heavy 计算就行。渲染优化的核心就一句:别在 build 方法里干重活。

总结

渲染优化需要从多个层面入手,包括减少重建、优化图层、合理使用缓存等。通过合理的优化措施,可以显著提升应用的渲染性能和流畅度。

参考资料


分享这篇文章:


相关文章

上一篇
Flutter 性能监控:FrameTiming、RSS 与 DevTools 实战