flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter

1. 卡顿的原理

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_缓存_02

一秒 60 帧,也就意味着平均两帧之间的间隔为 16.7ms

检测build—layout—paint三个阶段的耗时 , 同时有4大线程

所以,我们做性能优化,关心DartUI,关心GPU两个线程,掉不掉帧,卡不卡的关键,

就看这两位了,而且在99%情况下,作为Flutter开发人员,我们我们基本上解决好,DartUI线程上的问题,就==解决了渲染性能问题。

原因: Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因

1). UI线程慢了–>渲染指令出的慢

2). GPU线程慢了–>光栅化慢、图层合成慢、像素上屏慢

总之: 我们只需要在任意Flutter工程中,搜索drawFrame() 便可以得到答案。

2. 如何检测卡顿

我们经常在做性能调优的时候,会用到timeline工具,你会看到这样一幅图:

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_性能优化_03

现在串起来了吗,4个线程build—layout—paint三个阶段是不是都一目了然,各发生在什么地方,什么阶段,谁先谁后。

所以,我们说 要解决卡顿掉帧的问题,就是要解决build,layout,paint这三个阶段各函数执行耗时的问题。 3. 卡顿检测的工具

检测工具的效果:流畅度检测工具 APP 以悬浮框的方式显示

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_android_04

3.1 FPS检测工具 fps_monitor (重点 )

使用方法; dependencies: fps_monitor: ^1.12.13-1, 接入工程

显示Fps界面

显示 Fps 的页面比较简单,直接通过 OverlayState 插入即可。如果你不太熟悉 Overlay 可以把它理解成浮窗

实现原理: 渲染调度SchedulerBinding

3.2. 卡顿排查:DevTools是官方的开发配套工具,非常实用

  1. Performance检测单帧CPU耗时(build、layout、paint)、GPU耗时、Widget Build次数
  2. CPUProfiler 检测方法耗时
  3. Flutter Inspector观察不合理布局
  4. Memory 监控Dart内存情况

devTools, devTools的启动姿势是:

flutter pub global activate devtools
devTools
  • 1.
  • 2.
结合DevTools的分析图,我们可以看出。在上面130ms的构建的主要耗时集中在Layout中调用的build方法

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_Dart_05

3.3. Flutter Performance&Inspector

首推官方性能分析工具并结合使用 profile 模式查看性能问题

 https://www.sunmoonblog.com/2020/01/10/flutter-performance-tools/ (非常牛逼)

以AS为例,右侧会出现Flutter Performance和Inspector2个功能区。Performance功能区如下图:

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_缓存_06

3.4 其他工具:PerformanceOverLay 和 DoKit

4. 卡顿优化方案

4.1 小技巧

1)、尽量将setState放在叶子节点,好处是build时影响范围极小,简称局部刷新

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_07

Provider 中获取 Model 的方式会影响刷新范围。推荐使用 Selector 或 Consumer 来获取祖先

2). 缓存不变的Widget

缓存不变的Widget有2大好处。1.被缓存的Widget将无需重复创建, 虽然Flutter官方认为Widget是一种非常轻量级的对象,在实际业务中,Build耗时过高仍是一种常见现象。2.返回相同引用的Widget将使Flutter停止该子树后续遍历, 即Flutter认为该子树无变化无需更新。原理请看下图“Element.updateChild源码分析”

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_08

3). 减少不必要的build(setState)

直播Tab用到一个埋点曝光组件,经过DevTools检查,发现其在每一次进度回调中重新创建itemWidget,虽然这不会造成业务异常,但理论上itemWidget只需被创建一次,这块经排查是使用组件时误传了builder函数,而不是直接传itemWidget实例。

详情页的逻辑非常复杂,AppBar根据滚动距离实时计算透明度,这会导致高频的setState,实际上透明度变化前后应该满足一个差值后才应刷新一次状态, 为了性能考量,透明度应该只有少数几种值变更。

4). 避免频繁的triggerGC

