flutter学习-Widget-Element-RenderObject

1. Flutter渲染流程

1.1 Widget-Element-RenderObject的关系

在这里插入图片描述

1.2 什么是Widget

在这里插入图片描述

  • 官方对Widget的说明:
    • Flutter的Widgets的灵感来自React,中心思想是构造你的UI使用这些Widgets。
    • Widget使用配置和状态,描述这个View(界面)应该长什么样子。
    • 当一个Widget发生改变时,Widget就会重新build它的描述,框架会和之前的描述进行对比,来决定使用最小的改变(minimal changes)在渲染树中,从一个状态到另一个状态
  • 自己的理解:
    • Widget就是一个个描述文件,这些描述文件在我们进行状态改变的时候会不断build
    • 但对于渲染对象来说,只会使用最小的开销来渲染界面

1.3 Element是什么?

An instantiation of a [Widget] at a particular location in the tree.

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

1.4 RenderObject

  • 官方说明:
    • 渲染树上的一个对象
    • RenderObject层是渲染库的核心

2. 对象的创建过程

这里我们以PaddingWidget为例,来窥探这三者之间的关系

2.1 Widget

Padding是一个Widget集并且继承自SingleChildRenderObjectWidget ,最终的集成关系如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

我们之前在创建Widget时,经常使用StatelessWidgetStatefulWidget,这种Widget只在将其他的Widget在build方法中组织起来,并不是一个真正可以渲染的Widget

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

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

  • RenderObject

2.2 RenderObject

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

  • 这个方法其实是来自于RenderObjectWidget的类,在这个类中它是一个抽象方法

  • 这个抽象方法必须被子类实现,但是它的子类SingleChildRenderObjectWidget也是一个抽象类,所以可以不实现父类的抽象方法

  • 但是Padding不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现

  • RenderObject中的抽象方法
    在这里插入图片描述

//Padding的实现
@override
  RenderPadding createRenderObject(BuildContext context) {
    return RenderPadding(
      padding: padding,
      textDirection: Directionality.maybeOf(context),
    );
  }

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

  • RenderPadding的继承关系是什么?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject

我们具体查看下RenderPadding的源码:
在这里插入图片描述

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

2.3 Element

我们来思考一个问题

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

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

  • Element保存着对Widget的引用

SingleChildRenderObjectWidget中我们可以找到如下代码:
在这里插入图片描述

  • 在Widget中,Element被创建,并且在创建时,将this(Widget)传入了;
  • Element就保存在对应的Widget中

在创建完一个Element之后,Framework会调用mount方法来将Element插入树中具体的位置
在调用mount方法时,会使用Widget来创建RenderObject,并且保持对RenderObject的引用

  • _renderObject = widget.createRenderObject(this);
//RenderObjectElement中的mount方法实现
void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(() {
      _debugDoingBuild = true;
      return true;
    }());
    _renderObject = widget.createRenderObject(this); //创建RenderObject对象
    assert(() {
      _debugDoingBuild = false;
      return true;
    }());
    assert(() {
      _debugUpdateRenderObjectOwner();
      return true;
    }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

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

  • 我们发现ComponentElement最主要的 目的是挂在之后,调用_firstBuild()方法
    在这里插入图片描述

如果是一个StatefulWidget,则创建出来一个StatefulElement
我们来看下StatefulElement的构造器

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

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

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

2.4 build中的Context是什么

在StatelessElement中,我们发现将this传入,所以本质上BuildContext就是当前的Element:Widget build() => state.build(this);

我们来看下继承关系图:

  • Element是实现了BuildContext类(隐式接口)

abstract class Element extends DiagnosticableTree implements BuildContext

在StatefulElement中,build方法也是类似,调用state的build方式时,传入的是this(Widget build() => state.build(this);)

2.5 创建过程小结

Widget只是配置了描述信息:

  • 其中包含createElement方法用于创建Element
  • 也包含createRenderObject,但是不是自己在调用
  • 某些Widget中会创建RenderObject,但是有些WIdget是不会创建的
  • 每一个Widget都会创建一个Element对象

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

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

RenderObject是真正渲染的对象:

  • 其中有markNeedsLayout performLayout markNeedsPaint paint等方法

3. Widget中的key

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

3.1 key的案例需求

我们可以一起来做下key的需求
在这里插入图片描述

  • home界面的基本代码:
class GYHomePage extends StatefulWidget {
  const GYHomePage({Key? key}) : super(key: key);

  @override
  _GYHomePageState createState() => _GYHomePageState();
}

class _GYHomePageState extends State<GYHomePage> {
  List<String> items = ["aaaaa","bbbbb","ccccc"];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("test key"),
      ),
      body: ListView(
        children: items.map((name) {
          return GYHomeItemless(name);
        }).toList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          setState(() {
            items.removeAt(0);
          });

        },
      ),
    );
  }
}

