Flutter的Widget-Element-RenderObject

简介: Flutter的Widget-Element-RenderObject

一. Flutter的渲染流程


1.1. Widget-Element-RenderObject关系


image.png                                                           3棵tree的关系

1.2. Widget是什么?

image.png

官方对Widget的说明:

  • Flutter的Widgets的灵感来自React,中心思想是构造你的UI使用这些Widgets。
  • Widget使用配置和状态,描述这个View(界面)应该长什么样子。
  • 当一个Widget发生改变时,Widget就会重新build它的描述,框架会和之前的描述进行对比,来决定使用最小的改变(minimal changes)在渲染树中,从一个状态到另一个状态。

自己的理解:

  • Widget就是一个个描述文件,这些描述文件在我们进行状态改变时会不断的build。
  • 但是对于渲染对象来说,只会使用最小的开销来更新渲染界面。


1.3. Element是什么?


image.png

官方对Element的描述:

  • Element是一个Widget的实例,在树中详细的位置。
  • Widget描述和配置子树的样子,而Element实际去配置在Element树中特定的位置。


1.4. RenderObject


![image-20200302155014847](/Users/xiaomage/Library/Application Support/typora-user-images/image-20200302155014847.png)

官方对RenderObject的描述:

  • 渲染树上的一个对象
  • RenderObject层是渲染库的核心。


二. 对象的创建过程


我们这里以Padding为例,Padding用来设置内边距


2.1. Widget


Padding是一个Widget,并且继承自SingleChildRenderObjectWidget

继承关系如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

我们之前在创建Widget时,经常使用StatelessWidget和StatefulWidget,这种Widget只是将其他的Widget在build方法中组装起来,并不是一个真正可以渲染的Widget(在之前的课程中其实有提到)。

在Padding的类中,我们找不到任何和渲染相关的代码,这是因为Padding仅仅作为一个配置信息,这个配置信息会随着我们设置的属性不同,频繁的销毁和创建。

问题:频繁的销毁和创建会不会影响Flutter的性能呢?

那么真正的渲染相关的代码在哪里执行呢?

  • RenderObject


2.2. RenderObject


我们来看Padding里面的代码,有一个非常重要的方法:

  • 这个方法其实是来自RenderObjectWidget的类,在这个类中它是一个抽象方法;
  • 抽象方法是必须被子类实现的,但是它的子类SingleChildRenderObjectWidget也是一个抽象类,所以可以不实现父类的抽象方法
  • 但是Padding不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现
@override
RenderPadding createRenderObject(BuildContext context) {
  return RenderPadding(
    padding: padding,
    textDirection: Directionality.of(context),
  );
}

上面的代码创建了什么呢?RenderPadding

RenderPadding的继承关系是什么呢?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject

我们来具体查看一下RenderPadding的源代码:

  • 如果传入的_padding和原来保存的value一样,那么直接return;
  • 如果不一致,调用_markNeedResolution,而_markNeedResolution内部调用了markNeedsLayout;
  • 而markNeedsLayout的目的就是标记在下一帧绘制时,需要重新布局performLayout;
  • 如果我们找的是Opacity,那么RenderOpacity是调用markNeedsPaint,RenderOpacity中是有一个paint方法的;
set padding(EdgeInsetsGeometry value) {
    assert(value != null);
    assert(value.isNonNegative);
    if (_padding == value)
      return;
    _padding = value;
    _markNeedResolution();
  }


2.3. Element


我们来思考一个问题:

  • 之前我们写的大量的Widget在树结构中存在引用关系,但是Widget会被不断的销毁和重建,那么意味着这棵树非常不稳定;
  • 那么由谁来维系整个Flutter应用程序的树形结构的稳定呢?
  • 答案就是Element。
  • 官方的描述:Element是一个Widget的实例,在树中详细的位置。

Element什么时候创建?

在每一次创建Widget的时候,会创建一个对应的Element,然后将该元素插入树中。

  • Element保存着对Widget的引用;

在SingleChildRenderObjectWidget中,我们可以找到如下代码:

  • 在Widget中,Element被创建,并且在创建时,将this(Widget)传入了;
  • Element就保存了对Widget的应用;
@override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);

在创建完一个Element之后,Framework会调用mount方法来将Element插入到树中具体的位置:

image.png                                                 mount方法

在调用mount方法时,会同时使用Widget来创建RenderObject,并且保持对RenderObject的引用:

  • _renderObject = widget.createRenderObject(this);
