阅读 1996

Flutter 高性能原理浅析

这是我第三篇Flutter相关博客 欢迎 查看我的前两篇 Flutter实现篇

前言

Flutter是Google用以帮助开发者在Ios和Android两个平台开发高质量原生应用的全新移动UI框架.我开始认识Flutter时,经历了三个Flutter重要历史版本.

  • 2018年2月27日,在2018世界移动大会上,Google发布了Flutter的第一个Beta版本。
  • 2018年6月21日,在全球大前端技术大会上,发布了第一个Release Perview 1版本。
  • 2018年12月5日第一个Release 1.0版本发布.

此后越来越多的人开始关注到Flutter。

在 Flutter 诞生之前,已经有许多跨平台 UI 框架的方案,比如基于 WebView 的 Cordova、AppCan 等,还有使用 HTML+JavaScript 渲染成原生控件的 React Native、Weex 等。Flutter 则开辟了一种全新的思路,从头到 尾重写一套跨平台的 UI 框架,包括 UI 控件、渲染逻辑甚至开发语言。

Flutter框架

从图中可以看出 Flutter主要被分为两层 Framework层和Flutter Engine.

Framework层全部使用Dart编写,有完整UI框架的API,并预写了Android(MaterialDesign)和IOS的(Cupertino)风格的UI,极大方便了开发移动端.

Framework 底层是 Flutter 引擎, 引擎主要负责图形绘制 (Skia)、 文字排版 (libtxt) 和提供 Dart 运行时, 引擎全部使用 C++实现.

Flutter高性能原理

与其他跨平台框架对比

在看Flutter框架前,我们先看一下其他跨平台框架的设计

看Hybrid的架构,我们可以知道UI层的渲染是基于Webview去渲染,他的性能取决于webview的渲染性能,目前已知webview渲染性能 < NativeUI的性能

RN/Weex 的架构中,是基于Native的UI框架去适配,中间多了一层js转NativeUI的过程

而Flutter不需要中间层(Webview,js 转NativeUI这个过程),他是基于图像渲染引擎去直接绘制UI.

Dart 对于UI框架的高性能支持

我们知道Flutter的Framework层是使用了Dart语言编写,那Dart语言有哪些优势呢?下面分为几个点来阐述

Dart内存分配机制

DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程

Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。

Dart 垃圾回收机制

Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存如图.

整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种 多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化,非常适合Flutter框架中大量Widget重建的场景.

Dart 编体积优化,及编译JIT和AOT支持

代码体积优化(Tree Shaking),编译时只保留运行时需要调用的代码(不允许反射这样的隐式引用),所以庞大的Widgets库不会造成发布体积过大。

Dart支持两种编译模式:

  • JIT编译 Just In Time Compiler -即时编译
  • AOT编译Ahead Of Time 预编译

在debug模式下使用JIT编译,生成srcipt/bytecode进行解释执行,可以支持HotReload(热重载),修改代码,保持即可在设备上看到效果. 而在Release下 AOT编译生成Machine Code,高效的运行.

Dart 单线程 异步消息机制

客户端交互简述

对于移动端的交互来说,大多数情况下都是在等待状态,等待网络请求,等待用户输入等.那么设想一下,发起一个网络请求只在一个线程中可以进行吗?当然网络请求肯定是异步的(注意这里说的异步而多线程并非一个概念.),事实验证是可以的,Flutter就采用了Dart这种单线程机制,省去了多线程上下文切换带来的性能损耗.(对于高耗时操作,也同样支持多线程操作,通过Isolate开启,不过注意这里的多线程,内存是无法共享的.)

Dart 异步消息原理

当一个Dart的方法开始执行时,他会一直执行直至达到这个方法的退出点。换句话说Dart的方法是不会被其他Dart代码打断的。 当一个Dart应用开始的标志是它的main isolate执行了main方法。当main方法退出后,main isolate的线程就会去逐一处理消息队列中的消息。

有了消息队列,然后有了循环去读取消息队列中的消息,就可以有单线程去执行异步消息的能力. 一般的消息使用dart:async中使用Future来支持异步消息.

Flutter Engine 高性能

在讲Flutter Engin层时,我们先讲一下屏幕绘制的原理.

屏幕绘制原理

我们都知道显示器以固定的频率刷新,比如 iPhone的 60Hz、iPad Pro的 120Hz。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以 60Hz的屏幕就会一秒内发出 60次这样的信号。

并且一般地来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照 VSync信号从帧缓冲区取帧数据传递给显示器显示。

作为一个专职Android开发,看过Android的绘图机制,通过SurfaceFlinger 和HAL层之间的工作机制发现和Flutter的很像,那么IOS的如何呢?个人推测屏幕的绘图机制是一样的,只是不同平台有不同实现.

Flutter Engine的渲染机制

Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU.

所以 Flutter并不关心显示器、视频控制器以及 GPU具体工作,它只关心 GPU发出的 VSync信号,尽可能快地在两个 VSync信号之间计算并合成视图数据,并且把数据提供给 GPU.

Flutter Framework层的绘图机制

UI树原理

在 Flutter 界面渲染过程分为 3 个阶段: 布局、绘制、合成.

而布局阶段,有三个重要的对象.RenderObject、Element、Widget.

  • Widget是开发经常接触的控件,默认是只读的.

  • Element 是 Flutter 用来分离控件树和真正的渲染 对象的中间层, 控件用来描述对应的 element 属性,控件重建后可能会复用同一个 element.

  • RenderObject 负责提供配置信息并创建具体的 Element。

Element 持有真正负责布局、 绘制和碰撞测试 (hit test) 的 RenderObject 对象.

那么这样,如果控件的属性发生了变化 (因为控件的属性是只 读的, 所以变化也就意味着重新创建了新的控件树), 但是其树上每个节点的类型没有变化时, element 树和 render 树可以完全重用原来的对象 (因为 element 和 render object 的属性都是可变的)

布局原理

传统布局,如Android可能需要多次Measure,计算宽高。Flutter 采用约束进行单次测量布局. 整个布局过程中只需要深度遍历一次,极大的提升效能。

渲染对象树中的每个对象都会在布局过程中接受父 对象的 Constraints 参数,决定自己的大小, 然后父对象 就可以按照自己的逻辑决定各个子对象的位置,完成布局过程.

子对象不存储自己在容器中的位置, 所以在它的位置发生改变时并不需要重新布局或者绘制. 子对象的位 置信息存储在它自己的 parentData 字段中,但是该字段由它的父对象负责维护,自身并不关心该字段的内容。

同时也因为这种简单的布局逻辑, Flutter 可以在某些节 点设置布局边界 (Relayout boundary), 即当边界内的任 何对象发生重新布局时, 不会影响边界外的对象, 反之亦然.

参考资料

文中参考的资料如下

关注下面的标签,发现更多相似文章
评论