Flutter 学习与性能优化总结

3,436 阅读9分钟

前言

  有幸负责的模块使用Flutter编写,在三个月的开发过程中,在原有Demo自学基础上又学到了很多,谨以此篇文章做一个Flutter阶段性的学习和总结,以便于往后的学习过程中温故而知新,那么我们正篇开始。

前世今生

  新事物的诞生往往是有一定原因存在的,移动端在这条路上有几个阶段,从Android Native 到 WebView 阶段,为了获得不发版本就可以获得实时动态化的效果,双端使用JSBridge实现了与原生Native底层能力的对接。   Native容器阶段,主要有React Native为代表,虽然RN依赖于原生渲染,性能好与H5,但是还存在一些问题,比如:JavaScript和原生通信,部分场景存在通讯瓶颈,容易导致卡顿、不同移动平台,空间需要单独维护,原生版本更新后,社区控件更新较慢等问题。

  Flutter阶段,Flutter抛弃了原生系统控件和 Webview,使用自研高性能渲染引擎来绘制Widget,预先(AOT)编译,运行时直接执行Native(arm)代码,Dart代码执行(在UI TaskRunner),图片下载(IO TaskRunner),真正的渲染(GPU TaskRunner),同平台的通信等(Platform TaskRunner即Native概念下的主线程)是互相隔离的。针对布局等的优化:布局计算时单次树走动即可完成;Relayout Boundary机制:如果Child 的size是固定的,那么不会因为Child的Relayout导致Parent ReLayout等布局优化,都让Flutter脱颖而出。

  所以,App引入Flutter,可以有效提高开发效率,避免双端不一致的现象,缩小开发成本。但是Flutter毕竟不是原生,在代码写法上有很多需要注意的地方,文章第三章会主要总结本人在开发和工作中遇到的问题,相比于H5,flutter在动态化方面,支持的并不好。

1 基础知识

1.1 运行模式

  Debug:Debug模式可以在真机和模拟器上同时运行:会进入所有断点,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。命令flutter run就是以这种模式运行的。

  Release : Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。

  Profile:Profile模式只能在真机上运行,不能在模拟器上运行:和Release模式类似,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以使用DevTools)。flutter run --profile 可进入该模式。

  Test :headless test模式只能在桌面上运行:和Debug模式不同的是headless的而且你能在桌面运行。命令flutter test就是以这种模式运行的。

1.2 架构设计

  Framework : 使用dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。

  Engine : C++实现,主要包括:Skia,Dart和Text。 Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。

  Embedder : 嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。 从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

1.3 渲染流程

  Widget:Widget树实际上是一个配置树,而真正的UI渲染树是由Element构成;不过,由于Element是通过Widget生成,所以它们之间有对应关系,我们可以宽泛地认为Widget树就是指UI控件树或UI渲染树。

  Element : 一个Widget对象可以对应多个Element对象。这很好理解,根据同一份配置(Widget),可以创建多个实例(Element)。

  从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

举个栗子?

  以下例子取自 闲鱼技术博客 闲鱼Flutter复杂业务优化

Container(
    color: Colors.blue,
    child: Row(
        children: [
    Image.asset('image'),
    Text('text'),
    ],
  ),
);

在这里插入图片描述

  依据上图 来说就是 UI 刷新的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework 进行 animate, build,layout,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU, GPU 经过处理后在显示器上面显示

  在基础能力和业务开发完成后,性能优化和代码检查也是非常重要的,所以后边的文章主要围绕 性能检测和优化 等细节的总结。

3 性能检测工具

结合 Flutter 性能分析 链接 和Flutter 性能视图 等官方文档,整合调试工具说明,并添加自我理解

3.1 Performance Overlay

  开启方式比较多,通过 DevTools TimeLines 或者 run Flutter Inspector 都可以进入

在这里插入图片描述

  点击show CPU GPU 图后 在这里插入图片描述

● 竖轴表示耗时,沿竖轴的黑线是时间线 (间隔单位为 16ms) ● 横轴则表示帧,垂直的绿色条代表的是当前帧 (如果为红色则代表处理耗时点) ● 上图显示GPU线程消耗的时间 (帧为红色:Widget太多 场景复杂 渲染耗时) ● 下图显示UI线程消耗的时间 (帧为红色:Dart代码有问题 查看build中是否做太多耗时操作)

也可以在代码中开启 PerformanceOverlay 控件

@override
  Widget build(BuildContext context) {
    Widget app = MaterialApp(
    	// true 展示PerformanceOverlay
      showPerformanceOverlay: showPerformanceOverlay,
      title: 'appname',
      theme: ThemeData(
        appBarTheme: AppBarTheme(brightness: Brightness.light),
        textTheme: textTheme
      ),
      home: _buildHome(context),
      builder: (_, widget) {
        return MediaQuery(
          data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
          child: widget
        );
      },
    );

    return WillPopScope(
      child: app,
      onWillPop: dealWillPop,
    );
  }

  showPerformanceOverlay 为MaterialApp自带属性,如果没有使用MaterialApp,可以在代码中通过PerformanceOverlay.allEnabled(checkerboardOffscreenLayers: true); 开启

