Flutter- Element、BuildContext、RenderObject

118 阅读6分钟

image.png

大家知道UI树是由一个个独立的Element节点构成;组件最终的Layout、渲染是通过RenderObject来完成的,从创建到渲染的大体流程是:根据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树,如上图。

Element生命周期:

  1. Framework调用Widget.createElement创建一个Element实例,即为element。
  2. Framework调用element.mount(parentElement,newSlot),mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新添加),插入到渲染树后的element就处于active状态,处于active状态后就可以显示在屏幕上(可以隐藏)。
  3. 当有父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来避免复用。
  4. 当有父Element决定要移除element时(如Widget树结构发生变化,导致element对应Widget被移除),这时该父Element就会调用deactivateChild方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate方法,这时element状态变为"inactive"状态。
  5. "inactive"状态的element将不会再显示到屏幕,为了避免在一次动画执行过程中反复创建、移除某个特定的element,“inactive”状态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后,它还未能重新变为“active”状态,Framework就会调用其unmount方法,将其彻底移除,这时element的状态为defunct,它将永远不会再被插入搭到树中。
  6. 如果element要重新插入到element树的其他位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Franework会先将element从现有位置移除,然后再调用其他activate方法,并将其renderObject重新attach到渲染树。

BuildContext

StatelessWidget和StatefulWidget的build方法都会传一个BuildContext对象,在很多时候都需要使用这个context做一些事情,比如:

Theme.of(context) //获取主题
Navigator.push(context, route) //入栈新路由
Localizations.of(context, type) //获取Local
context.size //获取上下文大小
context.findRenderObject() //查找当前或最近的一个祖先RenderObject

那么context对象是怎么实现的?
build调用是发生在StatelessWidget和StatefulWidget对应的StatelessElement和StatefulElement的build方法中:

class StatelessElement extends ComponentElement {
  ...
  @override
  Widget build() => widget.build(this);
  ...
}

build传递的参数是this,也就是这个BuildContext就是StatelessElement,同样,StatefulWidget的context是StatefulElement。但StatelessElement和StatefulElement本身并没有实现BuildContext接口,继续看源码,发现它们间接继承自Element类,Element类实现了BuildContext接口:

class Element extends DiagnosticableTree implements BuildContext {
    ...
}

至此,BuildContext就是Widget对应的Element,可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。

为什么build方法的参数不定义成element类,而定义成buildContext?

进阶

Element是Flutter UI框架内部连接Widget和RenderObject的纽带,大多数时候只需关注Widget即可,但Widget层有时候并不能完成屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给开发者,这样,开发者在需要时可以直接操作Element对象。

问题:

如果没有widget层,单靠Element层是否可以直接搭建起一个可用的UI框架? Flutter UI框架能不做车响应式?

回答1:Widget树只是Element树的映射,所以完全可以直接通过Element来搭建UI框架。
示例:页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排序一次。

import 'package:flutter/material.dart';

class HomeView extends ComponentElement{
  HomeView(Widget widget):super(widget);
  String text = "123456789";
  @override
  Widget build() {
    // TODO: implement build
    Color primary = Theme.of(this).primaryColor;
    return GestureDetector(
      child: Center(
        child: TextButton(
          child: Text(text, style: TextStyle(color: primary),),
          onPressed: (){
            var t = text.split("")..shuffle();
            text = t.join();
            markNeedsBuild();//点击后将该Element标记为dirty,Element将会rebuild
          },
        ),
      )
    );
  }
}

class HomeViewSimple extends Widget{
  const HomeViewSimple({super.key});
  @override
  Element createElement() {
    // TODO: implement createElement
    return HomeView(this);
  }
}

class HomeViewTest extends StatelessWidget{
  const HomeViewTest({super.key});

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(
        title: const Text("SSL Test Element"),
      ),
      body: const HomeViewSimple(),
    );
  }
}

截屏2023-05-04 19.53.31.png 问题2:答案也是肯定的,Flutter引擎提供的API是原始且独立的,这个与操作系统提供的API类似,上层的UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成Android或者iOS风格。但是这些事Google不会去做,也没有必要再去搞一套,因为响应式的思想本身是很棒的。

RenderObject

每个Element对应一个RenderObject,可以通过Element.renderObject来获取。RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树RenderTree。

RenderObject就是渲染树中的一个对象,它的主要作用是实现事件响应以及渲染管线中除了build的执行过程(build过程有element实现),即包括:布局、绘制、层合成以及上屏。

RenderObject拥有一个parent和一个parentData属性,parent指向渲染树中自己的父节点,而parentData是一个预留变量,在父组件的布局过程,会确定其所有子组件布局信息(如位置信息,即相对于父组件的偏移),而这些布局信息需要在布局阶段保存起来,因为布局信息在后续的绘制阶段还需要被使用(用于确定组件的绘制位置),而parentData属性的主要作用就是保存布局信息,比如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中。

RenderObject类本身实现了一套基础的布局和绘制协议,但是并没有定义节点模型(如一个节点可以有几个子节点,没有子节点?一个、两个或更多)。它没有定义坐标系统(如子节点定位是笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constrain和size?或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。

为此,Flutter框架提供了一个RenderBox和一个RenderSliver类,它们都是继承自RenderObject,布局坐标系统采用笛卡尔坐标系,屏幕的(top、left)是原点。而Flutter基于这两个类分别实现了基于RenderBox的盒模型布局和基于Sliver的按需加载模型。