因为AliFlutter的关系,我们得以主动触发DartGC,但GC同样也是有消耗的,高频的GC更是如此。淘特之前因为iOS的内存压力,在列表滚动停止时ScrollEndNotification则会触发GC,ScrollEndNotification在每一次手Down->up事件后都会触发一次,如果用户多次触摸,则会较为频繁的触发GC,实测影响Y67 4帧左右的性能,这块增加页面不可见时GC 和在Y67等android低端机关闭滑动GC,提高滑动性能。

5). 优化 ClipPath 和 ClipRPath

5)、能不用 Opacity Widget,就尽量不要用,因为这货会粗发GPU一个saveLayer的指令,做Skia的大神说,这个指令相当耗时。

避免使用 Opacity widget,尤其是在动画中避免使用。请用 AnimatedOpacity 或 FadeInImage 进行代替

6)、多变图层与不变图层分离, 对于频繁更新的控件(比如倒计时,秒表, 就是动画等),使用RepaintBoundary隔离它,让他在一个独立的paint区域。

在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。以淘特为例。

直播Feed中的Gif图是不断高频跳动,这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。

同理, 秒杀倒计时也是电商常见场景, 该组件也适用于RepaintBoundary场景。

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_09

7)、使用const来修饰永远不需要变更的控件。

8)、优先使用StateLessWidget,而不是全部用StateFulWidget

9). 尽量减少或降级Clip、Opacity等组件的使用

Flutter中,Clip主要用于裁剪,裁矩形、圆角矩形、圆形。一旦调用,后续所有的绘图指令都会受其Clip影响。有些ClipRRect可以用ShapeDecoration代替,Opacitiy改用AnimatedOpacity, 针对图片的Clip裁切,可以走定制图片库Transform实现。

10)、使用Visibility控件替换if/else,有些小伙伴喜欢else时return一个 占位控件,须不知,这种效率是没有Visibility高效的。

11). 用 AnimatedBuilder 时,避免在不依赖于动画的 widget 的构造方法中构建 widget 树。动画的每次变动都会重建这个 widget 树。而应该构建子树的那一部分,并将其作为 child 传递给 AnimatedBuilder

12). 避免使用带换行符的长文本

4.2 小技巧如何提高UI线程性能:

如何提高build性能

  • 降低遍历出发点,降低setState的触发节
  • 停止树的遍历,不变的内容,返回同样的组件实例、Flutter将停止遍历该树(SlideTransition)
  • 减少非必要的build(setState)
  • 如何提高layout性能
  • layout暂时不太容易出问题
  • 如何提高paint性能
  • RepaintBoundary分离多变和不变的图层,如Gif、动画, 但多图层的合成也是有开销的
  • 其他
  • 耗时方法如大JSON解析用compute子线程化
  • 减少不必要的channel调用或批量合并
  • 减少动画
  • 减少Release时的log
  • 提高UI线程在Android/iOS的优先级
  • 列表组件支持局部build
  • 较小的cacheExtent值,减少渲染范围

如何提高GPU线程性能:

1). 谨慎saveLayer

2).尽量少ClipPath、一旦调用,后续所有绘图指令需与Path做相交。(ClipRect、ClipRRect等)

3).减少毛玻璃BackdropFilter、阴影boxShadow

4).减少Opacity使用,必要时用AnimatedOpacity

5. listview卡顿优化方案

5.1 Listiview卡顿的原因 :
listiview卡顿的原因 :在某一帧内,ListView构建多个复杂的item, 导致build方法耗时, 出现卡顿
5.2 istiview卡顿场景
1). 长列表懒加载
2). 首次进入多次的构建item,
3). 快速滑动,一帧内构建多个item
4). 一些分页列表上

Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个item

所以引起卡顿的原因非常明显主要由于,在某一帧内,ListView构建多个复杂的item。例如分析图中,在Layout阶段同时build了多个item,一个item的构建耗时已经接近10ms,同时构建自然超过了16ms。

5.3 如何优化ListView卡顿?
1). 长列表滑动性能优化

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_性能优化_10

ListView等长列表在滚动的过程中是Lazy Loading机制,按需加载滑窗范围内的items,但如果items的高度是没有显性的指定的时候,将会有严重的性能问题

