美文网首页
3.遥遥领先flutte3.0 性能优化系列|一文教你完全掌握L

3.遥遥领先flutte3.0 性能优化系列|一文教你完全掌握L

作者: 鹏城十八少 | 来源:发表于2024-03-13 17:45 被阅读0次

    目录:

    1. 卡顿的原理

    2. 如何检测卡顿

    3. 卡顿检测的工具

    1. 卡顿优化方案

    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 以悬浮框的方式显示

    卡顿显示指标.jpg
    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
    
    结合DevTools的分析图,我们可以看出。在上面130ms的构建的主要耗时集中在Layout中调用的build方法
    devtool.jpg
    3.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时影响范围极小,简称局部刷新

    卡顿方案.jpg

    Provider 中获取 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.jpg

    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性能问题分析中提到过,Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个item

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

    5.3 如何优化ListView卡顿?

    1). 长列表滑动性能优化
    长列表.jpg

    ListView等长列表在滚动的过程中是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缓存池,在创建的时候优先从缓冲池获取;

    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;
           }
         }
     }
    

    使用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 复用优化

    相关文章

      网友评论

          本文标题:3.遥遥领先flutte3.0 性能优化系列|一文教你完全掌握L

          本文链接:https://www.haomeiwen.com/subject/iqlizdtx.html