Flutter 原理初探
一、Flutter 框架全景图
上图是 Flutter 官网上的一张全景图。从结构上看,Flutter 主要分为三个层次:
- Dart 框架层(Framework)
- 上层框架,主要包括 dart 侧 Widget 管理、绘制、动画、手势等接口
- C++ 引擎层(Engine)
- 虚拟机、线程模型、与平台的通信、绘制流程、系统事件、文字布局、帧渲染管线等
- 平台相关的嵌入层(Embeder)
- 渲染图层、平台线程和事件循环管理,Native Plugin 等
今天主要阐述 Engine 层和 Framework 的一部分关键模块。
二、线程模型
在 Flutter 的设计中,各个不同层级中的独立执行单元分别对应一个专有的概念: DartVM 中的独立执行单元叫做 Isolate,Engine 中叫做 Runner,Embedder 中才对应平台真正的线程。Isolate、Runner 等这些概念对线程进行了封装,施加了特定的约束,既实现了线程安全性又保证了平台无关性。
下图主要展示了 Flutter 各层中执行单元与线程的对应关系:
其具体约束和行为特点总结如下:
-
Dart Isolate:
- 各 Isolate 间不共享内存,无需 lock
- 各 Isolate 间通过 port 发消息,调用皆异步
- 除 Root Isolate 以外都跑在 dart 自己维护的线程池上
-
Engine 从 Embedder 得到多个 Task runner:
- 每个 runner 一般都对应一个独立的线程
- Runner 所对应的线程在 Engine 的生命周期内保持不变
-
Engine 中的各 runner 的职责:
- UI runner:负责处理 Dart root isolate 中的逻辑、界面布局、生成 layer tree 等
- GPU runner:负责将 layer tree 信息转为 GPU 指令,配置绘制所需资源
- IO runner:配合 GPU runner,主要负责读取图片、解码,上传到 GPU 等耗时操作
- Platform runner:负责处理 Engine 与外部的所有交互
- Platform messages
- API 调用
Platform Runner 上的任务大多由 Embedder 提供
下面的流程图展示了 Flutter 程序渲染过程中各 runner 的基本职责:
-
Root Isolate 将待渲染的 frame 发给 Engine
-
Engine 让 Platform runner 监听 vsync 信号
-
Platform runner 收到 vsync 信号,通知 Engine
-
Engine 通知 Root Isolate 执行以下操作:
- 更新动画 interpolator
- rebuild 相关的 widget
- 布局、渲染图层树
- 发送辅助操作(Accessibility)相关信息
-
图层树从 Root Isolate 发送到 GPU runner
-
GPU runner执行以下操作:
- 配置绘制所需资源
- 准备好 frame buffer
- 管理绘制 surface 生命周期
- 纹理检查
最后再将 GPU 指令发送给 GPU 设备。
GPU runner 可能会由于负载过重导致与 UI runner 不能同步,这时它会影响 frame-scheduling 的机制,进而延缓 UI runner 发送新 frame 的速度,导致显示性能问题。
三、Flutter 界面逻辑处理流程
注:此部分内容主要出自参考资料中的视频,系 Flutter 项目两个创始人于 2016 年做的分享,其内部细节不确定有没有发生变化,如读者有发现本文描述有误或真实细节与视频描述不符的,欢迎留言更正。
下图是一个从用户操作开始到最后渲染的总体流程,主要包括以下阶段:
- 用户输入
- 动画处理
- Build Widgets
- 布局
- 绘制
- 组装
- 光栅化
1. Flutter 界面框架的分层架构
从界面构建、渲染的实现上,Flutter 提供了由高级到低级的几个不同层次的接口(API):
- Material
- Widgets
- Rendering
- dart:ui
下面从低级到高级一一举例说明:
1.1 dart:ui 层
dart:ui 这一层基本没有提供任何封装和抽象,开发者注册一个屏幕重绘回调函数 —— render()函数,系统会在每一帧绘制时调用此方法。
开发者需要在 render() 函数的实现中操作屏幕绝对坐标,调用相关子元素的绘制方法等。
具体的 render 函数实现细节示例:
可以看出实现一个较简单的界面也需要复杂的计算和逻辑,实现和维护都比较麻烦。
另外,这种实现方式对各子元素坐标等没有做任何缓存,每次屏幕重绘时都需要重新计算所有坐标,性能较差。
1.2 Rendering 层
Rendering 层比 dart:ui 层高级一些,封装了被渲染的对象和其父子关系,并且缓存了相关对象的坐标,省去了不必要的重复布局计算。
例:main() 函数中以声明的形式指定绘制结构:
render() 函数中调用 runApp() 绘制相应的 Rendering 对象:
其中,Rendering 对象是可变对象,应用程序需要先创建 Render object 树,然后在后续状态有变化时更新这棵树(结构和属性),管理起来比较麻烦。
Rendering Object 的设计对应于 iOS 中的 UIView/UILayer 等对象
1.3 Widget 层
Widget 层提供了完整、高效的封装:
- Widget 是不可变的,每次有状态变化都是重新生成 Widget 树(省去了维护可变树的复杂性)
- 可以将 Widget 理解为一个不可变的配置(config),很轻量
- runApp() 会生成 Element,并调用 Elment 来生成 Render objects
- Element 用来管理 RenderObject 的生命周期,轻量,不会随着 Widget 每次都重新生成
- RenderObject 是真正渲染的对象
下面的例子显示了 Widget 与 Element 以及 Render object 的对应关系:
- Widget(左):Rectangle 为父 Widget,Circle 为子 Widget
- Element(中):调用 runApp() 时,会生成这两个 Widget 对应的 Element
- RenderObject(右):Element 会调用相关方法生成自己的 RenderObject
- 父 Element 生成 RenderRectanble,子 Element 生成 RenderCircle
当 Widget 颜色发生改变(Rectangle 颜色绿变黄,Circle 颜色蓝变红)时,应用程序会生成新的 Widget(Rectangle-yellow 和 Circle-red)。
框架如果发现 Widget 类型没变,之前的 Element 仍可复用,则新 Widget 指向之前的 Element。Element 会调用相关方法,将对应的 RenderObject 对象的属性改为新值(RenderObject 也复用)。
旧的 Widget 则被销毁。
如果新的 Widget 不能复用之前的 Element(比如说 Widget 类型不同,下图中子结点的形状由之前的圆形变成了三角形),则将销毁原有的 Element 和 RenderObject 并重新生成新的 Element 和 RenderObject:
1.4 Material 层
主要是提供了一些成型的界面风格控件,此处略去。
2. 渲染流水线
渲染流水线主要包括以下三个阶段:
- 布局
- 绘制
- 组合
设计原则:Simple is fast
- 一次遍历,线性时间的布局和绘制
- 简单的盒模型布局约束可以实现复杂的布局
- 使用组合使绘制对象结构化,实现局部重绘
Flutter 支持的布局约束比 iOS 的 Autolayout 简单得多,但也有足够强大的表达能力。
2.1 布局
-
RenderObject:布局的对象
- Owner
- Parent:父结点
- Layout():布局方法
- Paint():绘制方法
- parentData: BoxParentData
- offset
- 子控件在父容器中的位置(父容器坐标系)
子控件只决定自己的大小,父容器可将它放置于任何位置
- offset
- visitChildren()方法
- 不存子结点,只提供了访问子结点的方式
-
RenderBox(一个 RenderObject 的具体实现)
- size
- getMinInstrinsicWidth
- getMaxIntrinsicWidth
- getMinInstrinsicHeight
- getMaxIntrinsicHeight
- getDistanceToBaseline
- hitTest
-
布局的数据流:
- 先从父结点向子结点传递约束信息
- 然后各子结点向父结点传递布局后的大小信息
- 例:Flex 布局
-
输入:minWidth, maxWidth, minHeight, maxHeight 约束
-
输出:
- 总体的 width, height
- 每个子控件的大小和位置
-
主要步骤如下:
- 布局非变长子结点
- 计算剩余空间
- 计算各变长子结点宽度
- 布局变长子结点
注:图中 +Inf(无穷大)表示当前结点不知道自己应该是什么值,由框架帮它决定。
-
- 重布局边界(Relayout boundary):
- 边界以内的结点布局变化不会导致其父结点重新布局
2.2 绘制
-
深度遍历 RenderObject 树,根据布局阶段计算出的 offset 绘制相应的结点
-
问题:应用程序可能有多个图层,Flutter 框架需要决定哪些元素绘制到哪个 layer 上?
例:下图中黄色的图层是一个视频播放器,目标是要将 6 个 widget 画在 3 个 layer 上。
下图中 1、2、3、4、5 是渲染顺序,深度遍历 Render object 树,因为 4 需要一个单独的 layer,4 以后的都在红色的图层上,所以 5、6 都是红色。
注:其中 2 和 5 的区别是:2 是此结点在其子结点绘制前绘制的, 5 是此结点在其子结点绘制之后绘制的,在目前这个 case 下,2 和 5 会被绘制到不同的图层上。
- 绘制数据流:
- 父结点 -> 子结点:将你自己画到这个 offset 上
- 子结点 -> 父结点:绘制完成,从这里(图层)接着绘制(类似 continuation passing)
注意区别 Flutter 复用 layer 的机制与别的框架的差异:在 Cocoa 中,UIView 和 CALayer 是一一对应的。
-
重绘边界(Repaint boundary):用来分隔兄弟结点的绘制图层,否则 5 的重绘会导致 6 的重绘
- 为了决定应该在哪里设置重绘边界,应该回答以下问题:
- 如果程序中的这部分元素重绘了,其他哪些元素是一定也需要跟着一起重绘的?
- 为了决定应该在哪里设置重绘边界,应该回答以下问题:
布局顺序 Vs. 渲染顺序:
- 依据各自的规则,二者顺序可能不同
2.3 图层组合
- 渲染列表时,为了防止上下滚动时视窗中的所有元素都重绘,列表中的每一项单独使用一个图层(layer)
- 单独使用一个图层通过设置重绘边界实现
- 向上滚动时,只需要绘制新露出来的一行
- 其他行只移动图层,不重绘
让 GPU 渲染一个 frame 有两种方式:
- GPU 绘制指令
- 纹理(Texture)+ blit(位图操作)
- Flutter:绘制时首先使用 GPU 指令,画三次后发现三次的 command 都一样,就会切换成使用 Texture
- iOS:所有元素都是使用 Texture(要求足够的 GPU 显存)
- Android:所有元素都是绘制指令绘制的
参考资料
- Flutter's Rendering Pipeline: youtu.be/UUfXWzp0-DU
- Flutter's Layered Design: youtu.be/dkyY9WCGMi0
转载自:https://juejin.cn/post/6890951845729009671