3.2 StatelessWidget实现

class GYHomeItemless extends StatelessWidget {
  final String name;
  final randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));

  GYHomeItemless(this.name);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 80,
      color: randomColor,
      child: Text(name, style: TextStyle(color: Colors.white, fontSize: 30),),
    );
  }
}

我们来查看实现效果:
在这里插入图片描述

由上图可知,它是没删除一个,其它的颜色都会发生一次变化

  • 原因: 因为你每删除一次,调用setState方法,就会重新调用build方法,那么重新build出来的StatelessWidget会重新生成一个颜色

3.3 StatefulWidget实现(没有key)

在这里插入图片描述

class GYHomeitemful extends StatefulWidget {
  final String name;

  GYHomeitemful(this.name);

  @override
  _GYHomeitemfulState createState() => _GYHomeitemfulState();
}

class _GYHomeitemfulState extends State<GYHomeitemful> {
  final randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
  @override
  Widget build(BuildContext context) {
    return Container(
      height: 80,
      color: randomColor,
      child: Text(this.widget.name, style: TextStyle(color: Colors.white, fontSize: 30),),
    );
  }
}

我们可以发现一个很奇怪的现象,颜色没有改变,但是数据像上移动了

  • 这是因为在删除第一条数据的时候,Widget对应的Element并没有改变
  • 而Element中对应的State引用也没有发生改变
  • 在更新Widget的时候,Widget使用了没有改变的Element中的State
  1. 首先每次调用setState的时候, Widget都会重新调用build方法,然后重建, 那么Element tree会不会重建了, 这里Element会调用Widget中的一个方法
    在这里插入图片描述

该方法是判断该Element需不需要重建,这里比较两个widget(runtimeType和key),当返回true的时候,就表示是完全一样的widget,那么就不会创建新的Element

这里调用setState方法之后, 就会重新生成一个新的widget tree, 饭后比较新旧widget, 发现有两个Element对象是完全可以用的不用重建,所以就直接使用, 那么该Element中保存的state的一些信息 就直接展示, 最后一个发现没有widget比较所以直接删除,这就是为什么颜色没有改变,数据像上移动的原因

在这里插入图片描述

没有重建的Element会保存以前的state对象的信息, 然后重新和新生成的widget对象,关联上

3.4 StatefulWidget的实现(随机key)

GYHomeitemful代码修改如下:

class GYHomeitemful extends StatefulWidget {
  final String name;

  GYHomeitemful(this.name,{Key? key}) : super(key: key);

  @override
  _GYHomeitemfulState createState() => _GYHomeitemfulState();
}

home代码修改

body: ListView(
        children: items.map((name) {
          //UniqueKey(), flutter会自动帮我们创建一个唯一的key
          //这里也可以使用随机数,但是使用随机数也有可能重复
          return GYHomeitemful(name, key: UniqueKey(),);
        }).toList(),
      ),

在这里插入图片描述

我们可以发现,当我们设置了key属性之后,当我们删除时每次的widget都出现了随机颜色的现象,这是因为修改了key以后,在比较widget的时候肯定会不通过,那么就会重新创建Element,Element会强制刷新,那么对应的state也会重建

3.5 StatefulWidget的实现(使用名称作为key)

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

这次实现了我们理想的效果:

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

3.6 Key的分类

key本身是一个抽象类,不过它有一个工程构造器,创建出来一个ValueKey,直接的子类有:LocalKeyGlobalKey

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

3.6.1 LocalKey

LocalKey有三个子类

  • ValueKey:当我们使用特定的值作为key时,比如一个字符串,数字等等
  • ObjectKey:在某些时候,单存的字符串,等属性不能确定唯一的时候,这个时候我们可以创建出一个对象来作为key
  • UniqueKey:比如要确保唯一性,就可以使用UniqueKey对象

3.6.2 GlobalKey

GlobalKey可以帮助我们访问某个Widget的信息,包括Widget或State或Element等对象
下面例子,我们可以在GYHomePage中访问GYHomeContent中的属性

class GYHomePage extends StatelessWidget {
  //创建一个GlobalKey
  final GlobalKey<_GYHomeContentState> _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("test key"),
      ),
      body: GYHomeContent(key: _globalKey,),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.delete),
        onPressed: (){
          print("获取绑定key的widget属性:${_globalKey.currentState?.widget.name}");
          print("获取绑定key的widget属性:${_globalKey.currentState?.value}");

        },
      ),
    );
  }
}

class GYHomeContent extends StatefulWidget {
  final String name = "test_globalKey";
  const GYHomeContent({Key? key}) : super(key: key);

  @override
  _GYHomeContentState createState() => _GYHomeContentState();
}

class _GYHomeContentState extends State<GYHomeContent> {
  final String value = "test_value";
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

打印结果:
在这里插入图片描述

其实GlobalKey是一个map集合在管理,我可以看下面源码:
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值