目录:
1. 卡顿的原理
2. 如何检测卡顿
3. 卡顿检测的工具
- 卡顿优化方案
5. listview卡顿优化方案
整体的卡顿优化架构.jpg
1. 卡顿的原理
框架.jpg一秒 60 帧,也就意味着平均两帧之间的间隔为 16.7ms
检测build---layout---paint三个阶段的耗时 , 同时有4大线程
所以,我们做性能优化,关心DartUI,关心GPU两个线程,掉不掉帧,卡不卡的关键,
就看这两位了,而且在99%情况下,作为Flutter开发人员,我们我们基本上解决好,DartUI线程上的问题,就==解决了渲染性能问题。
原因: Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因
1). UI线程慢了-->渲染指令出的慢
2). GPU线程慢了-->光栅化慢、图层合成慢、像素上屏慢
总之: 我们只需要在任意Flutter工程中,搜索drawFrame() 便可以得到答案。
2. 如何检测卡顿
我们经常在做性能调优的时候,会用到timeline工具,你会看到这样一幅图:
卡顿.jpg现在串起来了吗,4个线程,build---layout---paint三个阶段是不是都一目了然,各发生在什么地方,什么阶段,谁先谁后。
所以,我们说 要解决卡顿掉帧的问题,就是要解决build,layout,paint这三个阶段各函数执行耗时的问题。 3. 卡顿检测的工具
检测工具的效果:流畅度检测工具 APP 以悬浮框的方式显示
卡顿显示指标.jpg3.1 FPS检测工具 fps_monitor (重点 )
使用方法; dependencies: fps_monitor: ^1.12.13-1, 接入工程
显示Fps界面
显示 Fps 的页面比较简单,直接通过 OverlayState 插入即可。如果你不太熟悉 Overlay 可以把它理解成浮窗
实现原理: 渲染调度SchedulerBinding
3.2. 卡顿排查:DevTools是官方的开发配套工具,非常实用
- Performance检测单帧CPU耗时(build、layout、paint)、GPU耗时、Widget Build次数
- CPUProfiler 检测方法耗时
- Flutter Inspector观察不合理布局
- Memory 监控Dart内存情况
devTools, devTools的启动姿势是:
flutter pub global activate devtools
devTools
结合DevTools的分析图,我们可以看出。在上面130ms的构建的主要耗时集中在Layout中调用的build方法
devtool.jpg3.3. Flutter Performance&Inspector
首推官方性能分析工具并结合使用 profile 模式查看性能问题
https://www.sunmoonblog.com/2020/01/10/flutter-performance-tools/ (非常牛逼)
以AS为例,右侧会出现Flutter Performance和Inspector2个功能区。Performance功能区如下图:
inspector.jpg
3.4 其他工具:PerformanceOverLay 和 DoKit
4. 卡顿优化方案
4.1 小技巧
https://mp.weixin.qq.com/s/QgPXNpdU2mlAb6tlcsm_eQ
1)、尽量将setState放在叶子节点,好处是build时影响范围极小,简称局部刷新
卡顿方案.jpgProvider 中获取 Model 的方式会影响刷新范围。推荐使用 Selector 或 Consumer 来获取祖先
2). 缓存不变的Widget
缓存不变的Widget有2大好处。1.被缓存的Widget将无需重复创建, 虽然Flutter官方认为Widget是一种非常轻量级的对象,在实际业务中,Build耗时过高仍是一种常见现象。2.返回相同引用的Widget将使Flutter停止该子树后续遍历, 即Flutter认为该子树无变化无需更新。原理请看下图“Element.updateChild源码分析”
源码分析.jpg
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场景。
RepaintBoundary.jpg7)、使用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性能问题分析中提到过,Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个item
所以引起卡顿的原因非常明显主要由于,在某一帧内,ListView构建多个复杂的item。例如分析图中,在Layout阶段同时build了多个item,一个item的构建耗时已经接近10ms,同时构建自然超过了16ms。
5.3 如何优化ListView卡顿?
1). 长列表滑动性能优化
长列表.jpgListView等长列表在滚动的过程中是Lazy Loading机制,按需加载滑窗范围内的items,但如果items的高度是没有显性的指定的时候,将会有严重的性能问题
提供一个新的属性itemExtentBuilder
,有了它,我们可以为每一个item指定高度,同时有着丝滑的性能体验。
2)、分帧上屏
卡顿的本质原因是在一帧内,模块的运行时间过长,这不光是ListView的问题,所有有复杂元素的页面都一样。那么我们有没有一种通用的方案解决这个问题?其实答案很简单,我们可以从两条路去思考:第一种 优化模块时间(例如安卓上的布局优化等) 这个需要我们具体问题具体分析,因为导致模块卡顿的原因是多样的,有可能是Widget太复杂,没有合适的局部刷新,或者 UI isolate进行了大量计算等。第二条思路是在不优化模块的情况下,对时间进行分片,提升流畅度 也就是俗称的分帧运行
jietu-1710320048860.jpg假设,我们屏幕能显示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缓存池,在创建的时候优先从缓冲池获取;
- .按需加载滑窗范围内的items,但如果items的高度是没有显性的指定的时候,将会有严重的性能问题
ListView可以通过ListView.itemExtent
或者ListView.prototypeItem
设置高度来提高Lazy Loading过程中的耗
- . 跳转到某个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;
}
}
}
使用GlobalKey不应该在每次build的时候重建GlobalKey,它应该是State拥有的长期存在的对象。
4.2) 首页预加载
为了减少等待时间,能让用户进入列表页就能看到内容,在上个页面预加载列表的数据。预加载数据有几种情况,已加载成功直接带入加载数据结果,“在途请求”通过桥方法重新获取数据。代码如下:
_loadHotels() {
if (isFirstLoad && page == 1) {
// response首页携带已请求完毕的数据
if (response != null) {
// 处理展示列表页数据
return;
// 数据还在请求当中
} else if (isPreloading) {
// 首页数据加载完毕后回调,处理展示列表页数据
return;
}
}
// 正常加载数据
}
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;
}
4.4 ) 取消在途网络请求
频繁做一些筛选等操作会在短时间内多次请求网络,如果网络较差或者服务端返回时间过长,会导致数据展示错乱的问题,在刷新列表时要取消掉还未返回数据的请求。
_loadHotels() {
if (isRefresh) {
// 通过标识符取消请求
cancelRequest(identifier);
}
identifier = 'QUERY_IDENTIFIER' + '时间戳';
// 列表数据请求
}
4.5)、使用ListView.builder()而不是直接使用ListView()来构建列表。
4.6). 列表 Item 高度可知的情况下,推荐设置 itemExtent,减少滑动中频繁计算列表高度
5.4 高性能的ListView
参考文档如下:
(https://juejin.cn/post/7129888644290068487?searchId=2024031317364543C06BCCF389EE03AD55)
总结 :
listview优化卡顿
1. 长列表优化
2. Widget 分帧上屏
3. 列表 element 复用优化
网友评论