提供一个新的属性itemExtentBuilder,有了它,我们可以为每一个item指定高度,同时有着丝滑的性能体验。

2)、分帧上屏

卡顿的本质原因是在一帧内,模块的运行时间过长,这不光是ListView的问题,所有有复杂元素的页面都一样。那么我们有没有一种通用的方案解决这个问题?其实答案很简单,我们可以从两条路去思考:第一种 优化模块时间(例如安卓上的布局优化等) 这个需要我们具体问题具体分析,因为导致模块卡顿的原因是多样的,有可能是Widget太复杂,没有合适的局部刷新,或者 UI isolate进行了大量计算等。第二条思路是在不优化模块的情况下,对时间进行分片,提升流畅度 也就是俗称的分帧运行

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_缓存_11

假设,我们屏幕能显示4个item,每个item构建耗时是10ms。在现有的ListView布局过程中,会在第一帧的时候,同时构建这四个item,总共40ms。

采用分帧之后,在页面的第一帧我们先通过构建简单的占位item,占位的item可以是个简单的Container。由于其构建基本不耗时,在第一帧的时候构建四个Container不会导致卡顿。 之后将实际的四个item,分别延迟到后面四帧进行渲染。这样对于每个16.7ms而言,都没有发生超时渲染,整个流程不会发生卡顿

3)、Element复用?

闲鱼在一文中还提到了一点:element的复用。这个优化点在和lwlizhe交流之后,我个人认为可能效果没那么明显。因为如果从Native的角度出发以ViewHolder为例,他的复用本质是对于同类型的item减少创建view和解析xml的时间,其中有个关键的方法:onBindViewHolder将数据绑定到View上。

但是对于Flutter而言,即使item的类型相同,对于不同数据的item而言,并没有一个数据绑定Widget的方法。所以仅仅只能做建立一个缓存池来保存element,创建的时候优先从缓存获取。但这样问题就来了,其实官方本来就有一个cacheExtent缓存区的设计,缓存在cacheExtent内的的Element。个人认为没多大必要额外在做一个缓存。

最简单的,将cacheExtent设置大一点就行
4)、LoadMore增量更新

上面我们提到了,item的构建是由ListView的layout驱动,所以如果是增量更新的情况,我们只要修改itemCount之后,标记ListView进行layout即可。闲鱼在文中提到了这个在layout之前需要做Widget缓存的更新,但是实际上在1.22之后,因为这个缓存几乎没有任何优化作用,官方已经去掉了这个Widget缓存,所以这个过程变得更加简单。

1)、加载更多的更新问题
2)、Element被回收后的复用问题

其中核心的updateChild方法的第一个参数传递的是index对应的element对象,而第二个参数变成了null,在原来我一直在错误的使用 setState()?中提到过,在第二个参数为null的时候,那么之前的element对象会被卸载unmount()。这样在二次创建的时候,该index对应的element对象又会被再次创建。所以这里可以通过建立一个element缓存池,在创建的时候优先从缓冲池获取;

  1. .按需加载滑窗范围内的items,但如果items的高度是没有显性的指定的时候,将会有严重的性能问题

ListView可以通过ListView.itemExtent或者ListView.prototypeItem设置高度来提高Lazy Loading过程中的耗

  1. . 跳转到某个item, 没法做跳转到某个index的原因。

**5.4 ListView具体的优化措施&&**建议

5.3.1 )ListView Item 复用

通过GlobalKey可以得到widget,包括获得组件的renderBox在内的各种element有关的信息,可以得到state里面的变量。在长列表分页加载时,数据变更会造成整个ListView重现构建,我们就可以利用 globalkey 获得 widget 的属性,来实现 Item 复用。从而解决分页加载成功后大量渲染引造成的页面卡顿问题。

