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

在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 设备上以分析模式进行。
一般来讲,调试模式或者是模拟器上运行的应用的性能指标和发布模式的表现并不相同。 
应该考虑在用户使用的最慢的设备上检查性能。
复制代码

  • 为何应该在真机上运行:html

    • 各类模拟器使用的硬件并不相同,所以性能也不一样—模拟器上的一些操做会比真机快,而另外一些操做则会比真机慢。android

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

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





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 页面来获取安装和使用指导。)github

4.2 定位 GPU 图表中的问题

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

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

4.2.1 检查屏幕以外的视图

saveLayerjson

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

透明度(Opacity)、裁剪(clipping)以及阴影(shadows)它们自己并非个糟糕的注意。然而对 widget 树顶层 widget 的操做可能致使额外对 saveLayer 的调用以及无用的处理。api

4.2.2 检查没有缓存的图像

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

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开发的积累

相关文章
相关标签/搜索