Day10 - Flutter的Widget-Element-RenderObject

概述

  • Flutter的渲染流程
  • 对象的创造过程
  • 小部件的键
一、Flutter的渲染流程
  • 1.1、Widget的Element和RenderObject 的关系

    3棵树的关系
  • 1.2、Widget(窗口小部件) 是什么?

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

    Element(`元素`)
    • 官方对Element的描述:
      • Element是一个小部件的实例,在树中详细的位置。
      • 窗口小部件描述和配置子树的样子,而元素实际去配置在元素树中特定的位置。
  • 1.4、RenderObject(渲染对象),官方对RenderObject的描述:

    • 渲染树上的一个对象
    • RenderObject层是渲染库的核心。
二、对象的创造过程

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

  • 2.1、Widget(小部件)
    填充是一个小部件,并且继承自SingleChildRenderObjectWidget
    继承关系如下:

    Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
    

    我们之前在创建Widget时,经常使用StatelessWidget和StatefulWidget,这种Widget只是将其他的Widget在构建方法中组装起来,并不是一个真正可以渲染的Widget(在之前的课程中其实有提到)。
    在Padding的类中,我们发现任何和渲染相关的代码,这是因为Padding只是一个配置信息,这个配置信息会转移我们设置的属性不同,可以替换的销毁和创建。

    • 问题:不断的销毁和创造会不会影响Flutter的性能呢?
      答:并不会,答案在另一篇文章中:你不必担心Dart的垃圾回收器
    • 那么真正的渲染相关的代码在哪里执行呢?
      答:渲染对象
  • 2.2、渲染对象
    我们来看Padding里面的代码,有一个非常重要的方法:

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

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

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

      @override
      RenderPadding createRenderObject(BuildContext context) {
          return RenderPadding(
              padding: padding,
              textDirection: Directionality.of(context),
          );
      }
      

    顶部的代码创建了什么呢?RenderPadding的继承关系是什么呢?

    RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
    
    • 我们来具体查看一下RenderPadding的源代码:
      • 如果预设的_padding和原来保存的value一样,那么直接返回;

      • 如果绝对,调用_markNeedResolution,而_markNeedResolution内部调用了markNeedsLayout;

      • 而markNeedsLayout的目的就是标记在下一帧布局时,需要重新布局performLayout;

      • 如果我们找的是Opacity,那么RenderOpacity是调用markNeedsPaint,RenderOpacity中是有一个油漆方法的;

        set padding(EdgeInsetsGeometry value) {
            assert(value != null);
            assert(value.isNonNegative);
            if (_padding == value)
                 return;
                 _padding = value;
                 _markNeedResolution();
            }
        }
        
  • 2.3、Element(元件)
    我们来思考一个问题:

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

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

    • Element保存着对Widget的引用;

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

    • 在Widget中,元素被创造,并且在创造时,将this(Widget)设定了;

    • Element就保存了对Widget的应用;

      @override
      SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
      

    在创建完一个元素之后,框架会调用安装方法来将元素插入到树中具体的位置:

    在调用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();
           return true;
       }());
       assert(_slot == newSlot);
       attachRenderObject(newSlot);
       _dirty = false;
    }
    

    但是,如果您去看一下Text这种组合类的小部件,它也会执行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中,我们发现是将这个合并,所以本质上BuildContext就是当前的Element

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

    我们来看一下继承关系图:元素是实现了BuildContext类(隐式接口)

    abstractclass Element extends DiagnosticableTree implements BuildContext
    

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

    Widget build() => state.build(this);
    
  • 2.5、创建过程小结

    • 窗口小部件只是描述了配置信息:
      • 其中包含createElement方法用于创建元素
      • 也包含createRenderObject,但不是自己在调用
    • 元素是真正的保存树结构的对象:
      • 创建出来后会由framework调用mount方法;
      • 在mount方法中会调用widget的createRenderObject对象;
      • 并且Element对widget和RenderObject都有引用;
    • RenderObject 是真正渲染的对象:其中有 markNeedsLayoutperformLayoutmarkNeedsPaintpaint 等方法
三、小部件的键

在我们创造的小部件的时候,总是会看到一个关键的参数,它又是做什么的呢?

  • 3.1、key的案例需求

    key的案例需求
    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);
                    });
                 }
               ),
          );
       }
    }
    
  • 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,会重新构建,重新构建出来的新的StatelessWidget会重新生成一个新的随机颜色

  • 3.3、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对应的元素并没有改变;
    • 而元素中对应的状态引用也没有发生改变;
    • 在更新Widget的时候,Widget使用了没有改变的Element中的State;
  • 3.4、StatefulWidget的实现(随机键)
    我们使用一个随机的key,ListItemFul的修改如下:

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

    主页界面代码修改如下:

    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;
     }
    
  • 3.5、StatefulWidget的实现(名称为键)
    这次,我们将名称作为key来看一下结果

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

    我们理想中的效果:

    • 因为这是在更新widget的过程中根据key进行了diff算法
    • 在前后进行对比时,发现bbb对应的元素和ccc对应的元素会继续使用,那么就会删除之前aaa对应的元素,而不是直接删除最后一个元素
  • 3.6、钥匙的分类
    Key本质是一个抽象,不过它也有一个工厂构造器,创建出来一个ValueKey
    直接子类主要有:LocalKey和GlobalKey

    • LocalKey,它具有相同父元素的小部件进行比较,也是diff算法的核心所在;

    • GlobalKey,通常我们会使用GlobalKey某个Widget对应的Widget或State或Element

    • 3.6.1、本地密钥
      LocalKey有三个子类

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