Flutter核心原理
架构
Flutter架构分层设计如上图,从下到上依次为 Embedder、Engine、Framework。
- Embedder: 操作系统适配层,实现渲染surface设置、线程设置等。
- Engine: 实现Flutter渲染引擎、文字排版、事件处理、Dart运行时等功能,包括Skia图形绘制库、Dart VM、Text引擎等。其中Skia和Text为上层接口提供了调用底层渲染和排版的能力。Skia是Google开源的二维图形库,用于Chrome OS, Andoird、Firefox OS等。
- Framework:是一个用Dart实现的一套基础库,用于处理动画、绘制、手势等。
- Material和Cupertino分别是安卓和iOS风格的2套UI控件库。
- Widgets是flutter渲染的基本单元
- Rendering是渲染层,运行时Rendering会将Widgets构建出Widgets Tree, 经过处理后提供给Skia即Engine层渲染。
- Animation、Painting、Gestures分别提供动画功能,基础绘制功能,手势功能。
- Foundation提供Framework基础能力,类似iOS中的UIFoundation
渲染流水线
渲染流水线是由垂直同步信号(VSync)驱动的,当信号到来后,flutter按照流水线执行,最后将渲染的数据传给GPU进行绘制。
Animation阶段: 因为动画会随着每个VSync信号的到来而改变状态,因此动画阶段是第一个阶段。 Build阶段: 这里包括WidgetTree的build、ElementTree的build和计算重绘的差异diff阶段。 Layout阶段: 这里确认每个元素的位置,尺寸。 Paint阶段: 就是绘制阶段了。 Composition阶段:和上面2个阶段一起组成了Rendering的核心组成部分。
渲染过程中的数据结构
Widget Tree —> Element Tree —> RenderObject Tree —> Layers 分别通过 createElement 、createRenderObjet和.layer 时间数据结构的变化
三棵树对应的关系如下图:
Widget Tree
第一棵树,是控件实现的基本逻辑单位,是用户对UI界面的描述方式。 需要注意的是,Widget是不可变的,当试图配置信息发生变化时,Flutter会重建Widget来进行更新,这叫simple is fast 思想,从上到下重新创建Widget Tree来进行刷新,思路简单,也不用额外保存影响到的节点。由于Widget只是配置是数据结构,创建是轻量的,销毁也做过优化,不用担心性能问题。
Element Tree
第二棵树,是Widget的实例化对象,Widget提供了createElement工程方法来创建Element, 持久存在于运行时的Dart上下文之中,它承载了构建的上下文数据,是连接结构化的配置到最终渲染的桥梁。
之所以让他持久存在于Dart上下文中而不是像Widget重新构建,因为Element Tree的重新创建和渲染开销会很大所以Element Tree到Renderobject 有一 个diff环节,来计算最小重绘区域。
RenderObject Tree
第三棵树,渲染对象树,由Element创建并关联到Element.renderObject上,接收Element的信息同步,也是持久地存在于Dart Runtime上下文中,是主要负责实现视图渲染的对象。
RenderObject Tree的显示过程分为3个阶段:
- 布局:layout
- 绘制:paint
- 合成:composition
其中,布局和绘制在RenderObject中完成,Flutter采用深度优先机制遍历渲染树,每个对象会在布局过程中接收父对象的Constraints参数,决定自己的大小,然后父对象就能按照自己的逻辑决定各个子对象的位置,并将它们绘制到不同的图层上,绘制完毕后,交给Skia来合成和渲染。 子对象不存储自己在容器中的位置,位置存储在自己的parentData字段中,该字段由父对象维护,因此当它的位置发生改变时并不需要重新布局或绘制。flutter还可以设置某些节点的布局边界(Relayout boundary),即当边界内的任何对象发生重新布局时,不会影响边界外的对象。
布局完成后,渲染树中的每个节点都有了明确的尺寸和位置,Flutter会把所有对象尽量绘制到不同的同层上,取决于绘制深度遍历的顺序和控件尺寸。
在上图的例子中,因为是深度遍历,可以看到遍历顺序是1->2->3->4->6,
所以1,2,3都在绿色图层中,到4这里是需要独占一个图层的内容,比如视频,因此4单独在黄色图层中。由于某些原因(视图手动合并等)导致5和6处于同一个图层中,此时如果5需要重绘的话,导致与其无关的6节点也要重绘。
为此Flutter提出重绘边界(Repaint boundary)的机制,在重绘边界内,Flutter会强制切换新的图层,这样就避免边界内外的互相影响而引起的不必要重绘。经典场景就是ScrollView滚动时,其中的内容重绘,其他某些内容是不需要重绘的。
Layers
最后是Layer,他是依附于RenderObject,是绘图操作的载体,也可以缓存绘图操作的结果。Flutter分别在不用的图层上绘图,然后将这些缓存了绘图结果的图层按照规则叠加,得到最终的渲染结果。
Layer有needsAddScene和subtreeNeedsAddToScene属性,分别表示是否需要加入场景。提供了markNeedsAddToScenc()标记是否需要更新,和iOS 中layoutIfNeeded相同,都是设置需要渲染的flag。
statefulWidget控件发生变化的流程如下图:
Layout
只有布局类Widget会触发layout 如 container、padding、align等 每个RenderObject节点需要做两件事:
- 调用自己的performLayout来计算layout
- 调用child的layout,把parent的限制传入
递归下去,每个子节点都受到父节点的约束并计算出自己的size,父节点再按照size决定布局。
Flutter与Native的通信
必要性
Flutter封装了很多UI层面的能力,但是其他能力却没有帮开发者封装到flutter.framework中,例如获取电池电量,网络状态,使用相机,麦克风,蓝牙,GPS,通知等等。因为一旦将这些与platform强相关的API封装进去,意味着Flutter就需要跟随iOS、Android、PC等版本更新,容易出现兼容性和碎片化问题是其次,framework包大小比现在就要大的多了。 因此提供了一套Flutter与Native通信的能力–>Platform Channel
通信原理
Flutter与platform通信的方式是发送异步的二进制消息,在Flutter端由BinaryMessages来实现,在iOS端是由FlutterViewController遵守的FlutterBinaryMessager协议实现。
在flutter端和iOS端分别调用实现BinaryMessages.send()和sendOnChannel:message:binaryReply:方法就能实现向对端发送二进制消息。 BinaryMessages.setMessageHandler()和setMessageHandlerOnChannel:binaryMessageHandler就能实现接收消息的handler。
性能如何
我们会不禁担心频繁调用Channel会不会有性能问题,其实channel的性能挺好,在整个调用流程中是机器码的传递,是C++层面,其中相对较耗时的是消息的编解码部分,Flutter定义了4种基本消息的编解码器类型,我们就不需要在代码中对数据进行操作了。但即便如下,Channel还是不适合做大规模的数据传输。