3.2 DevTools TimeLine

  使用 DevTools 可以在Performance Overlay大粒度卡顿范围内深度挖掘 由于Observatory和DevTools功能类似,且正逐渐被DevTools替代,这里主要罗列DevTools的使用方式。

在这里插入图片描述

● 顶部部分列表类似于PerformanceOverlay ,深色代表GPU耗时,浅红色代表UI层耗时(图层过多) ● 中间部分为帧事件图表,分位 UI 和 GPU两部分组成 ● 底部部分为CPU图表,主要有三种不同呈现方式来展示CPU调用堆栈信息

  我们也可以通过添加一些系统api ,更方便我们定位事件图表具体执行位置 ● debugProfileBuildsEnabled - 向 Timeline 事件中添加 build 信息 ● debugProfilePaintsEnabled - 向 timeline 事件中添加 paint 信息 ● debugPrintRebuildDirtyWidgets - 记录每帧重建的 widget

  和Android Profiler 类似,帧事件横轴代表事件顺序,纵轴 代表方法调用堆栈,我们可以通过事件宽度来判断那些事件是比较耗时的。

3.3 Widget rebuild stats

  Android Studio 中 View > Tool Windows > Flutter Performance 打开性能工具窗口,在 Widget rebuild stats 中勾选 Track widget rebuilds 来查看 widget 的重建信息。重建信息包括 Widget 名字、源码位置、上一帧中重建次数、当前页面中重建次数。此外,Widget 名字前面还会显示一个小图标。 ● 黄色旋转圆圈 - 重建次数过多 ● 灰色圆圈 - 未重建 ● 灰色旋转圆圈 其他情况

4 性能优化

  上一章,对于检测工具大概的总结了使用方式和看法,下边我们就根据工具,来定位界面的问题,并总结出我的优化方式。

4.1 列表优化

  在商品2.0中列表占比是比较多得,首页列表的交互逻辑更为复杂,如何让用户用起来如丝般顺滑,需要进行一些性能的打磨。

问题: 界面滑动的时候 会有轻微掉帧现象,检测 fps长期飘红(小于 60fps) 在这里插入图片描述 (图一 Flutter Performance 截图)

在这里插入图片描述 (图二 DevTools Fps ui GPU 截图)

方案:

  更换PowerScrollView 后列表 滑动FPS和部分DevTools截图 PowerScrollView :概念链接

PowerScrollView 优化点:

  1. 针对PowerDataManager 数据可以做到增量更新。
  2. 添加缓存数组,监控_childElements方法来对element 进行缓存,当滚动超出 viewport 的显示以及预加载范围或者数据源发生变化,会通过调用 collectGarbage 方法回收不需要的 elements)
  3. cell 层面引入了 placeholder 的机制,快速滑动场景优化build工作量
  4. 更多样的瀑布流 、列表样式可供选择等等优化

引入后效果,

在这里插入图片描述 在这里插入图片描述

(图一 Flutter performance 截图)

图中红色部分主要处于手指第一次按下,并且执行了顶部商品工具栏动画的原因

在这里插入图片描述

(图二 DevTools FPS UI GPU 截图)

  上图可以看到 gpu占比较多,列表滑动起始部分还是有卡顿部分,主要是由于首页商品工具栏过度动画 和 首页复杂的UI展示引起的,后续可以通过

4.2 优化 ClipPath 和 ClipRPath

  在刚开始调试初期,使用 Timeline 查看渲染线程性能消耗,可以发现有多个 ClipRectLayer 和 ClipRRectLayer过程,对比商品首页界面后,猜测是在Flutter壳工程中使用自带Image展示图片的时候,对于大图没有进行裁剪显示,部分圆角Widget也需要优化,同时修复 radius 为0也会设置 ClipRRect 的问题。

4.3 常用优化方法

  1. 尽量将setState放在叶子节点,好处是build时影响范围极小,局部刷新
  2. 使用ListView.builder()而不是直接使用ListView()来构建列表 (官方推荐)
  3. 对于频繁更新的控件,使用RepaintBoundary隔离,让其拥有一个独立的paint区域
  4. 使用const来修饰永远不需要变更的控件,如果宽高固定,推荐固定宽高,避免重复计算
  5. 按需使用StateLessWidget,并非全部用StateFulWidget
  6. 使用Visibility控件,避免布局树频繁切换
  7. 针对于使用Fish-redux,state 对象中的视图数据真正发生变化的时候,新建 state 对象
  8. 使用图片替换半透明效果,减少saveLayer 或者clipPath 的使用。

最后

不知不觉已经写到了最后,对于Flutter的学习和使用,一直都在路上,文章中不免有些纰漏,还望大佬们海涵并指出,后续会继续深耕Flutter 动态化方案和Flutter性能优化,继续努力学习,继续满血冲冲冲!