Flutter 视图绘制
[TOC]
前言
Flutter有别于其他跨平台开发的一大特点是它自带UI组件和渲染器,而不是通过一些Bridge去做平台适配。其中自带UI组件在Flutter的Framework层而渲染器在Engine层,那么Google工程师面临的问题是如何尽可能快的(在VSync信号间隔内)完成这次传递?
概览
从UI绘制的整体流程来看,从用户的输入①到界面上动画的进度更新②,然后开始视图数据的build③,通过Layout④来确定视图的Position和Size,接下来是视图数据的Paint⑤和Composite⑥,最后是将合成后的视图数据进行”光栅化”处理使它真正的变成一个个像素填充的数据并提交给GPU。
主要流程
本文主要讲的是build、layout、paint三个阶段。
Build
三棵树
Widget Tree,Element Tree 以及 RenderObject Tree 。根据它们的功能我将它翻译成模型树,状态树和渲染树,也正是通过这三棵树维护起了整个应用的视图数据。
Widget Tree
存放属性的描述信息,更像是一个Model。同一个Widget可以同时描述多个渲染树中的节点,但是它是不可修改的,因此它只会被创建或销毁。
Element Tree
存放上下文状态信息,同时持有 Widget和RenderObject的引用。像是一个Controller控制着状态的更新(initial, mount,amount,activate,deactivate,update)。
RenderObject Tree
实现了layout和paint事件,是最终渲染的View视图。
Create ViewTree
如何将三棵树关联起来的?
- 从入口main.dart开始,我们会调用到runApp方法
1
2
3
4
5
6
7//@widgets/binding.dart
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}这里的Input是我们要展示的Widget。然后出现了第一个关键类WidgetFlutterBinding
1
2
3
4
5
6class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
new WidgetsFlutterBinding();
return WidgetsBinding.instance;
}它是一个单列类并且通过mixin可以使用手势,绘制等等功能,是连接Widget Layer和 Flutter Engine的纽带。
- 拿到单列之后会调用它的attachRootWidget方法。
1
2
3
4
5
6
7void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}这里的Input依旧是我们要展示的widget,并且出现第二个关键类
RenderObjectToWidgetAdapter。看他的构造函数其中的rootWidget就是Widget信息而初始化创建的renderView就是RenderObject。这样就只差Element了
- 再看接下来RenderObjectToWidgetAdapter的attachToRenderTree方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}这里出现第三个关键类 RenderObjectToWidgetElement,假如element为null则表示之前没有创建过,我们新建一个 ElementTree并通过它的mount方法将Widget Tree绑上;假如它不为null,我们则通过markNeedsBuild()重新调整Element Tree。
Update ViewTree
怎么对Element Tree进行更新的?
假设我们通过setState改变一个控件的高度
- 先调用到BuilderOwner.buildScope方法,然后其中的dirty element就会执行rebuild方法
1
2
3
4
5void rebuild() {
if (!_active || !_dirty)
return;
performRebuild();
}2.build出子Widget Tree,然后调用updateChild方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void performRebuild() {
Widget built;
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
_debugReportException('building $this', e, stack);
built = new ErrorWidget(e);
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
_debugReportException('building $this', e, stack);
built = new ErrorWidget(e);
_child = updateChild(null, built, slot);
}
}- 更新element Tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
return inflateWidget(newWidget, newSlot);
}① 假如newWidget为null,child不为null,则删除原来的控件。
② 假如newWidget不为null,child也不为null,通过调用canUpdate判断是否可以更新。如果可以则update,如果不可以则先删除原来的控件,再创建新的控件。
③ 假如newWidget不为null,但child为null,则需要创建新的控件
Layout
布局的计算
渲染树的每个对象会在布局的时候接收到父对象的Constraints参数,决定自己的大小,然后父对象就可以按照自己的布局方式来决定各个子对象的所在的位置了。
Relayout boundary
- flutter在布局上的优化,利用boundary信息来划分了一条“三八线”,发生Relayout的时候我们”井水不犯河水”
具体代码上的处理
1
2
3
4
5
6
7
8
9
10void layout(Constraints constraints, { bool parentUsesSize: false }) {
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
......
}① constraints.isTight :minWidth和maxWidth相等,是一个固定值,所以它可以认为它自身就是一个Boundary。
② !parentUsesSize:parent的布局并不需要依赖child的布局结果,换句话说child你随便怎么布局,我parent都不会变,所以它可以认为它被Boundary了。
③ sizedByParent:通过Parent给的约束条件就可以确定大小,不需要知道child的size,所以它也可以认为自身就是一个Boundary。
Size不会变但offset会变
Child将自己的位置信息储存在自己的parentData字段中,而该字段由它的父对象负责维护。这样我们才能设置boundary,才能让Child即使位置发生变化,也不需要重新布局或者绘制。
Paint
布局的绘制
有了明确的Size和Position信息后,我们开始真正的绘制。它与layout不同点在于,layout是先有child的size再有parent的size,draw是先绘制parent再child。
Relayout boundary
- 这是一个和Relayout boundary类似思想的优化方式,设置之后repaint咱们也“井水不犯河水”。
具体代码上的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}假如isRepaintBoundary或者alwaysNeedsCompositing那么设置_needsCompositing为true值。这个标记位的作用是创建一个新的Layer,这样把一个经常变化的区域画个圈圈圈起来,以后每次只要绘制这一小部分就可以了。
应用场景
轮播广告其实就是一个典型的应用场景,当横向滚动内容重绘的时候,一般情况下其他内容是不需要重新绘制的。