耗时三个月总结全网大部分Flutter性能分析的方法论,并且结合实战验证效果和自己开发经验,保姆式的一步步带你了解优化中的细节和注意点,希望能给你带来一些收获

原理

Flutter的架构

Framework使用dart实现主要提供我们开发用的API

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

Embedder是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等

Flutter线程

flutter里面有四个线程分别是UI线程,GPU线程,IO线程,Platform线程

UI线程运行dart代码,我们写的代码都是在这个线程运行

GPU线程被用于执行设备GPU的相关调用

IO线程主要功能是从图片存储(比如磁中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备

Platform线程主要负责和Engine的所有交互

我们主要关注UI线程和GPU线程的性能问题就可以了,其他两个主要跟底层交互多

Flutter视图树

Flutter视图树包含了三颗树:Widget、Element、RenderObject

  • Widget: 存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建

  • Element: 同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构

  • RenderObject: 根据Widget的布局属性进行layout,paint ,负责真正的渲染

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

例如下面这段布局代码

对应三棵树的结构如下图

 不同树对应运行的位置如下图

Flutter绘制流程

了解了这三棵树,我们再来看下页面刷新的时候具体做了哪些操作。

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

结合前面的例子,如果text文本或者image内容发生变化会触发哪些操作呢?

Widget是不可改变,需要重新创建一颗新树,build开始,然后对上一帧的element树做遍历,调用他的updateChild,看子节点类型跟之前是不是一样,不一样的话就把子节点扔掉,创造一个新的,一样的话就做内容更新,对renderObject做updateRenderObject操作,updateRenderObject内部实现会判断现在的节点跟上一帧是不是有改动,有改动才会别标记dirty,重新layout、paint,再生成新的layer交给GPU

流程如下图:

Flutter运行模式

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

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

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

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

建议在Profile模式下测试性能

实战

performance overlay

先看张效果图

开启performance overlay

方式1:在MaterialApp的属性showPerformanceOverlay设置true开启(缺点:不能动态控制显示隐藏)

方式2: 通过performance打开(动态开启关闭)

如果AS右边没有快捷键可以在tools找到打开

分析

开启showPerformanceOverlay可以看模拟器上面有两个柱状图: 上面柱状图图是GPU线程,下面树UI线程,绿色表示当前帧数据情况, 每一个柱状图横向有三格每格时间16ms,纵向是最后三百帧, 如果GPU线那帧颜色红色说明绘制过于复杂的图形,或者执行过多的GPU操作,如果CPU线程那帧颜色红色说明dart代码执行耗时操作导致ui线程阻塞。

案列

通过performance overlay我们可以快速直观发现那个页面性能有问题 具体定位问题代码需要借助DevTools工具分析具体情况,下面我通过一个案例来演示整个优化过程,通过上面原理我们知道绘制流程是animate, build,layout,paint,那么我们优化入口就是从这四个函数切入

下面是演示代码

运行效果

使用DevTools

可以直接点击快捷键打开(建议你的flutterSDK最好在2.8.1及以上,低版本的sdk会导致没有演示的部分功能按钮),

如果SDK低于2.8.1可以设置属性获取数据

VSCode开发工具(没有用vscode开发没有验证如果不行可以使用下面命令打开)

可以安装插件https://flutter.cn/docs/development/tools/devtools/vscode

使用命令打开 flutter run --profile

执行命令后点击链接就打开DevTools

打开devTools效果如下

选中select Widget Mode可以使我们直观看到我们写的代码的widget树结构,点击 右边Layout Explorer里面的图层模拟器可以显示对应的widget。

优化Builds

点击Performance里面的EnhanceTracing 选中Track Widget Builds

上面的柱状图是每帧的耗时情况,我们点击一帧看看里面具体情况

我们可以看到build的层级很长,实际情况我们只需要从改变显示计数器的Text开始build就可以了。

提取Text前代码                                   

提取后代码

提取后效果

可以看到提取后Build的层级只有两层了,时间也少了很多

优化Paint

点击Performance里面的EnhanceTracing 选中Track Paints

点击一帧我们看具体分析数据

 我们可以看到paint绘制层级也是很多,实际我们只需要绘制变化的Text,其他没有边的widget并不需要paint,我们可以使用RepaintBoundary包裹Text,利用RepaintBoundary提高paint效率,它为经常发生显示变化的内容提供一个新的隔离layer,新的layer paint不会影响到其他layer,

变化代码

使用后效果

我们可以看到piant层级只有一层了 ,时间也少了很多了。

AS也有些快捷键可以看到build的层级,例如Flutter Performance勾选Track widget reBuilds

 小结:

  • 提高build效率,setState刷新数据尽量下发到底层节点

  • 提高paint效率,RepaintBoundry创建单独layer减少重绘区域

Timeline工具

Timeline工具其实是DevTools工具的前世版本,那我们都介绍了DevTools工具了为什么还要介绍Timeline工具呢,因为DevTools工具现在功能还不完善,也不够稳定,所有我们想分析更多数据暂时还需要借助Timeline工具虽然他不是那么好用,后期稳定了就可以开心的用DevTools了。

注意:先配置你需要测试的数据,默认Timeline是不会记录构建具体数据的

执行命令 flutter run --profile 点击 Timeline链接

 打开后效果图

我们主要分析UI性能所以点击timeline 

可以看到左边是四大线程,右边是一帧的执行情况,如果你想测试那个页面性能就在那个页面多操作下,选中Flutter Developer,再点击有右上角Refresh刷新,再利用缩放移动显示下面的build,layout,paint的数据情况(选中向下箭头图标,当你鼠标点击图表左移动缩小,鼠标点击图表右移动放大;选中十字图标,就图表就可以跟着鼠标移动)优化思路跟Devtools工具一样。

检查屏幕之外的视图

主要检查调用SaveLayout的Widget,因为调用saveLayout会再GPU里面开启一个新的绘图缓存区,切换绘图目标,这个操作会导致GPU非常耗时,所以我们尽量少用这些Widget组件,使用其他Widget替代实现。

开启检查

代码 

当我们滑动时,顶部AppBar会不断闪烁,说明调用savelayout,

知道时问题所在,那就开始分析优化方案了,使用CupertinoNavigationBar默认不设置透明度属性是没有半透明玻璃效果,但是却导致我们性能下降了,如果ui原型只是普通背景颜色就直接使用Scaffold的AppBar ,如果需要我们可以给AppBar设置半透明效果最大还原ui需要的效果,当然需求一定需要,那就需要在性能和需求之间做平衡了。

优化方案

检查图片是否光栅格化缓存

从资源的角度看,另一类非常消耗性能的操作是渲染图像,因为图像渲染会涉及 I/O、GPU 存储以及不同通道的数据格式转换,因此渲染过程的构建需要消耗大量资源。为了缓解 GPU 的压力,Flutter 提供了多层次的缓存快照,这样 Widget 重建时就无需重新绘制静态图像了,开启后如果出现闪烁框就是没有缓存,我们可以使用RepaintBoundary包裹,让系统缓存起来

总结

1:开启flutter代码规范,使用flutter团队代码规范使用我们代码更性能更高效,不知道怎么开启代码规范的可以看下面这篇

https://blog.csdn.net/qq_36237165/article/details/123325012?spm=1001.2014.3001.5501

2:使用const 修饰widget,这样可以使用系统知道我们的widget可以缓存下来,提高rebuild的效率。

3:我们再开发中经常会把复杂的wiedget提取出一个方法使用代码看起来比较简洁,其实Flutter团队建议封装成个StatelessWidget性能会更好,你封装成方法底层框架不是你这方法是否需要复用所以他是不会缓存起来,如果你封装成一个StatelessWidget系统就知道你这个Widget是可以缓存复用的,间距提供效率。

https://stackoverflow.com/questions/53234825/what-is-the-difference-between-functions-and-classes-to-create-reusable-widgets

4:使用状态管理框架对widget实现局部刷新,常用的状态管理框架有provider,mobx,get,前面两个都是Flutter社区推荐的。

5:避免更改组件树的结构和组件的类型,树结构改变会导致树重新rebuild,

有如下场景,有一个 Text 组件有可见和不可见两种状态

 如果需要返回两种不同类型的widget,可以把变化的widget封装到StatefulWidget里面,如果你使用状态管理框架可以封装到StatelessWidget里面,

 6:ListView是我们最常用的组件之一,用于展示大量数据的列表。如果展示大量数据请使用 ListView.builder 或者 ListView.separated,实现复用,如果item布局高度可以固定可以使用itemExtent 属性提高性能(提高系统测量效率)

7:AnimatedBuilder 、TweenAnimationBuilder 等一类的组件的问题,这些组件都有一个共同点,带有 builder 且其参数重有 child。以 AnimatedBuilder 为例,如果 builder 中构建的树中包含与动画无关的组件,将这些无关的组件当作 child 传递到 builder 中比直接在 builder 中构建更加有效

8:避免调用 saveLayer(调用 saveLayer() 会开辟一片离屏缓冲区。将内容绘制到离屏缓冲区可能会触发渲染目标切换,这些切换在较早期的 GPU 中特别慢)

1 ShaderMask

2 ColorFilter

3 Chip -- might cause call to saveLayer() if disabledColorAlpha != 0xff (简单的圆角效果可以使用Container 实现)

4 Text -- might cause call to saveLayer() if there’s an overflowShader

5 Opacity减少使用特别是动画中,淡入效果可以使用AnimatedOpacity和FadeInImage,透明效果可以设置widget的背景颜色实现

9:如果你要执行一些比较耗时操作建议使用isolate,如果任务比较耗时使用异步也会导致ui卡顿

当你执行上面同步和异步任务发现加载进度动画会被卡住,使用isolate不会导致加载动画卡顿

10:优先使用StateLessWidget,而不是全部用StateFulWidget

11:对于频繁更新的控件(比如倒计时,秒表),使用RepaintBoundary隔离它,让他在一个独立的paint区域 

12:简单的ui样式,可以自己封装实现

 代码:

刷新率对比:

 使用自定义的按钮刷新率更高,因为系统封装的按钮功能比较丰富里面的Widget嵌套很多,但是实际业务需求可能只需要些普通效果,我们为了提升性能可以根据需求自定义封装达到突破性能瓶颈

13:建议及时更新Flutter版本,每个新的版本都会有很多性能上的优化,白嫖Flutter团队带来的性能优化

讨论

-   流畅:一帧耗时低于 18ms
-   良好:一帧耗时在 18ms-33ms 之间
-   轻微卡顿:一帧耗时在 33ms-67ms 之间
-   卡顿:一帧耗时大于 66.7ms

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