Flutter的渲染流程
如果想了解flutter的渲染原理,那么flutter的三棵树是无论如何也绕不过去的。
创建树
创建widget树
调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树
Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable(如何更新数据呢?查看后续内容)
Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject
RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容
从创建到渲染的大体流程是:根据Widget生成Element
,然后创建相应的RenderObject
并关联到Element.renderObject
属性上,最后再通过RenderObject
来完成布局排列和绘制。Element
就是Widget
在UI树具体位置的一个实例化对象,大多数Element
只有唯一的renderObject
,但还有一些Element
会有多个子节点,如继承自RenderObjectElement
的一些类,比如MultiChildRenderObjectElement
。最终所有Element
的RenderObject
构成一棵树,我们称之为Render Tree
即渲染树
。总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树
、Element树
、渲染树
。他们的依赖关系是:Element树
根据Widget树
生成,而渲染树
又依赖于Element树
,最终的UI树其实是由一个个独立的Element
节点构成。
我习惯把三者之间的关系比作:UI设计的原型图(Widget)、产品经理角色(Element)、开发(RenderObject):
- 相比于代码实现,原型图设计、更改显得更加轻量,耗费时间成本和人力成本比较低,同时原型图也是实际开发中必不可少的部分。
原型图在flutter的设计理念中就好比
Widget
,它只是一个配置数据结构,创建是非常轻量的,加上flutter团队对Widget
的创建、销毁做了优化,不用担心整个Widget
树重新创建所带来的性能问题;
- 产品经理的角色负责协调设计、开发等资源,来实现原型图和具体的需求;
Element
同时持有Widget
和RenderObject
对象,Element
负责Widget
的渲染逻辑,同时决定要不要把RenderObject
实例attach
到Render Tree
上,只有attach
到Render Tree
上,才会被真正的渲染到屏幕上。
- 开发拿到需求,负责实现。
RenderObject
主要负责layout、paint等复杂操作,是一个真正渲染到屏幕上的View,RenderObject
和Widget
相比就不一样了,整个RenderObject 树
重新创建开销就比较大,所以当Widget
重新创建,Element
树和RenderObject
树并不会完全重新创建。
通过这个简单的比喻,flutter渲染的三棵树是不是就比较容易理解了,接下来我们再来看看它的具体实现。
Widget
在Flutter
中,几乎所有的对象都是一个Widget
。与原生开发中 “控件” 不同的是,Flutter
中的Widget
的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector、用于APP主题数据传递的Theme、布局元素等等。
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
StatelessWidget 和 StatefulWidget
-
StatelessWidget
:无中间状态变化的Widget
,需要更新展示内容就得通过重新new,flutter推荐尽量使用StatelessWidget
; -
StatefullWidget
:存在中间状态变化,那么问题来了,Widget
都是immutable的,状态变化存储在哪里?flutter 引入State
的类用于存放中间态,通过调用state.setState()
进行此节点及以下的整个子树更新。
State
一个StatefulWidget
类会对应一个State
类,State
表示与其对应的StatefulWidget
要维护的状态,State
中的保存的状态信息可以:
- 在
Widget
构建时可以被同步读取。 - 在
Widget
生命周期中可以被改变,当State
被改变时,可以手动调用其setState()
方法通知Flutter framework
状态发生改变,Flutter framework
在收到消息后,会重新调用其build
方法重新构建Widget树
,从而达到更新UI的目的。
State中有两个常用属性:
1.Widget
,它表示与该State
实例关联的Widget
实例,由Flutter framework
动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的Widget
实例在重新构建时可能会变化,但State
实例只会在第一次插入到树中时被创建,当在重新构建时,如果Widget
被修改了,Flutter framework
会动态设置State.widget
为新的Widget
实例。
-
context
。StatefulWidget
对应的BuildContext
,作用同StatelessWidget
的BuildContext
。
State的生命周期
abstract class State extends Diagnosticable {
T get widget => _widget;
T _widget;
BuildContext get context => _element;
StatefulElement _element;
@protected
@mustCallSuper
void initState() { ... }
@protected
@mustCallSuper
void reassemble() { ... }
@protected
void setState(VoidCallback fn) {
// 省略掉一些逻辑判断
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
@protected
@mustCallSuper
void deactivate() { ... }
@protected
@mustCallSuper
void dispose() { ... }
@protected
Widget build(BuildContext context);
@protected
@mustCallSuper
void didChangeDependencies() { ... }
}
- initState(): state create之后被insert到tree时调用的
- didUpdateWidget(newWidget):祖先节点rebuild widget时调用
- deactivate():widget被remove的时候调用,一个widget从tree中remove掉,可以在dispose接口被调用前,重新instert到一个新tree中
- didChangeDependencies():
• 初始化时,在initState()之后立刻调用
• 当依赖的InheritedWidget rebuild,会触发此接口被调用 - build():
• After calling [initState].
• After calling [didUpdateWidget].
• After receiving a call to [setState].
• After a dependency of this [State] object changes (e.g., an[InheritedWidget] referenced by the previous [build] changes).
• After calling [deactivate] and then reinserting the [State] object into the tree at another location. - dispose():Widget彻底销毁时调用
- reassemble(): hot reload调用
注意事项:
- 在可滚动的Widget上,当子Widget滚动出可显示区域的时候,子Widget会被从树中remove掉,子Widget树中所有的state都会被dispose,state记录的数据都会销毁,子Widget滚动回可显示区域时,会重新创建全新的state、element、renderobject;
- 使用hot reload功能时,要特别注意state实例是没有重新创建的,如果该state中资源文件更新需要重启才能生效,例如,读取本地json文件,将数据显示到屏幕上,修改json文件后,如果不重启热重载不会生效。
BuildContext
我们已经知道,StatelessWidget
和StatefulWidget
的build
方法都会传一个BuildContext
对象:
Widget build(BuildContext context) {}
在很多时候我们都需要使用这个context
做一些事,比如:
Theme.of(context) //获取主题
Navigator.push(context, route) //入栈新路由
Localizations.of(context, type) //获取Local
context.size //获取上下文大小
context.findRenderObject() //查找当前或最近的一个祖先RenderObject
那么BuildContext
到底是什么呢,查看其定义,发现其是一个抽象接口类:
abstract class BuildContext {
Widget get widget;
...
}
还记得Widget
抽象类中的createElement
方法吗?你是不是已经猜到了?没错,Widget build(BuildContext context)
中的 BuildContext
就是 Element
的实例。
Element
查看Element
定义,发现它也是一个抽象类:
abstract class Element extends DiagnosticableTree implements BuildContext {
Element(Widget widget)
: assert(widget != null),
_widget = widget;
Element _parent;
@override
Widget get widget => _widget;
Widget _widget;
RenderObject get renderObject { ... }
@mustCallSuper
void mount(Element parent, dynamic newSlot) { ... }
@mustCallSuper
void activate() { ... }
@mustCallSuper
void deactivate() { ... }
@mustCallSuper
void unmount() { ... }
StatefulElement
和 StatelessElement
继承自 ComponentElement
, ComponentElement
是继承自 Element
的抽象类:
abstract class ComponentElement extends Element { ... }
以StatefulElement
为例:
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
...
_state._element = this;
_state._widget = widget;
...
}
State get state => _state;
State _state;
...
@override
Widget build() => state.build(this);
...
}
在创建StatefulElement
实例时,会调用widget.createState()
赋给私有变量_state
,同时把widget
和element
赋给_state
,从而三者产生关联关系,它的build
方法就是调用state.build(this)
,这里的this
就是StatefulElement
对象自己。
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
到渲染树。
看完Element的生命周期,可能有些读者会有疑问,开发者会直接操作Element树吗?其实对于开发者来说,大多数情况下只需要关注Widget树就行,Flutter框架已经将对Widget树的操作映射到了Element树上,这可以极大的降低复杂度,提高开发效率。但是了解Element对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element这个纽带将Widget和RenderObject关联起来,了解Element层不仅会帮助读者对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。
RenderObject
我们说过每个Element都对应一个RenderObject
,我们可以通过Element.renderObject
来获取。并且我们也说过RenderObject
的主要职责是Layout和绘制,所有的RenderObject
会组成一棵渲染树Render Tree
。RenderObject
就是渲染树中的一个对象,它拥有一个parent
和一个parentData
插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而parentData
正是一个预留变量,它正是由parent
来赋值的,parent
通常会通过子RenderObject
的parentData
存储一些和子元素相关的数据,如在Stack
布局中,RenderStack
就会将子元素的偏移数据存储在子元素的parentData
中(具体可以查看Positioned实现)。
RenderObject
类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。为此,Flutter提供了一个RenderBox
类,它继承自RenderObject
,布局坐标系统采用笛卡尔坐标系,这和Android
和iOS
原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴。
我们知道 StatelessWidget 和 StatefulWidget 两种直接继承自 Widget 的类,在 Flutter 中,还有另一个类 RenderObjectWidget 也同样直接继承自 Widget,它没有 build 方法,可通过 createRenderObject 直接创建 RenderObject 对象放入渲染树中。Column 和 Row 等控件都间接继承自 RenderObjectWidget。
主要属性和方法如下:
- constraints 对象,从其父级传递给它的约束
- parentData 对象,其父对象附加有用的信息。
- performLayout 方法,计算此渲染对象的布局。
- paint 方法,绘制该组件及其子组件。
RenderObject 作为一个抽象类。每个节点需要实现它才能进行实际渲染。扩展 RenderOject 的两个最重要的类是RenderBox 和 RenderSliver。这两个类分别是应用了 Box 协议和 Sliver 协议这两种布局协议的所有渲染对象的父类,其还扩展了数十个和其他几个处理特定场景的类,并实现了渲染过程的细节,如 RenderShiftedBox 和 RenderStack 等等。
RenderObject
具体如何布局以及Size、Offset的计算方式可以查阅咸鱼的技术文章深入了解Flutter界面开发,这里就不赘述了。
参考资料:
https://github.com/flutter/flutter
https://github.com/flutter/engine
https://flutter.io/
深入了解Flutter界面开发
Flutter核心原理