55、Flutter核心原理--Element、BuildContext和RenderObject_flutter element_风雨「83」的博客-程序员秘密

技术标签: flutter  android  Flutter  vue.js  ios  

Element


我们知道最终的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,Element的生命周期如下:

  1. Framework 调用Widget.createElement 创建一个Element实例,记为element
  2. Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
  3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidgetoldWidgetruntimeTypekey是否同时相等,如果同时相等就返回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树的其它位置,如elementelement的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

看完Element的生命周期,可能有些读者会有疑问,开发者会直接操作Element树吗?其实对于开发者来说,大多数情况下只需要关注Widget树就行,Flutter框架已经将对Widget树的操作映射到了Element树上,这可以极大的降低复杂度,提高开发效率。但是了解Element对理解整个Flutter UI框架是至关重要的,Flutter正是通过Element这个纽带将Widget和RenderObject关联起来,了解Element层不仅会帮助读者对Flutter UI框架有个清晰的认识,而且也会提高自己的抽象能力和设计能力。另外在有些时候,我们必须得直接使用Element对象来完成一些操作,比如获取主题Theme数据,具体细节将在下文介绍。

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 {
    ...
}

那这个context对象对应的实现类到底是谁呢?我们顺藤摸瓜,发现build调用是发生在StatelessWidgetStatefulWidget对应的StatelessElementStatefulElementbuild方法中,以StatelessElement为例:


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

 发现build传递的参数是this,很明显!这个BuildContext就是StatelessElement。同样,我们同样发现StatefulWidgetcontextStatefulElement。但StatelessElementStatefulElement本身并没有实现BuildContext接口,继续跟踪代码,发现它们间接继承自Element类,然后查看Element类定义,发现Element类果然实现了BuildContext接口:

class Element extends DiagnosticableTree implements BuildContext {
    ...
}

至此真相大白,BuildContext就是widget对应的Element,所以我们可以通过contextStatelessWidgetStatefulWidgetbuild方法中直接访问Element对象。我们获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。

进阶

我们可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidgetStatefulWidget中通过build方法参数又将Element对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element对象。那么现在笔者提一个问题: 

  1. 如果没有widget层,单靠Element层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?
  2. Flutter UI框架能不做成响应式吗?

对于问题1,答案当然是肯定的,因为我们之前说过widget树只是Element树的映射,我们完全可以直接通过Element来搭建一个UI框架。下面举一个例子:

我们通过纯粹的Element来模拟一个StatefulWidget的功能,假设有一个页面,该页面有一个按钮,按钮的文本是一个9位数,点击一次按钮,则对9个数随机排一次序,代码如下:

class HomeView extends ComponentElement{
  HomeView(Widget widget) : super(widget);
  String text = "123456789";

  @override
  Widget build() {
    Color primary=Theme.of(this).primaryColor; //1
    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
          },
        ),
      ),
    );
  }
}
  • 上面build方法不接收参数,这一点和在StatelessWidgetStatefulWidgetbuild(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可,如代码注释1处Theme.of(this)参数直接传this即可,因为当前对象本身就是Element实例。

  • text发生改变时,我们调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。

  • 上面代码中build方法返回的仍然是一个widget,这是由于Flutter框架中已经有了widget这一层,并且组件库都已经是以widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。

如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:

class HomeCustomPage extends StatelessWidget {
  const HomeCustomPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: getAppBar("自定义UI"),body: CustomHome(),);
  }
}

class CustomHome extends Widget{
  @override
  Element createElement() {
    return HomeView(this);
  }

}

 

点击按钮则按钮文本会随机排序。

对于问题2,答案当然也是肯定的,Flutter 引擎提供的 API是原始且独立的,这个与操作系统提供的API类似,上层UI框架设计成什么样完全取决于设计者,完全可以将UI框架设计成 Android 风格或 iOS 风格,但这些事Google不会再去做,我们也没必要再去搞这一套,这是因为响应式的思想本身是很棒的,之所以提出这个问题,是因为笔者认为做与不做是一回事,但知道能不能做是另一回事,这能反映出我们对知识的理解程度。

RenderObject


每个Element都对应一个RenderObject,我们可以通过Element.renderObject 来获取。并且我们也说过RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树Render Tree。本节我们将重点介绍一下RenderObject的作用。

RenderObject就是渲染树中的一个对象,它主要的作用是实现事件响应以及渲染管线中除过 build 的部分(build 部分由 element 实现),即包括:布局、绘制、层合成以及上屏,这些我们将在后面章节介绍。

RenderObject拥有一个parent和一个parentData 插槽(slot),所谓插槽,就是指预留的一个接口或位置,这个接口和位置是由其它对象来接入或占据的,这个接口或位置在软件中通常用预留变量来表示,而parentData正是一个预留变量,它正是由parent 来赋值的,parent通常会通过子RenderObjectparentData存储一些和子元素相关的数据,如在Stack布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中(具体可以查看Positioned实现)。

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

