Flutter 的 UI渲染性能很好。在生产环境下,Flutter 将代码编译成机器码执行,并充分利用 GPU 的图形加速能力,因此使用Flutter 开发的移动应用即使在低配手机上也能实现每秒 60 帧的 UI渲染速度。
Flutter 渲染引擎使用 C++ 编写,包括高效的 Skia 2D 渲染引/擎,Dart 运行时和文本渲染库。
与React Native的对比
由于RN的本质是通过JavaScript VM调用原生接口,通信相对比较低效,而且框架本身不负责渲染,而是是间接通过原生进行渲染的。
也就是说它必须通过某些桥接的方式先转成原生进行调用,之后再进行渲染。
Flutter 自带渲染引擎,省出了中间商的挣差价:
- Flutter利用Skia绘图引擎,直接通过CPU、GPU进行绘制,不需要依赖任何原生的控件
- Android操作系统中,我们编写的原生控件实际上也是依赖于Skia进行绘制,所以flutter在某些Android操作系统上甚至还要高,于原生(因为原生Android中的Skia必须随着操作系统进行更新,而Flutter SDK中总是保持最新的)
计算机绘图原理
屏幕显示器一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,这时显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,CPU/GPU 向 Buffer 中生成图像,屏幕从 Buffer 中取图像、刷新后显示。
理想的情况是帧率和刷新频率相等,每绘制一帧,屏幕显示一帧。所以本质上我们看到的都是一个一个的图像,只是在一秒内刷新了60次
双重缓存存在的问题:当 CPU/GPU 绘制一帧的时间过长(比如超过16ms)时,会产生 Jank(画面停顿,甚至空白)。为了解决这个问题,就有Vsync信号
- CPU生成蓝色B的数据,由GPU进行B的绘制,但是这个过长由于过长,那么第二个A就产生了Jank。
- B在屏幕上显示之后,发出Vsync信号,A开始绘制,但是由于绘制时间过长,第二个B位置又产生了Jank
- 在第二个A展示,Vsync信号发出后,直接绘制C Buffer
- 在第一个B展示,Vsync信号发出后,绘制A Buffer
- 因为C已经在缓存中,可以直接从缓存中取出C Buff来进行展示,依次类推
其实本质是在每次Vsync信号发出后,多缓存一个Buffer作为备用,从这点上来盾有点类似于打点滴的时使用的输液器
Flutter的图象绘制流程
- GPU将信号同步到 UI线程
- UI线程用Dart来构建图层树
- 图层树在GPU 线程进行合成
- 合成后的视图数据提供给Skia引擎 (Skia就是 Flutter向GPU提供数据的途径)
- Skia 引擎通过OpenGL 或者 Vulkan将显示内容提供给GPU
Flutter Framework
纯 Dart实现的 SDK,类似于 React在 JavaScript中的作用。
它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。
纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。
我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。
Flutter Engine: 纯 C++实现的 SDK,其中包括 Skia引擎、Dart运行时、文字排版引擎等。
它是 Dart的一个运行时,它可以以 JIT 或者 AOT的模式运行 Dart代码。
这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。
GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构,绘制好的抽象视图数据结构在GPU线程中进行图层合成(在Flutter Engine层的工作),然后提供给Skia引擎渲染为GPU数据,最后通过OpenGL或者 Vulkan提供给 GPU。
Framework的最底层叫做Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法。
绘制库(Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等。
Animation是动画相关的类。
Gesture提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器。
如果使用Flutter提供的控件进行开发,则需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接调用Render层,则需要使用RenderingFlutterBinding,而我们平时看到的大多数开发案例都是使用WidgetsFlutterBinding的。
Flutter在Android和iOS两个平台,还提供了两套设计语言的控件实现Material & Cupertino,以提供更好的用户体验。
Widget
Widget里面存储了一个视图的配置信息,包括布局、属性等。它是一份轻量的数据结构,在构建时是结构树,它不参与直接的绘制,所以说Widget仅仅是配置文件,Flutter团队对它做了优化,频繁的创建/销毁它们,都不会存在明显的性能问题。
Widget包含StatelessWidget和StatefullWidget两个常用类,StatelessWidget是无状态变化的类,需要重新展示时得重新new,StatefullWidget是有状态变化的类,很类似react的设计理念,state存放于中间,通过调用state.setState()才会触发该节点及以下整个子树更新。
Element
void attachRootWidget(Widget rootWidget) { _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement); }
Element是Widget的抽象,当一个Widget首次被创建的时候,那么这个Widget会通过Widget.createElement,创建一个element,挂载到Element Tree遍历视图树。在attachRootWidget函数中,把 widget交给了 RenderObjectToWidgetAdapter这座桥梁,Element创建的同时还持有 Widget和 RenderObject的引用。构建系统通过遍历Element Tree来创建RenderObject,每一个Element都具有一个唯一的key,当触发视图更新时,只会更新标记的需变化的Element。类似react中setState后虚拟dom树的更新。
RenderObject
RenderObject作为UI视图的描述方式,其中含有4个重用的属性和方法,
- constraints: 从 parent 传递过来的约束。
- parentData: 这里面携带的是 parent 渲染 child 的时候所用到的数据。
- performLayout():此方法用于布局所有的 child。
- paint():这个方法用于绘制自己或者 child。
后面会看到具体出现场景。
在 RenderObject树中会发生 Layout、Paint的绘制事件(下面会具体讲到),大部分绘图性能优化发生在这里,RenderObject Tree构建为Canvas的所需描述数据,加入到Layer Tree中,最终在Flutter Engine中进行视图合成并光栅化交给GPU。
接下来看下Flutter Widget渲染的三个阶段:
- Layout(布局的计算):确定每个子widget大小和在屏幕中的位置。
- Paint(视图的绘制):为每个子widget提供canvas,让他们绘制自己。
- Composite(合成):所有widget合成到一起,交给GPU处理。
Layout 布局
- 父控件(parent)将布局约束传递给子控件(child),父控件通过传递Containers参数,告诉子控件自己的大小(布局约束),以此来决定子控件的位置。
- 子控件将布局详情上传给父控件,并继续向下约束子控件,子控件的位置不存储在自己的容器(布局详情)中,而是存储在自己的parentData字段里,所以当他的位置发生变化时,并不需要重新布局或绘制。
例如:parent会给child一个约束,最大宽度500px(布局约束)、最大高度500px(布局约束),child会说我只用100px(布局详情),并将其传递给parent,parent会继续向上传递,直到root widget为止。所以布局约束数据的传递顺序是自上而下,和web一样,布局约束条件和布局详情都取决于盒子模型协议和滑动布局协议。
性能优化(布局)
在上面的布局过程中,视图会不断更新,也就不断的触发布局和绘制,这会很损耗性能,所以这里也就到了之前说的大部分绘图性能优化的发生地方。Flutter可以在某些节点设置布局边界 Relayout boundary(Paint过程同样可以设置 Repaint (重新粉刷) boundary 进行优化),需要开发人员自己设置,边界内的控件发生重新布局或绘制时,不会影响边界外的控件。
Paint 绘制
布局完成后,每个节点就会有各自的位置和大小,然后Flutter会把所有Widget绘制到不同的图层上
在进行绘制时会自上而下,先绘制自身,然后向下绘制子节点,原本会统一绘制在绿色图层上,当绘制到节点“4”时,由于节点“4”可能是视频,需要单独占据一个图层(黄色图层),这样就会导致节点“2”的前景部分需要重绘,而影响到了后面的节点“6”一起重绘,占据到蓝色图层。
性能优化(绘制)
为避免这种情况,Flutter提供了重绘边界 Repaint (重新粉刷) boundary,设置了重绘边界后,Flutter会强制切换到新的图层,避免之间的相互影响