flutter 渲染三棵树(Widget、Element、RenderObject)

Flutter的渲染流程

如果想了解flutter的渲染原理,那么flutter的三棵树是无论如何也绕不过去的。

创建树
  1. 创建widget树

  2. 调用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。最终所有ElementRenderObject构成一棵树,我们称之为Render Tree渲染树。总结一下,我们可以认为Flutter的UI系统包含三棵树:Widget树Element树渲染树。他们的依赖关系是:Element树根据Widget树生成,而渲染树又依赖于Element树,最终的UI树其实是由一个个独立的Element节点构成。

我习惯把三者之间的关系比作:UI设计的原型图(Widget)、产品经理角色(Element)、开发(RenderObject):

  • 相比于代码实现,原型图设计、更改显得更加轻量,耗费时间成本和人力成本比较低,同时原型图也是实际开发中必不可少的部分。

原型图在flutter的设计理念中就好比Widget,它只是一个配置数据结构,创建是非常轻量的,加上flutter团队对Widget的创建、销毁做了优化,不用担心整个Widget树重新创建所带来的性能问题;

  • 产品经理的角色负责协调设计、开发等资源,来实现原型图和具体的需求;

Element 同时持有 WidgetRenderObject 对象,Element 负责 Widget 的渲染逻辑,同时决定要不要把 RenderObject 实例 attachRender Tree 上,只有 attachRender Tree 上,才会被真正的渲染到屏幕上。

  • 开发拿到需求,负责实现。

RenderObject主要负责layout、paint等复杂操作,是一个真正渲染到屏幕上的View,RenderObjectWidget 相比就不一样了,整个 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
  1. StatelessWidget:无中间状态变化的Widget,需要更新展示内容就得通过重新new,flutter推荐尽量使用StatelessWidget;
  2. StatefullWidget:存在中间状态变化,那么问题来了,Widget都是immutable的,状态变化存储在哪里?flutter 引入State的类用于存放中间态,通过调用state.setState()进行此节点及以下的整个子树更新。

State

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

  1. Widget 构建时可以被同步读取。
  2. 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实例。

  1. contextStatefulWidget对应的BuildContext,作用同StatelessWidgetBuildContext
State的生命周期
abstract class State<T extends StatefulWidget> 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() { ... }
}
  1. initState(): state create之后被insert到tree时调用的
  2. didUpdateWidget(newWidget):祖先节点rebuild widget时调用
  3. deactivate():widget被remove的时候调用,一个widget从tree中remove掉,可以在dispose接口被调用前,重新instert到一个新tree中
  4. didChangeDependencies():
    • 初始化时,在initState()之后立刻调用
    • 当依赖的InheritedWidget rebuild,会触发此接口被调用
  5. 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.
  6. dispose():Widget彻底销毁时调用
  7. reassemble(): hot reload调用

注意事项:

  1. 在可滚动的Widget上,当子Widget滚动出可显示区域的时候,子Widget会被从树中remove掉,子Widget树中所有的state都会被dispose,state记录的数据都会销毁,子Widget滚动回可显示区域时,会重新创建全新的state、element、renderobject;
  2. 使用hot reload功能时,要特别注意state实例是没有重新创建的,如果该state中资源文件更新需要重启才能生效,例如,读取本地json文件,将数据显示到屏幕上,修改json文件后,如果不重启热重载不会生效。
BuildContext

我们已经知道,StatelessWidgetStatefulWidgetbuild方法都会传一个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() { ... }

StatefulElementStatelessElement 继承自 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<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
  ...
  @override
  Widget build() => state.build(this);
  ...
}

在创建StatefulElement实例时,会调用widget.createState()赋给私有变量_state,同时把widgetelement赋给_state,从而三者产生关联关系,它的build方法就是调用state.build(this),这里的this就是StatefulElement对象自己。

Element的生命周期:
  1. Framework 调用Widget.createElement 创建一个Element实例,记为element

  2. Framework 调用 element.mount(parentElement,newSlot)mount方法中首先调用element所对应WidgetcreateRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。

  3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的elementelement节点在更新前都会调用其对应WidgetcanUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的ElementWidget.canUpdate主要是判断newWidgetoldWidgetruntimeTypekey是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。

  4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为inactive状态。

  5. inactive态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定elementinactive态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成active状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。

  6. 如果element要重新插入到Element树的其它位置,如elementelement的祖先拥有一个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 TreeRenderObject就是渲染树中的一个对象,它拥有一个parent和一个parentData 插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而parentData正是一个预留变量,它正是由parent 来赋值的,parent通常会通过子RenderObjectparentData存储一些和子元素相关的数据,如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中(具体可以查看Positioned实现)。

RenderObject类本身实现了一套基础的layout和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。为此,Flutter提供了一个RenderBox类,它继承自RenderObject,布局坐标系统采用笛卡尔坐标系,这和AndroidiOS原生坐标系是一致的,都是屏幕的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界面开发,这里就不赘述了。

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容