渲染机制似乎是所有前端框架开发者都要深入了解的知识。
绘图原理
计算机绘图原理:
屏幕显示器一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,这时显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,GPU渲染好传递给显示器显示。
Flutter也遵循这种模式:
- GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构
- 绘制好的抽象视图数据结构在GPU线程中进行图层合成,然后提供给Skia引擎渲染为GPU数据,最后通过OpenGL或者 Vulkan提供给 GPU。
Flutter框架
flutter架构主要分三部分:
- framework 主要有Dart开发业务逻辑和各种UI组件。
- engine 主要使用 C++ 编写,提供了 Flutter 核心 API 的底层实现。
- embedder 充当着宿主操作系统和 Flutter 之间的粘合剂的角色,类似java和c通信的jni层。
其中负责渲染的主要是framework和engine。
Framework
Flutter Framework 是一个纯 Dart实现的 SDK。它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。
Engine
Flutter Engine 是一个纯 C++实现的 SDK。囊括了 Skia引擎、Dart运行时、文字排版引擎等。它是 Dart的一个运行时,它可以以 JIT、JIT Snapshot 或者 AOT的模式运行 Dart代码。这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。
绘图流程
1.首先是获取到用户的操作,然后你的应用会因此显示一些动画,接着 Flutter 开始构建 Widget 对象。
2.Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:
- 布局元素:决定页面元素在屏幕上的位置和大小;
- 绘制阶段:将页面元素绘制成它们应有的样式;
- 合成阶段:按照绘制规则将之前两个步骤的产物组合在一起。
3.最后的光栅化由 Engine 层来完成。
布局
- 布局约束,由父控件约束子控件。父控件通过传递Containers参数,告诉子控件自己的大小(布局约束),以此来决定子控件的位置。
- 大小尺寸,由子控件传给父控件。子控件的位置不存储在自己的容器中,而是存储在自己的parentData字段里,所以当他的位置发生变化时,并不需要重新布局或绘制。
- 子节点不关心自己所在的位置,父节点也不关心子节点具体长什么样子。
Flutter三棵树
在渲染阶段,有三颗重要的树,Widget树,Element树,Render树。其中,控件树(widget)最终会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。
Widget树
- 整个Flutter项目结构也是由很多个Widget构成的, 本质上就是一个Widget Tree。
- Widget里面存储了一个视图的配置信息,包括布局、属性等。
- Widget仅仅是配置文件,比较轻量,不参与直接的绘制。
- Widget频繁创建和销毁不会太影响性能。
Element树
- Element是Widget在树中具有特定位置的是实例化。当一个Widget首次被创建的时候,那么这个Widget会通过Widget.createElement,创建一个element。
- Element创建的同时还持有 Widget和 RenderObject的引用。
- 当Widget Tree所依赖的状态发生改变,Element根据拿到之前所保存的旧的Widget和新的Widget做一个对比, 判断两者的Key和类型是否是相同的, 相同的就不需要重新创建,只需要更新对应的属性,并将真正需要修改的部分同步到真实的RenderObject树中。
Render树
- RenderObject层是渲染库的核心,主要负责layout、paint等复杂操作,最终Flutter Engin是把RenderObject真正渲染到界面上的。
- 并不是所有的Widget都会被独立渲染,只有继承RenderObjectWidget的才会创建RenderObject对象。
树的创建过程
- Widget隐式调用
Widget.createElement
创建一个Element实例,记为element; - 接着调用
element.mount(parentElement,newSlot)
,将element相关联的renderObject插入到渲染树中,插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了。 - 为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的
canUpdate
方法,主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,表示复用旧Element,否则返回false,重新创建。
大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。
不同的Widget会生成不同的Element,以下是源码中常见额三种Element:
- StatelessElement StatelessWidget会创建StatelessElement,这里主要就是调用build方法,将Element传出去。
- StatefulElement StatefulWidget会创建StatefulElement,创建出来后调用createState方法,创建state,将Widget赋值给state,最后调用state的build方法,并且将Element传出去。
- RenderElement RenderElement主要是创建RenderObject对象,只有继承自RenderObjectWidget的Widget会创建RenderObjectElement,创建步骤先创建RanderElement,创建出来后调用mount方法,在mount方法中会调用createRenderObject方法,来创建RenderObject。
BuildContext
创建Element之后,创建出来的elment会拿到传过来的widget,然后调用widget自己的build方法。针对StatefulWidget,调用build的时候,调用的是state中的build方法。build方法传入的参数都是Element自己,所以本质上BuildContext就是当前的Element。
构造函数中的Key
我们在自定义Widget的时候,每一个Widget, 在其构造方法中我们都会看到一个参数Key,其实这个key就是更新Element用的,通过Widget的canUpdate
做到有效的增量更新:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
默认情况下我们几乎不设置key,Element的重用机制主要靠runtimeType来决定,大部分情况都是可重用的,所以想要Element强制重建,我们可以自定一个唯一的key,传入进去既可。
key的分类
Key本身是一个抽象类,子类包含LocalKey和GlobalKey。
LocalKey可以派生出多个子类,用于不同的场景:
- ValueKey 以一个数据作为Key,如:数字,字符
- ObjectKey 以object对象作为Key,例如 ObjectKey(Text(‘222’))
- UniqueKey 可以保证key的唯一性(一旦使用UniqueKey那么就不存在Element复用了)
GlobalKey 可以获取到对应的widget的state对象,GlobalKey 使用了一个静态常量 Map 来保存它对应的 Element。你可以通过 GlobalKey 找到持有该GlobalKey的 Widget,State 和 Element。需要注意的是:GlobalKey 是非常昂贵的,需要谨慎使用。
以下是GlobalKey使用场景:
class GlobalKeyDemo extends StatelessWidget {
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GlobalKeyDemo'),
),
body: ChildPage(
key: _globalKey,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_globalKey.currentState.data =
'old:' + _globalKey.currentState.count.toString();
_globalKey.currentState.count++;
_globalKey.currentState.setState(() {});
},
),
);
}
}
class ChildPage extends StatefulWidget {
ChildPage({Key key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
Text(count.toString()),
Text(data),
],
),
);
}
}
提几个问题
1.createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?
答:Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget,state 里面有个 widget 的get 方法可以获取到 _widget 对象。
2.build 方法是在什么时候调用的?
答:Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是常见的 BuildContext。
3.BuildContext 是什么?
答:StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。简而言之 BuidContext 就是 Element。
4.Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
答:不会影响性能,widget 只是简单的配置信息,并不直接涉及布局渲染相关。Element 层通过判断新旧 widget 的runtimeType 和 key 是否相同决定是否可以直接更新之前的配置信息,也就是替换之前的 widget,而不必每次都重新创建新的 Element。
5.创建 Widget 里面的 Key 到底是什么作用?
答:Key 作为 Widget 的标志,在widget 变更的时候通过判断 Element 里面之前的 widget 的 runtimeType 和 key来决定是否能够直接更新。
参考
- https://www.jianshu.com/p/9650780dcbf5
- https://www.jianshu.com/p/bcc74f37aba5
- https://www.jianshu.com/p/7fe3cedb67da