⚠️Flutter 性能优化实践 总结⚠️

12,198 阅读16分钟

👏欢迎前往本人的GitHub查看更多内容。点击前往GitHub

在flutter的开发和工作中,因为工作内容的要求越来越高,加上一位优秀的同事,自己也对自己的写的代码除了规范的要求,也开始对性能做了优化。我们开发的App属于首页就是重点,刚好是我负责,所以再简单的UI和逻辑搭建完成后,要求达到一定的性能优化,所以自己开始了解和学习相关的处理。




0.渲染相关知识了解

0.0 Flutter有四种运行模式:Debug、Release、Profile和test,这四种模式在build的时候是完全独立的。

Debug
  Debug模式可以在真机和模拟器上同时运行:会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。命令flutter run就是以这种模式运行的,通过sky/tools/gn --android或者sky/tools/gn --ios来build。有时候也被叫做“checked模式”或者“slow模式”。

Release
  Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。命令flutter run --release就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=release或者sky/tools/gn --ios --runtime-mode=release来build。

Profile
   Profile模式只能在真机上运行,不能在模拟器上运行:基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。命令flutter run --profile就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=profile或者sky/tools/gn --ios --runtime-mode=profile```来build。因为模拟器不能代表真实场景,所以不能在模拟器上运行。

test
   headless test模式只能在桌面上运行:基本和Debug模式一致,除了是headless的而且你能在桌面运行。命令flutter test就是以这种模式运行的,通过sky/tools/gn来build。

0.1 Flutter的架构主要分成三层:Framework,Engine,Embedder。

1.Framework使用dart实现,包括Material 
Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。
此部分的核心代码是:flutter仓库下的flutter 
package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。


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


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

0.2 Widget、Element、RenderObject三者的关系如下:

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

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

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



1.能否在模拟器下进行性能调试?

答案:可以,但是调试很不准确。所以不建议使用模拟器进行性能调试。
几乎全部的 Flutter 应用性能调试都应该在真实的 Android 或者 iOS 设备上以分析模式进行。
通常来说,调试模式或者是模拟器上运行的应用的性能指标和发布模式的表现并不相同。 
应该考虑在用户使用的最慢的设备上检查性能。

  • 为什么应该在真机上运行:

    • 各种模拟器使用的硬件并不相同,因此性能也不同—模拟器上的一些操作会比真机快,而另一些操作则会比真机慢。

    • 调试模式相比分析模式或者发布编译来说,增加了额外的检查(例如断言),这些检查可能相当耗费资源。

  • 调试模式和发布模式代码执行的方式也是不同的。调试编译采用的是“just in time”(JIT)模式运行应用,而分析和发布模式则是预编译到本地指令(“ahead of time”,或者叫 AOT)之后再加载到设备中。JIT本身的编译就可能导致应用暂停,从而导致卡顿。





2.如何进行App性能测试?

答案:
1.在 Android Studio 和 IntelliJ 使用 Run > Flutter Run main.dart in Profile Mode 选项
    1.1 选择 View > Tool Windows > Flutter Inspector。
    1.2 在工具栏中选择书架图标。

2.在 VS Code中,打开 launch.json 文件,设置 flutterMode 属性为 profile(当分析完成后,改回 release 或者 debug)
    2.1 选择 View > Command Palette… 来打开 command palette。
    2.2 在文本框中输入“performance”并在弹出列表中选中 Toggle Performance Overlay。如果命令不可用,请确保应用在运行状态。

3.From the command line, use the --profile flag: 命令行使用 --profile 参数运行
  3.1 flutter run --profile
  3.2 使用 p 参数触发性能图层
  
  
4.可以通过在 MaterialApp 或者 WidgetsApp 的构造方法中设置 showPerformanceOverlay 属性为 true 来展示 PerformanceOverlay widget:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      showPerformanceOverlay: true,
      title: 'My Awesome App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'My Awesome App'),
    );
  }
}



3.如何查看分析性能图层?

答案:
性能图层用两张图表显示应用的耗时信息。如果 UI 产生了卡顿(跳帧),这些图表可以帮助分析原因。图表在当前应用的最上层展示,但并不是用普通的 widget 方式绘制的—Flutter 引擎自身绘制了该图层来尽可能减少对性能的影响。每一张图表都代表当前线程的最近 300 帧表现。

左图:GPU 线程的性能情况在上面,UI 线程显示在下面,垂直的绿色条条代表的是当前帧。
右图:每一帧渲染过程当中总共使用的时间
Flutter 用了一些额外的线程来完成这项工作。开发者的 Dart 代码都在 UI 线程运行。尽管没有直接访问其他线程的权限,但 UI 线程的动作还是对其他线程的性能有影响的。

平台线程
该平台的主线程。插件代码在这里运行。更多信息请参阅:iOS 的 UIKit 文档,或者 Android 的 MainThread 文档。性能图层并不会展示该线程。

UI 线程
UI 线程在 Dart VM 执行 Dart 代码。该线程包括开发者写下的代码和 Flutter 框架根据应用行为生成的代码。当应用创建和展示场景的时候,UI 线程首先建立一个 图层树(layer tree) ,一个包含设备无关的渲染命令的轻量对象,并将图层树发送到 GPU 线程来渲染到设备上。 不要阻塞这个线程! 在性能图层的最低栏展示该线程。

GPU 线程
GPU 线程取回图层树并通知 GPU 渲染。尽管无法直接与 GPU 线程或其数据通信,但如果该线程变慢,一定是开发者 Dart 代码中的某处导致的。图形库 Skia 在该线程运行,有时也被叫做 光栅器(rasterizer)线程 。在性能图层的最顶栏展示该线程。

I/O 线程
可能阻塞 UI 或者 GPU 线程的耗时任务(大多数情况下是I/O)。该线程并不会在性能图层中展示。

红色竖条表明当前帧的渲染和绘制都很耗时 当两张图表都是红色时,就要开始对 UI 线程(Dart VM)进行诊断了。

每一帧都应该在 1/60 秒(大约 16ms)内创建并显示。
如果有一帧超时(任意图像)而无法显示,就导致了卡顿,图表之一就会展示出来一个红色竖条。
如果是在 UI 图表出现了红色竖条,则表明 Dart 代码消耗了大量资源。
而如果红色竖条是在 GPU 图表出现的,意味着场景太复杂导致无法快速渲染。
为什么需要在 16ms 内渲染完成每一帧
1.将帧渲染时间降低到 16ms 以下可能在视觉上看不出来什么变化,但可以延长电池寿命以及避免发热问题。
2.可能在你当前测试设备上运行良好,但请考虑在应用所支持的最低端设备上的情况。
3.当 120fps 的设备普及之后,便需要在 8ms 之内完成每一帧的渲染来保证流畅平滑的体验。



4.如何进行性能分析并开始处理?

4.1 定位 UI 图表中的问题

如果性能图层的 UI 图表显示红色,就要从分析 Dart VM 开始着手了,即使 GPU 图表同样显示红色。

使用 Dart DevTool 进行性能分析
Dart DevTool 提供诸如性能分析、堆测试以及显示代码覆盖率等功能。
DevTool 的 timeline 界面可以让开发者逐帧分析应用的 UI 性能。

(Observatory 被 Dart DevTools 取代了。这个基于浏览器的工具仍在开发中,但只用来预览。参考 DevTools’ docs 页面来获取安装和使用指导。)

4.2 定位 GPU 图表中的问题

有些情况下界面的图层树构造起来虽然容易,但在 GPU 线程下渲染却很耗时。
这种情况发生时,UI 图表没有红色,但 GPU 图表会显示红色。
这时需要找出代码中导致渲染缓慢的原因。
特定类型的负载对 GPU 来说会更加复杂。
可能包括不必要的对 saveLayer 的调用,许多对象间的复杂操作,还可能是特定情形下的裁剪或者阴影。

如果推断的原因是动画中的卡顿的话,可以使用 timeDilation 属性来极大地放慢动画。也可以使用 Flutter Inspector 来减慢动画速度。在 inspector 的 gear 菜单下选中 Enable Slow Animations。如果想对动画速度进行更多操作,请在代码中设置 timeDilation 属性。卡顿是第一帧发生的还是贯穿整个动画过程呢?如果是整个动画过程的话,会是裁剪导致的么?也许有可以替代裁剪的方法来绘制场景。比如说,不透明图层的长方形中用尖角来取代圆角裁剪。如果是一个静态场景的淡入、旋转或者其他操作,可以尝试使用 RepaintBoundary。

4.2.1 检查屏幕之外的视图

saveLayer

saveLayer 方法是 Flutter 框架中最重量的操作之一。 更新屏幕时这个方法很有用,但它可能使应用变慢,如果不是必须的话,应该避免使用这个方法。 即便没有显式地调用 saveLayer,也可能在其他操作中间接调用了该方法。可以使用 PerformanceOverlayLayer.checkerboardOffscreenLayers 开关来检查场景是否使用了 saveLayer。 打开开关之后,运行应用并检查是否有图像的轮廓闪烁。如果有新的帧渲染的话,容器就会闪烁。 举个例子,也许有一组对象的透明度要使用 saveLayer 来渲染。 在这种情况下,相比通过 widget 树中高层次的父 widget 操作,单独对每个 widget 来应用透明度可能性能会更好。其他可能大量消耗资源的操作也同理,比如裁剪或者阴影。

透明度(Opacity)、裁剪(clipping)以及阴影(shadows)它们本身并不是个糟糕的注意。然而对 widget 树顶层 widget 的操作可能导致额外对 saveLayer 的调用以及无用的处理。

4.2.2 检查没有缓存的图像

RepaintBoundary 使用 RepaintBoundary 来缓存图片是个好主意, 当需要的时候 。 从资源的角度看,最重量级的操作之一是用图像文件来渲染纹理。 首先,需要从持久存储中取出压缩图像,然后解压缩到宿主存储中(GPU 存储),再传输到设备存储器中(RAM)。也就是说,图像的 I/O 操作是重量级的。 缓存提供了复杂层次的快照,这样就可以方便地渲染到随后的帧中。 因为光栅缓存入口的构建需要大量资源,同时增加了 GPU 存储的负载,所以只在必须时才缓存图片。 打开PerformanceOverlayLayer.checkerboardRasterCacheImages 开关可以检查哪些图片被缓存了。 运行应用来查看使用随机颜色网格渲染的图像,标识被缓存的图像。当和场景交互时,网格里的图片应该是静止的—代表重新缓存图片的闪烁视图不应该出现。 大多数情况下,开发者都希望在网格里看到的是静态图片,而不是非静态图片。如果静态图片没有被缓存,可以将其放到 RepaintBoundary widget 中来缓存。虽然引擎也可能忽略 repaint boundary,如果它认为图像还不够复杂的话。

4.2.3 检视 widget 重建性能

显示性能数据 Flutter 框架的设计使得构建达不到 60fps 流畅度的应用变得困难。通常情况下如果卡顿,就是因为每一帧被重建的 UI 比需求更多的简单 bug。Widget rebuild profiler 可以帮助调试和修复这些问题引起的 bug。 可以检视 widget inspector 中当前屏幕和帧下的 widget 重建数量。了解细节,可以参考 在 Android Studio 或类 IntelliJ 里开发 Flutter 应用 中的 显示性能数据。




5.UI 应用性能优化总结

5.1 UI 渲染

5.2 UI 调试步骤

1.在mian里面设置

  • debugDumpLayerTree ○ 查看layer树
  • debugPaintLayerBordersEnabled ○ 查看layer界限
  • debugRepaintRainbowEnabled ○ 被重新绘制的RenderObject
  • debugProfilePaintsEnabled ○ 在观测台里显示绘制树

2.profile下真机运行

3.选择Open TimeLine View,建议使用chrome打开

4.查看分析

5.3 UI 提高性能的总结

1.避免在 build() 方法中进行重复且耗时的工作,因为当父 Widget 重建时,子 Wdiget 的 build() 方法会被频繁地调用。



2.当在 State 上调用 setState()时,所有后代 Widget 都将重建。因此,将 setState() 的调用转移到其 UI 实际需要更改的 Widget 子树部分。如果改变的部分仅包含在 Widget 树的一小部分中,请避免在 Widget 树的更高层级中调用 setState()。【提高build的效率- 降低遍历的出发点】



3.当重新遇到与前一帧相同的子 Widget 实例时,将停止遍历。这种技术在框架内部大量使用,用于优化动画不影响子树的动画。请参阅 TransitionBuilder 模式和使用此原则的 SlideTransition,以避免在动画过程中重建其后代 Widget。【提高build的效率- 停止树的遍历】



4.需要更新的地方添加RepaintBoundary去设置一个独立图层,来减少图层更新节点的数量【提高paint的效率】




6.GPU 应用性能优化总结

6.1 GPU 图形渲染

因为Dart代码直接调用SKia的C和C++代码,当Dart代码能够媲美Java代码就能够达到Flutter App的性能媲美原生App。

Skia(开源图形引擎)是一个C++的开源2D向量图形处理函数库(Cairo是一个矢量库),包括字型、坐标转换、位图等等,相当于轻量级的Cairo,目前主要用于Google的Android和Chrome平台,Skia搭配OpenGL/ES与特定的硬件特征,强化显示的效果。另外,Skia是WebKit支持的众多图形平台之一,在WebKit的GraphicsContext.h/.c中有相关实现。

6.2 GPU 调试步骤

使用真机进行性能调试,Skia 有两套很不同的后端,Flutter在iOS模拟器中使用纯CPU后端,而真机设备一般使用GPU硬件加速后端,所以性能特性很不一样

1.在项目路径下运行:flutter run --profile --trace-skia

2.点击运行完成后的链接,打开的其实就是TimeLine View,但这时候需要选择All,把所有函数都勾选上

3.然后操作App,点击refresh生成渲染图表。

4.flutter 将一帧录制成SkPicture(skp)送给Skia进行渲染。
用flutter screenshot --type=skia --observatory-port=<port>捕捉skp,并利用[debugger.skia.org]()我们可以上传skp然后单步分析每一条绘图指令。

6.3 GPU 提高性能的总结

1.避免使用 Opacity widget,尤其是在动画中避免使用。请用 AnimatedOpacity 或 FadeInImage 进行代替。更多信息,请参阅:Performance considerations for opacity animation

有关将透明度直接应用于图像的示例,请参见 Transparent image,这比使用 Opacity widget 更快。
  For example:
  Container(color: Color.fromRGBO(255, 0, 0, 0.5))  👍
  Opacity(opacity: 0.5, child: Container(color: Colors.red)). 🙅



2.Clip 不会调用 saveLayer()(除非明确使用 Clip.antiAliasWithSaveLayer),因此这些操作没有 Opacity 那么耗时,但仍然很耗时,所以请谨慎使用。



3.如果大多数 children widget 在屏幕上不可见,请避免使用返回具体列表的构造函数(例如 Column() 或 ListView()),以避免构建成本。使用带有回调的惰性方法(例如ListView.builder)。



4.避免调用 saveLayer()。

【为什么 saveLayer 代价很大?】
调用 saveLayer() 会开辟一片离屏缓冲区。将内容绘制到离屏缓冲区可能会触发渲染目标切换,这些切换在较早期的 GPU 中特别慢。

下面可能触发saveLayer
  1  ShaderMask
  2  ColorFilter
  3  Chip -- might cause call to saveLayer() if disabledColorAlpha != 0xff
  4 Text -- might cause call to saveLayer() if there’s an overflowShader 
  
 避免调用 saveLayer() 的方式: 
  1: 要在图像中实现淡入淡出,请考虑使用 FadeInImage 小部件,该小部件使用 GPU 的片段着色器应用渐变不透明度。了解更多详情,请参见 Opacity 文档。
  2: 要创建带圆角的矩形,而不是应用剪切矩形,请考虑使用很多 widget 都提供的 borderRadius属性。



5.当有些widget被遮挡住了,不需要渲染了,可以使用Visibility来控制不可见。



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



7.避免在动画中剪裁。如果可能,请在动画开始之前预先剪切图像。



8.优化页面当有大量图片加载的时候,性能的消耗,比如降低图片质量来降低




参考:

  1. Flutter 应用性能优化最佳实践
  2. Flutter 的性能测试和理论(剖析你的 Flutter app)
  3. Flutter 的高性能渲染原理

👇推荐👇:

日常学习Flutter开发的积累