@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    assert(() {
      _debugUpdateRenderObjectOwner();
      returntrue;
    }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

但是,如果你去看类似于Text这种组合类的Widget,它也会执行mount方法,但是mount方法中并没有调用createRenderObject这样的方法。

  • 我们发现ComponentElement最主要的目的是挂载之后,调用_firstBuild方法
@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_active);
    _firstBuild();
    assert(_child != null);
  }
  void _firstBuild() {
    rebuild();
  }

如果是一个StatefulWidget,则创建出来的是一个StatefulElement

我们来看一下StatefulElement的构造器:

  • 调用widget的createState()
  • 所以StatefulElement对创建出来的State是有一个引用的
  • 而_state又对widget有一个引用
StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
  ....省略代码
  _state._widget = widget;

而调用build的时候,本质上调用的是_state中的build方法:

Widget build() => state.build(this);


2.4. build的context是什么


在StatelessElement中,我们发现是将this传入,所以本质上BuildContext就是当前的Element

Widget build() => widget.build(this);

我们来看一下继承关系图:

  • Element是实现了BuildContext类(隐式接口)
abstractclass Element extends DiagnosticableTree implements BuildContext

在StatefulElement中,build方法也是类似,调用state的build方式时,传入的是this

Widget build() => state.build(this);


2.5. 创建过程小结


Widget只是描述了配置信息:

  • 其中包含createElement方法用于创建Element
  • 也包含createRenderObject,但是不是自己在调用

Element是真正保存树结构的对象:

  • 创建出来后会由framework调用mount方法;
  • 在mount方法中会调用widget的createRenderObject对象;
  • 并且Element对widget和RenderObject都有引用;

RenderObject是真正渲染的对象:

  • 其中有markNeedsLayoutperformLayoutmarkNeedsPaintpaint等方法


三. Widget的key


在我们创建Widget的时候,总是会看到一个key的参数,它又是做什么的呢?


3.1. key的案例需求


我们一起来做一个key的案例需求

image.png                                                  key的案例需求

home界面的基本代码:

class _HYHomePageState extends State<HYHomePage> {
  List<String> names = ["aaa", "bbb", "ccc"];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Test Key"),
      ),
      body: ListView(
        children: names.map((name) {
          return ListItemLess(name);
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: () {
          setState(() {
            names.removeAt(0);
          });
        }
      ),
    );
  }
}

注意:待会儿我们会修改返回的ListItem为ListItemLess或者ListItemFul


3.2. StatelessWidget的实现


我们先对ListItem使用一个StatelessWidget进行实现:

class ListItemLess extends StatelessWidget {
  finalString name;
  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
  ListItemLess(this.name);
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      child: Text(name),
      color: randomColor,
    );
  }
}

它的实现效果是每删除一个,所有的颜色都会发现一次变化

  • 原因非常简单,删除之后调用setState,会重新build,重新build出来的新的StatelessWidget会重新生成一个新的随机颜色

image.png


3.3. StatefulWidget的实现(没有key)


我们对ListItem使用StatefulWidget来实现

class ListItemFul extends StatefulWidget {
  finalString name;
  ListItemFul(this.name): super();
  @override
  _ListItemFulState createState() => _ListItemFulState();
}
class _ListItemFulState extends State<ListItemFul> {
  final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 60,
      child: Text(widget.name),
      color: randomColor,
    );
  }
}

我们发现一个很奇怪的现象,颜色不变化,但是数据向上移动了

  • 这是因为在删除第一条数据的时候,Widget对应的Element并没有改变;
  • 而Element中对应的State引用也没有发生改变;
  • 在更新Widget的时候,Widget使用了没有改变的Element中的State;

image.png


3.4. StatefulWidget的实现(随机key)


我们使用一个随机的key

ListItemFul的修改如下:

class ListItemFul extends StatefulWidget {
  finalString name;
  ListItemFul(this.name, {Key key}): super(key: key);
  @override
  _ListItemFulState createState() => _ListItemFulState();
}

home界面代码修改如下:

body: ListView(
  children: names.map((name) {
    return ListItemFul(name, key: ValueKey(Random().nextInt(10000)),);
  }).toList(),
),

这一次我们发现,每次删除都会出现随机颜色的现象:

  • 这是因为修改了key之后,Element会强制刷新,那么对应的State也会重新创建
// Widget类中的代码
staticbool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}

image.png


3.5. StatefulWidget的实现(name为key)


这次,我们将name作为key来看一下结果:

body: ListView(
  children: names.map((name) {
    return ListItemFul(name, key: ValueKey(name));
  }).toList(),
),

我们理想中的效果:

  • 因为这是在更新widget的过程中根据key进行了diff算法
  • 在前后进行对比时,发现bbb对应的Element和ccc对应的Element会继续使用,那么就会删除之前aaa对应的Element,而不是直接删除最后一个Element

