flutter渲染原理及优化

986 阅读8分钟

flutter渲染的类概念

一切皆Widget

Widget 是空间实现的基本逻辑单位,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。

Flutter 将 Widget 设计成不可变的,所以当视图渲染的配置信息发生变化时,Flutter 会选择重建 Widget 树的方式进行数据更新,以数据驱动 UI 构建的方式简单高效。缺点是,因为涉及到大量对象的销毁和重建,所以会对垃圾回收造成压力。但是,Widget 本身并不涉及实际渲染位图,所以它只是一份轻量级的数据结构,重建成本很低。

我们写flutter代码的时候,用的各个控件都是Widget类的子类,我们写继承的类一般StatefulWidget和StatelessWidget,但是Wiget并不是真正的渲染对象。

Element

Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。

Element 同时持有 Widget 和 RenderObject,无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正干活儿的只有 RenderObject。

RenderObject

RenderObject 是主要负责实现视图渲染的对象。

渲染对象树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。

Flutter中的三棵树

即:Widget树、Element树和RenderingObject树

总结出三者的关系是:配置文件 Widget 生成了 Element,而后创建 RenderObject 关联到 Element 的内部 renderObject 对象上,最后Flutter 通过 RenderObject 数据来布局和绘制。 理论上你也可以认为 RenderObject 是最终给 Flutter 的渲染数据,它保存了大小和位置等信息,Flutter 通过它去绘制出画面。

Widget只是显示的数据配置,所以相对而言是轻量级的存在,而 Flutter 中对 Widget 的也做了一定的优化,所以每次改变状态导致的 Widget 重构并不会有太大的问题。 RenderObject 就不同了,RenderObject 涉及到布局、计算、绘制等流程,要是每次都全部重新创建开销就比较大了。只渲染当前的组件和子组件,触发 SetSate() 后,其所有子组件都将重新渲染。

setState 干了什么?

我们写代码时经常用到setState来刷新界面,setState做了什么呢?其实是调用了 markNeedsBuild ,markNeedsBuild 内部会标记 element 为 diry,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这可以也看出 setState 并不是立即生效的。

flutterstudio.app 可以拖动widget到模拟器中,就可以生成相关的布局代码,自己手动复制粘贴就可以了。

flutter的树与渲染阶段

Widget和Element树中的每个对象都是一一对应的,列如Container,它是实现自己内部的DecorateBox来形成绘制对象,对应Element树上的就是RenderObjectElement,然后再生成对应的RenderDecoratedBox,如下图: image.png

image.png

image.png

卡顿与CPU、GPU关联

CPU即中央处理器,GPU即图形处理器

关系

CPU 的任务繁多,做逻辑计算外,还要做内存管理、显示操作,因此 在实际运算的时候性能会大打折扣,在没有 GPU 的时代,不能显示复 杂的图形,其运算速度远跟不上今天复杂三维游戏的要求。即使 CPU 的工作频率超过 2GHz 或更高,对它绘制图形提高也不大。这时 GPU 的设计就出来了

结构

16a593a792c895f1.jpg

由图分析 CPU GPU :

黄色的 Control 为控制器,用于协调控制整个 CPU 的运行,包括取出指令、控制其它模块的运行等;

绿色的 ALU (Arithmetic Logic Unit) 是算术逻辑单元,用于进行数学、逻辑运行;

橙色的 Cache 和 DRAM 分别为缓存和 RAW,用于存储信息;

60HZ刷新频率由来

12 fps: 由于人类眼睛的特殊生理结构,如果所看到的画面之帧率高于每秒约 10 - 12 帧的时候,就会认为是连贯的;

24 fps: 有声电影的拍摄及播放帧率均为 24 帧,对一般人而言可以接受;

30 fps: 早期的高动态电子游戏,帧率少于每秒 30 帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低;

60 fps: 在于手机交互过程中,如触摸和反馈 60 帧以下,肉眼是能感觉出来的。60 帧以上不能察觉变化。当低于 60 fps 时感觉画面有卡顿现象。

