Flutter 原理
UI 系统
无论是 Android SDK 还是 iOS 的 UIKit 的职责都是相同的,它们只是语言载体和底层的系统不同而已。Flutter 提供了一套 Dart API,然后在底层通过 OpenGL 这种跨平台的绘制库(内部会调用操作系统 API)实现了一套代码跨多端。由于 Dart API 也是调用操作系统 API,所以它的性能接近原生。
Element 与 BuildContext
组件最终的 Layout、渲染都是通过 RenderObject 来完成的,从创建到渲染的大体流程是:根据 Widget 生成 Element,然后创建相应的 RenderObject 并关联到 Element.renderObject 属性上,最后再通过 RenderObject 来完成布局排列和绘制。
Element 生命周期
Framework 调用 Widget.createElement 创建一个 Element 实例,记为 element
Framework 调用 element.mount(parentElement,newSlot) ,mount 方法中首先调用 element 所对应 Widget 的 createRenderObject 方法创建与 element 相关联的 RenderObject 对象,然后调用 element.attachRenderObject 方法将 element.renderObject 添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在 Element 树结构发生变化时才需要重新 attach)。插入到渲染树后的 element 就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
当有父 Widget 的配置数据改变时,同时其 State.build 返回的 Widget 结构与之前不同,此时就需要重新构建对应的 Element 树。为了进行 Element 复用,在 Element 重新构建前会先尝试是否可以复用旧树上相同位置的 element,element 节点在更新前都会调用其对应 Widget 的 canUpdate 方法,如果返回 true,则复用旧 Element,旧的 Element 会使用新 Widget 配置数据更新,反之则会创建一个新的 Element。Widget.canUpdate 主要是判断 newWidget 与 oldWidget 的 runtimeType 和 key 是否同时相等,如果同时相等就返回 true,否则就会返回 false。根据这个原理,当我们需要强制更新一个 Widget 时,可以通过指定不同的 Key 来避免复用。
当有祖先 Element 决定要移除 element 时(如 Widget 树结构发生了变化,导致 element 对应的 Widget 被移除),这时该祖先 Element 就会调用 deactivateChild 方法来移除它,移除后 element.renderObject 也会被从渲染树中移除,然后 Framework 会调用 element.deactivate 方法,这时 element 状态变为“inactive”状态。
“inactive”态的 element 将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定 element,“inactive”态的 element 在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework 就会调用其 unmount 方法将其彻底移除,这时 element 的状态为 defunct,它将永远不会再被插入到树中。
如果 element 要重新插入到 Element 树的其它位置,如 element 或 element 的祖先拥有一个 GlobalKey(用于全局复用元素),那么 Framework 会先将 element 从现有位置移除,然后再调用其 activate 方法,并将其 renderObject 重新 attach 到渲染树。
BuildContext
1 | Widget build(BuildContext context) {} |
BuildContext 就是 widget 对应的 Element,所以我们可以通过 context 在 StatelessWidget 和 StatefulWidget 的 build 方法中直接访问 Element 对象。我们获取主题数据的代码 Theme.of(context)内部正是调用了 Element 的 dependOnInheritedWidgetOfExactType()方法。
- 如果没有 widget 层,单靠
Element
层是否可以搭建起一个可用的 UI 框架?如果可以应该是什么样子?
答案当然是肯定的,因为我们之前说过 widget 树只是Element
树的映射,我们完全可以直接通过 Element 来搭建一个 UI 框架。
- Flutter UI 框架能不做成响应式吗?
答案当然也是肯定的,Flutter engine 提供的 dart API 是原始且独立的,这个与操作系统提供的 API 类似,上层 UI 框架设计成什么样完全取决于设计者,完全可以将 UI 框架设计成 Android 风格或 iOS 风格,但这些事 Google 不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的。
Flutter 运行机制-从启动到显示
启动
1 | void main() { |
1 | void runApp(Widget app) { |
WidgetsFlutterBinding
是绑定 widget 框架和 Flutter engine 的桥梁。1
2
3
4
5
6
7class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
Window
是 Flutter Framework 连接宿主操作系统的接口。
1 | class Window { |
GestureBinding
:提供了window.onPointerDataPacket
回调,绑定 Framework 手势子系统,是 Framework 事件模型与底层事件的绑定入口。ServicesBinding
:提供了window.onPlatformMessage
回调, 用于绑定平台消息通道(message channel),主要处理原生和 Flutter 通信。SchedulerBinding
:提供了window.onBeginFrame
和window.onDrawFrame
回调,监听刷新事件,绑定 Framework 绘制调度子系统。PaintingBinding
:绑定绘制库,主要用于处理图片缓存。SemanticsBinding
:语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持。RendererBinding
: 提供了window.onMetricsChanged
、window.onTextScaleFactorChanged
等回调。它是渲染树与 Flutter engine 的桥梁。WidgetsBinding
:提供了window.onLocaleChanged
、onBuildScheduled
等回调。它是 Flutter widget 层与 engine 的桥梁。
WidgetsFlutterBinding.ensureInitialized()
负责初始化一个WidgetsBinding
的全局单例,紧接着会调用WidgetsBinding
的attachRootWidget
方法,该方法负责将根 Widget 添加到RenderView
上,代码如下:
1 | void attachRootWidget(Widget rootWidget) { |
代码中的有renderView
和renderViewElement
两个变量,renderView
是一个RenderObject
,它是渲染树的根,而renderViewElement
是renderView
对应的Element
对象,可见该方法主要完成了根 widget 到根 RenderObject
再到根Element
的整个关联过程。
1 | RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { |
该方法负责创建根 element,即RenderObjectToWidgetElement
,并且将 element 与 widget 进行关联,即创建出 widget 树对应的 element 树。如果 element 已经创建过了,则将根 element 中关联的 widget 设为新的,由此可以看出 element 只会创建一次,后面会进行复用。那么BuildOwner
是 widget framework 的管理类,它跟踪哪些 widget 需要重新构建。
渲染
回到runApp
的实现中,当调用完attachRootWidget
后,最后一行会调用 WidgetsFlutterBinding
实例的 scheduleWarmUpFrame()
方法,该方法的实现在SchedulerBinding
中,它被调用后会立即进行一次绘制(而不是等待”vsync” 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。
1 | void scheduleWarmUpFrame() { |
- Frame: 一次绘制过程,我们称其为一帧。Flutter engine 受显示器垂直同步信号”VSync”的驱使不断的触发绘制。我们之前说的 Flutter 可以实现 60fps(Frame Per-Second),就是指一秒钟可以触发 60 次重绘,FPS 值越大,界面就越流畅。
- FrameCallback:
SchedulerBinding
类中有三个 FrameCallback 回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行:transientCallbacks
:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback
添加回调。persistentCallbacks
:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。SchedulerBinding.instance.addPersitentFrameCallback()
,这个回调中处理了布局与绘制工作。postFrameCallbacks
:在 Frame 结束时只会被调用一次,调用后会被系统移除,可由SchedulerBinding.instance.addPostFrameCallback()
注册,注意,不要在此类回调中再触发新的 Frame,这可以会导致循环刷新。
绘制
1 | void initInstances() { |
通过addPersistentFrameCallback
向persistentCallbacks
队列添加了一个回调 _handlePersistentFrameCallback
:
1 | void _handlePersistentFrameCallback(Duration timeStamp) { |
该方法直接调用了RendererBinding
的drawFrame()
方法:
1 | void drawFrame() { |
flushLayout()
1 | void flushLayout() { |
该方法主要任务是更新了所有被标记为“dirty”的RenderObject
的布局信息。主要的动作发生在node._layoutWithoutResize()
方法中,该方法中会调用performLayout()
进行重新布局。
flushCompositingBits()
1 | void flushCompositingBits() { |
检查RenderObject
是否需要重绘,然后更新RenderObject.needsCompositing
属性,如果该属性值被标记为true
则需要重绘。
flushPaint()
1 | void flushPaint() { |
该方法进行了最终的绘制,可以看出它不是重绘了所有 RenderObject,而是只重绘了需要重绘的 RenderObject。真正的绘制是通过 PaintingContext.repaintCompositedChild()来绘制的,该方法最终会调用 Flutter engine 提供的 Canvas API 来完成绘制。
compositeFrame()
1 | void compositeFrame() { |
这个方法中有一个Scene
对象,Scene 对象是一个数据结构,保存最终渲染后的像素信息。这个方法将 Canvas 画好的Scene
传给window.render()
方法,该方法会直接将 scene 信息发送给 Flutter engine,最终由 engine 将图像画在设备屏幕上。
1 |
|
调用RendererBinding.drawFrame()
方法前会调用 buildOwner.buildScope()
(非首次绘制),该方法会将被标记为“dirty” 的 element 进行 rebuild()
。