image.png


3.6. Key的分类


Key本身是一个抽象,不过它也有一个工厂构造器,创建出来一个ValueKey

直接子类主要有:LocalKey和GlobalKey

  • LocalKey,它应用于具有相同父Element的Widget进行比较,也是diff算法的核心所在;
  • GlobalKey,通常我们会使用GlobalKey某个Widget对应的Widget或State或Element


3.6.1. LocalKey


LocalKey有三个子类

ValueKey:

  • ValueKey是当我们以特定的值作为key时使用,比如一个字符串、数字等等

ObjectKey:

  • 如果两个学生,他们的名字一样,使用name作为他们的key就不合适了
  • 我们可以创建出一个学生对象,使用对象来作为key

UniqueKey

  • 如果我们要确保key的唯一性,可以使用UniqueKey;
  • 比如我们之前使用随机数来保证key的不同,这里我们就可以换成UniqueKey;

3.6.2. GlobalKey

GlobalKey可以帮助我们访问某个Widget的信息,包括Widget或State或Element等对象

我们来看下面的例子:我希望可以在HYHomePage中直接访问HYHomeContent中的内容

class HYHomePage extends StatelessWidget {
  final GlobalKey<_HYHomeContentState> homeKey = GlobalKey();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("列表测试"),
      ),
      body: HYHomeContent(key: homeKey),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.data_usage),
        onPressed: () {
          print("${homeKey.currentState.value}");
          print("${homeKey.currentState.widget.name}");
          print("${homeKey.currentContext}");
        },
      ),
    );
  }
}
class HYHomeContent extends StatefulWidget {
  finalString name = "123";
  HYHomeContent({Key key}): super(key: key);
  @override
  _HYHomeContentState createState() => _HYHomeContentState();
}
class _HYHomeContentState extends State<HYHomeContent> {
  finalString value = "abc";
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
相关文章
|
Dart 算法 前端开发
Day10 - Flutter的Widget-Element-RenderObject
Day10 - Flutter的Widget-Element-RenderObject
95 0
Day10 - Flutter的Widget-Element-RenderObject
|
2月前
|
缓存 监控 前端开发
【Flutter 前端技术开发专栏】Flutter 应用的启动优化策略
【4月更文挑战第30天】本文探讨了Flutter应用启动优化策略,包括理解启动过程、资源加载优化、减少初始化工作、界面布局简化、异步初始化、预加载关键数据、性能监控分析以及案例和未来优化方向。通过这些方法,可以缩短启动时间,提升用户体验。使用Flutter DevTools等工具可助于识别和解决性能瓶颈,实现持续优化。
【Flutter 前端技术开发专栏】Flutter 应用的启动优化策略
|
19天前
|
开发框架 前端开发 测试技术
Flutter开发常见问题解答
Flutter开发常见问题解答
|
2月前
|
前端开发 C++ 容器
Flutter-完整开发实战详解(一、Dart-语言和-Flutter-基础)(1)
Flutter-完整开发实战详解(一、Dart-语言和-Flutter-基础)(1)
|
7天前
|
开发框架 移动开发 Android开发
构建高效移动应用:探索Flutter开发框架
【6月更文挑战第28天】随着移动设备的普及,用户对移动应用的需求日益增长。开发者面临着在众多平台间提供无缝体验的挑战。本文深入探讨了Flutter框架如何通过其跨平台特性、热重载功能以及丰富的组件库简化移动应用的开发流程,同时确保高性能和优雅的用户界面设计。
16 2
|
2月前
|
Dart 安全
简化代码、提高效率:Dart和Flutter开发小技巧
在日常开发中,我们常常会使用一些常用的技巧或语法糖,以简化代码、提高开发效率。本文将分享一些在Dart和Flutter中常用的小贴士,帮助你更轻松地编写优雅高效的代码。
简化代码、提高效率:Dart和Flutter开发小技巧
|
25天前
|
Dart 监控 测试技术
在Flutter开发中,注重代码质量与重构实践显得尤为重要
【6月更文挑战第11天】随着Flutter在跨平台开发的普及,保持高质量代码成为开发者关注的重点。良好的代码质量关乎应用性能、稳定性和开发效率。为提升Flutter代码质量,开发者应遵循最佳实践,编写可读性高的代码,实施代码审查和自动化测试。重构实践在应对代码复杂性时也至关重要,包括识别重构时机、制定计划、逐步操作及利用重构工具。注重代码质量和重构是Flutter开发成功的关键。
36 3
|
3天前
|
Dart Android开发 iOS开发
flutter插件开发
flutter插件开发
|
17天前
|
移动开发 小程序 安全
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
|
2月前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践