为此,Flutter框架提供了一个RenderBox和一个 RenderSliver类,它们都是继承自RenderObject,布局坐标系统采用笛卡尔坐标系,这和Android和iOS原生坐标系是一致的,都是屏幕的top、left是原点,然后分宽高两个轴,大多数情况下。而 Flutter 基于这两个类分别实现了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加载模型。

#

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wywinstonwy/article/details/122872985

智能推荐

C# 使用ffmpeg视频截图_angzhuo9931的博客-程序员秘密

<appSettings> <add key="ffmpeg" value="E:\ffmpeg\ffmpeg-20141012-git-20df026-win32-static\bin\ffmpeg.exe" /> <add key="CatchFlvImgSize" value="240x180" /></appSettings&...

python工程师是什么专业-猎维科技告诉你人工智能工程师与Python工程师有什么区别?..._weixin_37988176的博客-程序员秘密

学习Python就等于学习人工智能吗?当然不!千万不要混淆它们的概念!Python工程师并不等同于人工智能工程师。(关注公众号"猎维人工智能”)Python工程师与人工智能工程师最根本的区别是什么?Python和JAVA、C语言一样,是一种编程语言,所以从事Python开发的人员也只能算是程序员。而人工智能工程师是从事算法研究的,所以被称作算法工程师。一个是程序员,一个是算法工程师。这两...

k8s 查看 删除,更新POD信息_k8s查看已经销毁的pod信息_dididiia的博客-程序员秘密

查看所有POD:kubectl  get pod查看某个POD:  kubectl  get pod name以JSON格式输出POD信息: kubectl get pod name --output json以yaml格式输出POD信息:  kubectl get pod name --output yaml删除某个POD:kubectl delete pod name删除所有POD(慎用):k...

Qt6.1教程:Qt Quick3D渲染实例_3d模型渲染 qt_不如温暖过生活的博客-程序员秘密

Qt是一个跨平台框架,通常用作图形工具包,它不仅创建CLI应用程序中非常有用。而且它也可以在三种主要的台式机操作系统以及移动操作系统(如Symbian,Nokia Belle,Meego Harmattan,MeeGo或BB10)以及嵌入式设备,Android(Necessitas)和iOS的端口上运行。现在我们为你提供了免费的试用版。下载Qt6最新试用版Qt组件推荐:QtitanRibbon|下载试用:遵循Microsoft Ribbon UI Paradigm for Qt技术的Ribb..

使用Docker Compose容器化Ruby on Rails应用程序进行开发_cukw6666的博客-程序员秘密

介绍 (Introduction)If you are actively developing an application, using Docker can simplify your workflow and the process of deploying your application to production. Working with containers in develo...

MySQL面试------收藏篇_奋斗小亮的博客-程序员秘密

学习链接:100道MySQL常见面试题总结写给 Java 程序员的 24 个MySQL面试题,拿走不谢!写给 Java 程序员的 24 个MySQL面试题,拿走不谢! 转MySQL经典面试题玩转Mysql系列 - 第24篇:如何正确的使用索引?...

随便推点

Python爬虫实战+数据分析+数据可视化(美团美食信息)_美团数据可视化分析_专业的小马哥的博客-程序员秘密

一、爬虫部分爬虫说明:1、本爬虫是以面向对象的方式进行代码架构的2、本爬虫爬取的数据存入到MongoDB数据库中3、爬虫代码中有详细注释4、爬虫爬取的美食是以无锡为例代码展示import jsonimport reimport timefrom pymongo import MongoClientimport requestsfrom lxml import htmlclass MeituanSpider(): def __init__(self): #

java的基本特征_狗贼爱逃跑的博客-程序员秘密

java的基本特征1.简单Java最初是为对家用电器进行集成控制而设计的一种语言,因此它必须简单明了。Java语言的简单性主要体现在以下三个方面:Java的风格类似于C++,因而C++程序员是非常熟悉的。从某种意义上讲,Java语言是C及C++语言的一个变种,因此,C++程序员可以很快就掌握Java编程技术。Java摒弃了C++中容易引发程序错误的地方,如指针和内存管理。Ja...

node-webkit中使用sqlite3(含编译教程)_node sqlite3 重新编译_carfge的博客-程序员秘密

node-webkit中使用sqlite3sqlite3的官方文档提到:nodejs和node-webkit的ABI不同,所以通过npm install sqlite3下载的sqlite3是无法使用的,需要重新编译。windows编译:以LTS版本(0.14.7)为例一、所需编译环境安装Python 2.7.14(不支持3.x版本)并设置好环境变量,下载地址:https://www.python.org/downloads/ 安装最新的nodejs+npm 下载地址:https...

UVA 1630 Folding_DS-K的博客-程序员秘密

题目链接:http://acm.hust.edu.cn/vjudge/problem/51191题意:给一个字符串,相同部分可以折叠,折叠可以嵌套。求最短长度的一种折叠方法。括号和数字的长度也要考虑进去。思路:对于一个字符串,有三种策略:1、不折叠。2、本身可以折叠。3、分为两个区间子问题。#include #include #include #inclu

推荐文章

热门文章

相关标签