React Native 最终渲染工作交还给了系统,虽然同样使用类HTML JS的UI构建逻辑,但是最终会生成对应的自定义原生控件,以充分利用原生控件相对于WebView的较高的绘制效率。
不仅框架本身需要处理大量平台相关的逻辑,随着系统版本变化和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现,这样框架的跨平台特性就会大打折扣。
Flutter 从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。
Flutter所使用的Dart语言同时支持AOT和JIT运行方式,JIT模式下还有一个备受欢迎的开发利器“热刷新”(Hot Reload)
Flutter通过将新的代码注入到正在运行的DartVM中,来实现Hot Reload这种神奇的效果,在DartVM将程序中的类结构更新完成后,Flutter会立即重建整个控件树,从而更新界面。并不是所有的代码改动都可以通过热刷新来更新:
1.编译错误,如果修改后的Dart代码无法通过编译,Flutter会在控制台报错 2.控件类型从StatelessWidget到StatefulWidget的转换,因为Flutter在执行热刷新时会保留程序原来的state 3.全局变量和静态成员变量,这些变量不会在热刷新时更新。 4.修改了main函数中创建的根控件节点,Flutter在热刷新后只会根据原来的根节点重新创建控件树,不会修改根节点。 5.某个类从普通类型转换成枚举类型,或者类型的泛型参数列表变化,都会使热刷新失败。
热刷新无法实现更新时,执行一次热重启(Hot Restart)就可以全量更新所有代码,同样不需要重启App,区别是restart会将所有Dart代码打包同步到设备上,并且所有状态都会重置。
Flutter重写了一套跨平台的 UI 框架,渲染引擎是依靠 Skia 图形库实现 Flutter 中的控件树直接由渲染引擎和高性能本地 ARM 代码直接绘制,不需要通过中间对象(Web 应用中的虚拟 DOM 和真实 DOM,原生 App 中的虚拟控件和平台控件)来绘制
Flutter插件 Flutter使用的Dart语言无法直接调用Android系统提供的Java接口,这时就需要使用插件来实现中转。Flutter官方提供了丰富的原生接口封装:
Dart本身提供了三种运行方式: 1.使用Dart2js编译成JavaScript代码,运行在常规浏览器中(Dart Web)。 2.使用DartVM直接在命令行中运行Dart代码(DartVM)。 3.AOT方式编译成机器码,例如Flutter App框架(Flutter)。
最终选择Dart作为开发语言主要有几个原因:
1.健全的类型系统,同时支持静态类型检查和运行时类型检查。 2.代码体积优化(Tree Shaking),编译时只保留运行时需要调用的代码(不允许反射这样的隐式引用),所以庞大的Widgets库不会造成发布体积过大。 3.丰富的底层库,Dart自身提供了非常多的库。 4.多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化。 5.跨平台,iOS和Android共用一套代码。 6.JIT & AOT运行模式,支持开发时的快速迭代和正式发布后最大程度发挥硬件性能。
DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程:
Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。
Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存:
整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框架中大量Widget重建的场景。
Flutter架构 从下到上依次为:Embedder(嵌入器)、Engine、Framework。
Embedder 是嵌入层,做好这一层的适配 Flutter 基本可以嵌入到任何平台上去; Engine 层主要包含 Skia、Dart 和 Text。Skia 是开源的二位图形库;Dart 部分主要包括 runtime、Garbage Collection、编译模式支持等;Text 是文本渲染。Framework 在最上层。
Flutter 流水线包括 7 个步骤:
在渲染阶段,控件树(widget)会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。
在布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将自己的约束传递给子节点,子节点根据接收到的约束来计算自己的大小,然后将自己的尺寸返回给父节点。整个过程中,位置信息由父节点来控制,子节点并不关心自己所在的位置,而父节点也不关心子节点具体长什么样子。
为了防止因子节点发生变化而导致的整个控件树重绘,Flutter 加入了一个机制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 会被自动创建
例如,控件被设置了固定大小(tight constraint)、控件忽略所有子视图尺寸对自己的影响、控件自动占满父控件所提供的空间等等。很好理解,**就是控件大小不会影响其他控件时,就没必要重新布局整个控件树。**有了这个机制后,无论子树发生什么样的变化,处理范围都只在子树上。
在确定每个空间的位置和大小之后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,然后把不同的 RenderObject 绘制到不同的图层上。
这时有可能出现一种特殊情况,如下图所示节点 2 在绘制子节点 4 时,由于其节点 4 需要单独绘制到一个图层上(如 video),因此绿色图层上面多了个黄色的图层。之后再需要绘制其他内容(标记 5)就需要再增加一个图层(红色)。再接下来要绘制节点 1 的右子树(标记 6),也会被绘制到红色图层上。所以如果 2 号节点发生改变就会改变红色图层上的内容,因此也影响到了毫不相干的 6 号节点。
为了避免这种情况,**Flutter 的设计者这里基于 Relayout Boundary 的思想增加了 Repaint Boundary。**在绘制页面时候如果遇见 Repaint Boundary 就会强制切换图层。
如下图所示,在从上到下遍历控件树遇到 Repaint Boundary 会重新绘制到新的图层(深蓝色),在从下到上返回的时候又遇到 Repaint Boundary,于是又增加一个新的图层(浅蓝色)。
Repaint Boundary 并不会像 Relayout Boundary 一样自动生成,而是需要我们自己来加入到控件树中。
【Widget】控件层
所有控件的基类都是 Widget,Widget 的数据都是只读的, 不能改变。所以每次需要更新页面时都需要重新创建一个新的控件树。每一个 Widget 会通过一个 RenderObjectElement 对应到一个渲染节点(RenderObject),可以简单理解为 Widget 中只存储了页面元素的信息,而真正负责布局、渲染的是 RenderObject。
图 7: Widget、Element 和 Render 之间的关系
如果想把方形的颜色换成黄色,将圆形的颜色变成红色,由于控件是不能被修改的,需要重新生成两个新的控件 Rectangle yellow 和 Circle red。由于只是修改了颜色属性,所以 Element 和 RenderObject 都被重用,而之前的控件树会被释放回收。
那么如果把红色圆形变成三角形又会怎样呢?由于这里发生变化的是类型,所以对应的 Element 节点和 RenderObject 节点都需要重新创建。但是由于黄色方形没有发生改变,所以其对应的 Element 节点和 RenderObject 节点没有发生变化。
最后是【Material】 & 【Cupertino】,这是在 Widget 层之上框架为开发者提供的基于两套设计语言实现的 UI 控件,可以帮助我们的 App 在不同平台上提供接近原生的用户体验。
StatelessWidget:内部没有保存状态,UI界面创建后不会发生改变; StatefulWidget:内部有保存状态,当状态发生改变,调用setState()方法会触发StatefulWidget的UI发生更新,对于自定义继承自StatefulWidget的子类,必须要重写createState()方法。
三棵树
Widget:负责创建Element,决定Element是否需要更新,Flutter Framework通过差分算法比对Widget树前后的变化,决定Element的State是否改变。当重建Widget树后并未发生改变, 则Element不会触发重绘
Element:表示Widget配置树的特定位置的一个实例,同时持有Widget和RenderObject,负责管理Widget配置和RenderObject渲染。Element状态由Flutter Framework管理, 开发人员只需更改Widget即可。
RenderObject:表示渲染树的一个对象,负责真正的渲染工作,比如测量大小、位置、绘制等都由RenderObject完成。
渲染原理
UI线程完成布局、绘制操作,生成Layer Tree;GPU线程执行合成并光栅化后交给GPU来处理,其中几个关键步骤:
Animate: 遍历_transientCallbacks,执行动画回调方法; Build: 对于dirty的元素会执行build构造,没有dirty元素则不会执行,对应于buildScope() Layout: 计算渲染对象的大小和位置,对应于flushLayout(),这个过程可能会嵌套再调用build操作; Compositing bits: 更新具有脏合成位的任何渲染对象, 对应于flushCompositingBits(); Paint: 将绘制命令记录到Layer, 对应于flushPaint(); Compositing: 将Compositing bits发送给GPU, 对应于compositeFrame();
Flutter产物分为Dart业务代码和Engine代码各自生成的产物,图中的Dart Code包含开发者编写的业务代码,Engine Code是引擎代码
一份Dart代码,可编译生成双端产物,Android产物是由vm、isolate各自的指令段和数据段以及flutter.jar组成的app.apk,iOS产物是由App.framework和Flutter.framework组成的Runner.app。
frontend_server前端编译器,将dart代码转换为AST(抽象语法树),并生成app.dill格式的dart kernel
Flutter TaskRunner
Flutter的任务队列处理机制跟Android的消息队列处理相通,只不过Flutter分为Task和MicroTask两种类型,引擎和Dart虚拟机的事件以及Future都属于Task,Dart层执行scheduleMicrotask()所产生的属于Microtask。
Step 1: 检查task,当task队列不为空,先执行一个task; Step 2: 检查microTask,当microTask不为空,则执行microTask;不断循环Step 2 直到microTask队列为空,再回到执行Step 1; 可简单理解为先处理完所有的Microtask,然后再处理Task。因为scheduleMicrotask()方法的调用自身就处于一个Task,执行完当前的task,也就意味着马上执行该Microtask。
isolate之间是逻辑隔离的,Isolate中的代码也是按顺序执行,因为Dart没有共享内存的并发,没有竞争的可能性,故不需要加锁,也没有死锁风险。对于Dart程序的并发则需要依赖多个isolate来实现。
flutter如何调用原生代码 Flutter通过提供Platform Channel的功能,使得Dart代码具备与Native交互的能力。
Platform Channel用于Flutter与Native之间的消息传递,整个过程的消息与响应是异步执行,不会阻塞用户界面。Flutter引擎框架已完成桥接的通道,这样开发者只需在Native层编写定制的Android/iOS代码,即可在Dart代码中直接调用