Widget listItem(int index, dynamic model) {
 if (listViewModel!.listItemKeys[index] == null) {
   listViewModel!.listItemKeys[index] =RectGetter.createGlobalKey();
 } else {
     final rectGetter = listViewModel!.listItemKeys[index];
     if (rectGetter is GlobalKey) {
       final widget = rectGetter.currentWidget as RectGetter?;
       if (widget != null) {
         return widget;
       }
     }
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

使用GlobalKey不应该在每次build的时候重建GlobalKey,它应该是State拥有的长期存在的对象。

4.2) 首页预加载

为了减少等待时间,能让用户进入列表页就能看到内容,在上个页面预加载列表的数据。预加载数据有几种情况,已加载成功直接带入加载数据结果,“在途请求”通过桥方法重新获取数据。代码如下:

_loadHotels() {
  if (isFirstLoad && page == 1) {
    // response首页携带已请求完毕的数据
    if (response != null) {
      // 处理展示列表页数据
      return;
      // 数据还在请求当中
    } else if (isPreloading) {
      // 首页数据加载完毕后回调,处理展示列表页数据
      return;
    }
  } 
  // 正常加载数据
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

4.3 )分页预加载

通常情况下当用户滑动到底部的时候才会去加载下一页的数据,这样用户要花费等待加载的时间,影响用户体验。可以采用剩余法预加载数据,当用户滑动到剩余一定数量的酒店时,开始加载下一页的数据,在网络良好的情况下,滑动场列表界面,界面基本不会存在等待加载的时间

// getRectFromKey获取到scrollView的位置信息,遍历指定剩余数量的item,如果在当前屏幕中去加载一下页数据
if (!(itemRect.top > rect.bottom || itemRect.bottom < rect.top)) {
    // 加载下一页数据
}
Rect? getRectFromKey(GlobalKey key) {
  final renderObject = key.currentContext?.findRenderObject();
  final translation = renderObject?.getTransformTo(null).getTranslation();
  final size = renderObject?.semanticBounds.size;
  if (translation != null && size != null) {
    return Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
  }
  return null;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

4.4 ) 取消在途网络请求

频繁做一些筛选等操作会在短时间内多次请求网络,如果网络较差或者服务端返回时间过长,会导致数据展示错乱的问题,在刷新列表时要取消掉还未返回数据的请求。

_loadHotels() {
    if (isRefresh) {
        // 通过标识符取消请求
        cancelRequest(identifier);
    }
    identifier = 'QUERY_IDENTIFIER' + '时间戳';
    // 列表数据请求
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

4.5)、使用ListView.builder()而不是直接使用ListView()来构建列表。

4.6). 列表 Item 高度可知的情况下,推荐设置 itemExtent,减少滑动中频繁计算列表高度

最后

为了能够方便大家快速学习Flutter, 这里整理了Flutter学习路线图以及《Flutter Dart 语言编程入门到精通》&《Flutter实战:第二版》帮助大家配置相关环境,学习Flutter 的基本语法以及最后的项目实际利用。

学习路线:

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_性能优化_12


《Flutter Dart 语言编程入门到精通》
第一章 Dart语言基础
  • 环境准备
  • 基础语法
第二章 Dart 异步编程
  • Dart的事件循环
  • 调度任务
  • 延时任务
  • Future详解
  • async和await
  • lsolate

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_android_13

第三章 异步之 Stream 详解
  • 什么是Stream
  • 单订阅流
  • 广播流
  • Stream Transformer
  • 总结
第四章 Dart标准输入输出流
  • 文件操作

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_14

第五章 Dart 网络编程
  • TCP服务端
  • TCP客户端
  • UDP服务端
  • UDP客户端
  • HTTP服务器与请求
  • WebSocket
第六章 Flutter 爬虫与服务端
  • Dart爬虫开发
  • Dart服务端
  • 创建Flutter项目演示
  • 总结
第七章 Dart 的服务端开发
  • 注册登录实现
第八章 Dart 调用C语言混合编程
  • 环境准备
  • 测试Dart ffi接口
  • 总结
第九章 LuaDardo中Dart与Lua的相互调用
  • Lua C API
  • 创建运行时
  • Dart调Lua
  • Lua调Dart

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_android_15


《Flutter实战:第二版》
第一章:起步
  • 1.1 移动开发技术简介
  • 1.2 初始Flutter
  • 1.3 搭建Flutter开发环境
  • 1.4 Dart语言简介