手机系统每隔 16ms 发出信号 (1000 ms / 60 = 16.66 ms) ,触发对 UI 进行渲染, 如果每次渲染都成功这样就能够达到流畅的画面所需要的 60 fps ,为了能够实现 60 fps ,这意味着计算渲染的大多数操作都必须在 16ms 内完成,当我们的任务处理时间大于16ms,就会出现丢帧现象,动画就会不流畅,肉眼就会发现卡顿。

性能

概述

我们的UI系统能够多么有效的每16毫秒所运行代码命令的内容

高效的原因

flutter官方工程师给的图片介绍,只说了安卓能够直接调用底层的skia开源图形渲染引擎,完全绕过了Android框架,RN是调用了Android框架的,所以肯定效果差一点,这样就是变成了Flutter框架和Android框架之间的性能比较,但是官方并没有给出iOS的原因。

image.png

flutter中widget树是不可改变的,也包括了树的父子节点关系也是不可改变的,所以要改变的话,只能把原来的widget树销毁,再创建新的树。

更新widget树,会调用Element的updateChild()方法。element会从根节点进行新旧比较,系统会查看当前element的子节点,看是否相同,如果不同,扔掉原来的创建新的,如果相同,就对原来的子节点进行更新。如果Element是component类型,会对Element的statefulwidget和statelesswidget做更新,叫build,会继续遍历子节点,找出子节点不同的地方,进行标脏(dirty)。每个UI的实现都会有对应的RenderObject类型,更新就会调用updateRenderObject,会把RenderObject的所有setter方法执行,set方法内部的比较如下图:

分析

image.png

上面这张图是我们调试时用工具显示的性能图片,上半部分是CPU性能,下半部分是CPU性能,当下半部分报红时,一般都是由于我们代码写法的问题,过多的控件刷新和处理,这是就需要我们去优化代码结构。上半部分报红时,一般都是我们调用了消耗高性能的函数、绘制复杂的图形,这个就需要去深入到底层,去优化一些不必要的图形渲染。

调试

build调试: flutter run -- profile skia调试:flutter run -- profile --trace-skia 捕捉SkPicture分析每一条绘图指令: flutter screenshot --type=skia --observatory-port=

几乎全部的 Flutter 应用性能调试都应该在真实的 Android 或者 iOS 设备上以 profile mode 进行。通常来说,调试模式或者是模拟器上运行的应用的性能指标和发布模式的表现并不相同。应该考虑在用户使用的最慢的设备上检查性能。

如何提高build效率

1、减少遍历 如果你的一个界面大部分都是静态的,只有其中的一个控件是动态的,那应该把动态的节点单独抽出来,这边再重构的遍历中只会遍历这个控件,而不是去遍历整个页面。

2、停止遍历 利用RepaintBoundary这个widget封装,在child下的widget就不会进行再次的遍历。

3、用Visibility来隐藏一些控件

优化建议

1、尽量用const,不然setState()后都会重构

2、避免用statefulWidget

3、组件透明度,因此尽量不用Opacity Widget,ShaderMask

4、尽量不要调用 saveLayer(),clipPath() 如下这几个组件,底层都会触发 saveLayer() 的调用,同样也都会导致性能的损耗: ShaderMask、ColorFilter、Chip,当 disabledColorAlpha != 0xff 的时候,会调用 saveLayer()。Text,如果有 overflowShader,可能调用 saveLayer()

5、推荐使用ListView.builder来动态实现列表,而不是直接使用ListView静态创建。

6、避免将一些耗时计算放在UI线程,我们可以把耗时计算放到Isolate去执行(多线程)。

7、尽量给Widget指定大小,避免不必要的Layout计算。比如ListView的itemExtent使用。

8、可以使用一些Curves曲线动画(先快后慢)。这样在相同的时间内,视觉上会比线性动画显得快,让人觉得流畅。