第二章:第一个Flutter应用
  • 2.1 计数器应用实例
  • 2.2 Widget简介
  • 2.3 状态管理
  • 2.4路由管理
  • 2.5包管理
  • 2.6 资源管理
  • 2.7 调试Flutter应用
  • 2.8 Flutter异常捕获

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_android_16

第三章:基础组件
  • 3.1 文本及样式
  • 3.2 按钮
  • 3.3 图片及ICON
  • 3.4 单选开关和复选框
  • 3.5 输入框及表单
  • 3.6 进度指示器
第四章:布局类组件
  • 4.1 布局类组件简介
  • 4.2 布局原理与约束(constraints)
  • 4.3 线性布局(Row和Column)
  • 4.4 弹性布局(Flex)

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_android_17

第五章:容器类组件
  • 5.1 填充(Padding)
  • 5.2 装饰容器(DecoratedBox)
  • 5.3 变换(Transform)
  • 5.4 容器组件(Container)
  • 5.5 剪裁(Clip)
  • 5.6 空间适配(FittedBox)
  • 5.7 页面骨架(Scaffold)
第六章:可滚动组件
  • 6.1 可滚动组件简介
  • 6.2 SingleChildScrollView
  • 6.3 ListView
  • 6.4 滚动监听及控制

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_18

第七章:功能型组件
  • 7.1 导航返回拦截(WillPopScope)
  • 7.2 数据共享(InheritedWidget)
  • 7.3 跨组件状态共享
  • 7.4 颜色和主题
  • 7.5 按需rebuild(ValueListenableBuilder)
  • 7.6 异步UI更新(FutureBuilder、StreamBuilder)
  • 7.7 对话框详解
第八章:事件处理与通知
  • 8.1 原始指针事件处理
  • 8.2 手势识别
  • 8.3 Flutter事件机制
  • 8.4 手势原理与手势冲突
  • 8.5 事件总线
  • 8.6 通知 Notification

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_flutter_19

第九章:动画
  • 9.1 Flutter动画简介
  • 9.2 动画基本结构及状态监听
  • 9.3 自定义路由切换动画
  • 9.4 Hero动画
  • 9.5 交织动画
  • 9.6 动画切换组件(AnimatedSwitcher)
  • 9.7 动画过渡组件
第十章:自定义组件
  • 10.1 自定义组件方法简介
  • 10.2 组合现有组件
  • 10.3 组合实例:TurnBox
  • 10.4 CustomPaint 与 Canvas
  • 10.5 自绘实例:圆形背景渐变进度条
  • 10.6 自绘组件:CustomCheckbox
  • 10.7 自绘组件: DoneWidget
  • 10.8 水印实例: 文本绘制与离屏渲染

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_性能优化_20

第十一章:文件操作与网络请求
  • 11.1 文件操作
  • 11.2 通过HttpClient发起HTTP请求
  • 11.3 Http请求库-dio
  • 11.4 实例:Http分块下载
第十二章:Flutter扩展
  • 12.1 包和插件
  • 12.2 Flutter Web
第十三章:国际化
  • 13.1 让App支持多语言
  • 13.2 实现Localizations
  • 13.3 使用Intl包
  • 13.4 国际化常见问题

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_Dart_21

第十四章:Flutter核心原理
  • 14.1 Flutter UI 框架(Framework)
  • 14.2 Element、BuildContext和RenderObject
  • 14.3 Flutter启动流程和渲染管线
  • 14.4 布局(Layout)过程
  • 14.5 绘制(一)绘制原理及Layer
第十五章:一个完整的Flutter应用
  • 15.1 Github客户端示例
  • 15.2 Flutter APP代码结构
  • 15.3 Model类定义
  • 15.4 全局变量及共享状态
  • 15.5 网络请求封装
  • 15.6 APP入口及主页
  • 15.7 登录页
  • 15.8 多语言和多主题

flutte3.0 性能优化系列|一文教你完全掌握ListView卡顿优化_性能优化_22