flutter的main函数过程

Widget

  • 除渲染外的所有类, 常见的有4种
    1. StatelessWidget
    2. StatefulWidget
    3. RendObjectWidget
    4. InheritedWidget(<font color=green>这里先不说这个类</font>)

其中 <font color=red>RenderObjectWidget是一个抽象类</font>. 在实际开发中基本上使用的都是前2个.
<font color=red>RenderObjectWidget</font>也是一种Widget, 它由用户间接提供渲染的数据给RenderObject. 如Text再被构造时, 内部的build方法会依赖Text数据再去创建一个RenderObjectWidget的子类, 相当于用户的StatelessWidget所构造的数据间接传递给了RenderObjectWidget, 最后再合适的时机将数据提供给渲染结点RenderObject

Element

  • 由Widget创建, 不同的Widget会创建不同的Element
Widget Element
StatelessWidget StatelessElement
StatefulWidget StatefulElement
RendObjectElement RendObjectElement(<font color=green>抽象类</font>)

上面所有的Element都是由Widget来创建, 这意味着在开发中, 用户不需要自己创建Element. 并且在运行的过程中. 用户构建不同的Widget, 所创建出来的Element也不一样


RenderObject

  • RenderObjectWidget创建, 它是渲染的结点数据. 在程序的运行过程中, 不同的RenderObjectWidget所创建出来的RenderObject是不一样的.

众所周知, 屏幕显示出来的视图在逻辑上是分层的. 这个结构就是渲染树. 树上的每个结点记录着某一层的视图状态. 在Flutter中直接渲染的数据所定义的结构就是RenderObject. 它实际上就是树中的结点.

Widget-Tree

  • 上述的Widget主要由应用层面的用户来编写. 以 <font color=red>StatelessWidgit为例</font>, 嵌套的形成是由 <font color=red>用户的build方法,逐步递归创建Widget</font>.

    同理 <font color=red>StatefulWidget</font>也是一样的.

  • 若从这一点来看, 用户编写的所有Widget在逻辑上就是一棵树

这里要提一点: 从代码的调用流程来看, 若用户创建的Widget都是child, 则在逻辑上实际上形成的是链表. 但实际开发中用户会创建children类型的Widget, 这就造成在逻辑上形成的是一棵树结构.

  • Widget-Tree在开发中会被频繁的创建和销毁(build方法), 所以若是直接渲染这棵树, 性能会大大下降. 也就是说Widget-Tree并不适合做渲染的原数据.

Element-Tree

  • 由Element对象组成的树结构, 一个Widget必须创建一个Element, 也就是说 <font color=red>用户创建的Widget和Elment是一一对应的</font>.

这里先这样理解, 后面细说它们之间的这种对应是怎么优化的


RenderObject-Tree

  • 由 <font color=red>结点构成的树</font>, 这一层是Flutter渲染时真正的状态数据, 这一层在实现上要尽量保持稳定.
  • 在Element-Tree中, 有一部分的Element是RenderObjectElement, 所有的RenderObject由这类Element生成, 所以RenderObject-Tree并不和Element-Tree是一一对应的


3种树的关系

  • 所有的Widget构成Widget-Tree, 这棵树形成在build的递归调用中. 当调用build后, 仅仅只返回了一个Widget, 此时并不会创建Element, 更不会创建RenderObject

也就是说:

MyWidget1 extends StatelessWidget{
    @override
    Widget build(BuildContext ctx){
        return MyWidget2();
    }
}
MyWidget2 extends StatelessWidget{
    @override
    Widget build(BuildContext ctx){
        return MyWidget3();
    }
}
MyWidget3 extends StatelessWidget{
    @override
    Widget build(BuildContext ctx){
        return Text("hello");
    }
}

对于上面的代码, 当MyWidget1被构建时, 并不会创建Element


main函数启动加载过程

以下面这个代码为例

void main(List<String> args) {
   runApp(Text("hello flutter", textDirection: TextDirection.ltr,));
 }


  • 创建用户的Widget, 即Text, 此时 <font color=red>仅仅是返回Text, 并未创建对应的Element</font>


  • 调用runApp函数

一般来说, 窗口程序的启动函数的功能是:1. 向操作系统索要一个窗口; 2. 并开启事件循环


  • 创建全局核心对象WidgetBinding
  • 包装用户的Widget(Text)
  • 开始渲染
  • 唤醒更新frame
void runApp(Widget app) {
  final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
  assert(binding.debugCheckZone('runApp'));
  binding
    ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
    ..scheduleWarmUpFrame();
}

binding对象负责管理窗口, 事件等. 相当于iOS中的APPDelegate
scheduleAttachRootWidget只是准备渲染


  • 渲染过程(只展示核心代码)
    1. 创建RenderObjectWidget
    2. 创建RenderObjectElement, 并且这个Element为树的根节点
void attachRootWidget(Widget rootWidget) {
    final bool isBootstrapFrame = rootElement == null;
    _readyToProduceFrames = true;
    _rootElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    )           // 创建RenderObjectWidget


        // 返回 RenderObjectElement, 
    .attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
    if (isBootstrapFrame) {
      SchedulerBinding.instance.ensureVisualUpdate();
    }
}

这一整个函数的的结果是返回了一个rootRenderObjectElement. 内部的widget为最开始的被包装的Text,renderObject为全局对象的containerView


  • 返回rootElement的过程
    1. 创建RenderObjectElement
    2. 引用全局的BuildOwner
    3. 挂载
// 当前对象是 RenderObjectToWidgetAdapter, 也就是 RenderObjectWidget
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();              // 创建rootElement, 并且 绑定widget为this
        assert(element != null);
        element!.assignOwner(owner);                
      });
      owner.buildScope(element!, () {
        element!.mount(null, null);             // 最核心的挂载
      });


      // 下面的不看
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element!;
  }

挂载是Element的核心, 每一种不同子类的Element的挂载效果是不一样的. 比如
StatelessElement和StatefulElement所实现的挂载是要调用build, 然后更新 子Element的widget. 这中间的动作会造成递归调用所有build方法得到子widget, 并依次创建对应的Element
RenderObjectElement的挂载只是更新子Element


Element的mount公用处理

  • 上面的mount是RenderObjectElement的, 对于公用的父类Element, 有所有Element的mount的公共处理

// this当前是Element, 要将 this挂载到指定的 parent中


void mount(Element? parent, Object? newSlot) {
    assert(_lifecycleState == _ElementLifecycle.initial);
    assert(_parent == null);
    assert(parent == null || parent._lifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    
    // 若当前 this的状态不是初始化, 则异常
    // 若当前 this已经有 parent了,  则异常
    // 若当前 指定的parent为空, 则表示this是根据节点
    //      或 指定了parent, 但自己的状态已经是active了, 也是异常
    // 若当前 this的槽不为空, 则异常

    // 以上断言的总结就是:  自己是刚被初始化状态时, 才可以被挂载
    /// 初始化时, 自己一定没有槽, 并且一定没有 parent



    _parent = parent;
    _slot = newSlot;
    _lifecycleState = _ElementLifecycle.active;
    _depth = _parent != null ? _parent!.depth + 1 : 1;
    if (parent != null) {
        _owner = parent.owner;
    }

    // 以上是为 this绑定 父结点和槽
    // 指定自己的深度
    // 引用全局的 BuildOwner对象


    assert(owner != null);

    final Key? key = widget.key;
    if (key is GlobalKey) {
      owner!._registerGlobalKey(key, this);
    }
    // 这一步暗示了 GrobalKey的作用
    // 即若指定的 Widget的Key的类型是GlobalKey,则会被全局引用
    // 它是 GlobalKey共享数据的原因


    _updateInheritance();
    attachNotificationTree();
    // 这2步这里先不说
  }

以上是所有Element在挂载时要做的相同动作


RenderObjectElement的挂载过程

  • 这一步的挂载就是接着runApp的代码所来
    1. 调用公共处理(Element.mount)
    2. 创建RenderObject
    3. 临时的为当前的 this 生成节点信息, 但最后要不要挂载, 要在后面做虚拟DOM的diff算法
void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(() {
      _debugDoingBuild = true;
      return true;
    }());

    _renderObject = (widget as RenderObjectWidget).createRenderObject(this);        // __code_0
    assert(!_renderObject!.debugDisposed!);
    assert(() {
      _debugDoingBuild = false;
      return true;
    }());
    assert(() {
      _debugUpdateRenderObjectOwner();
      return true;
    }());
    assert(_slot == newSlot);

    // 内部会判断要不要挂载, 虚拟dom的diff算法
    attachRenderObject(newSlot);
    super.performRebuild(); // clears the "dirty" flag
  }




  // 该函数虽然是override, 但事实上 RenderObjectElement并未调用 Element的实现, 对于
  // RenderObjectElement attachRenderObject函数自己的操作
@override
 void attachRenderObject(Object? newSlot) {
    assert(_ancestorRenderObjectElement == null);

    // 当前 renderObjectElement对象的 槽(slot)被更新为指定的newSlot
    _slot = newSlot;

    // 离 this(RenderObjectElement)最近的 祖先结点
    // 即在 Element-Tree中通过 parent往上找, 找到最近的 RenderObjectElement的结点
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();


    // 找到最近的 祖先(RenderObjectElement)后, 将 this.renderObject挂载到 祖先的renderObject的child下
    // 这一步相当于在 操作 RenderObject-Tree(第3层)
    // 并且也可以得出这样一个结论, RenderObjectElement是Element-Tree中的一部分
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);


    
    // 因为上面会改变 RenderObject-Tree(插入了新结点)
    // 这里根据官方说的, 要更新 最近祖先的数据, 这里的祖先和_ancestorRenderObjectElement 不同
    // 它所表示的类型是  ParentDataElement, 也是RenderObject-Tree中的RenderObject结点
    //  这里查找的结果parentDataElement 可能等于_ancestorRenderObjectElement
    //  也可能是_ancestorRenderObjectElement的祖先结点
    final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null) {
        // 所以parentDataElement.widget一定是 RenderObjectWidget(用户提供的渲染数据)
      _updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
    }
  }

__code_0在创建RenderObject, 由Widget创建. 换句话说:RenderObject是由RenderObjectWidget所创建
上面2个函数总结就是: RenderObjectWidgetElement通过引用的RenderObjectWidget创建渲染结点(RenderObject), 并将创建的结点附加到RenderObject-Tree中, 当添加到渲染树中时, 会更新最近的父结点的数据
它为什么要更新父结点的数据? 以一个场景来说: 若在树中插入了一个新的结点(如用户提供了RichText后, 根据流程会创建RenderObject)后, 当前节点往上的部分可能是Flex布局在操作, 新节点的可能会引起Flex要重新计算,并排布界面, 所以要在当前结点上往上找到父节点, 然后重新计算布局数据
ps: <font color = red>所以更新父节点的后续操作中, 所谓的父节点可能是Flex, 可能是Positioned等, 它们要重新布局</font>


RenderObjectToWidgetElement的mount

  • 上面所有的mount所指向的this事实上是RendObjectToWidgetElement对象
    1. 调用父类的mount
    2. _rebuild
void mount(Element? parent, Object? newSlot) {
    assert(parent == null);
    super.mount(parent, newSlot);
    _rebuild();
    assert(_child != null);
  }


   void _rebuild() {
    try {

        // 这里更新子Element
      _child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);

    } catch (exception, stack) {
        ...
    }
  }

</br/>

Element更新子Element过程<a id="elemnt-update"/>

  • 该方法是Element-Tree实现虚拟DOM的核心, 方法如下:
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {

    // 若当前要更新的 子element(child) 没有指定widget(newWidget)则表示
    // 当 子element没有对应widget,即直接从 Element-Tree删除
    // 但 child 只是从Element-Tree中删除, 它会被记录到 BuildOwner不活动的map中
    if (newWidget == null) {
      if (child != null) {
        deactivateChild(child);
      }
      return null;
    }

    final Element newChild;
    if (child != null) {
        // 它表示的意义是: 当前this的子element(child) 的类型 是否为 widget所对应的element类型
        // 即 StatelessWidget应该对应StatelessElement
        //    StatefulWidget应该对应StatefulElement
        // 这样的意义是 若 子element(child) 和 widget(newWidget) 类型对应时
        //// 会直接在 子element(child)下作更新操作, 然后 直接返回 element
        //// 若类型不对应, 则表示原来 child可能是 StatelessElement, 现在对应要更新的widget(newWidget)却为 StatefulWidget, 则要重新删除 child, 并创建新的Element, 然后会重新mount
      bool hasSameSuperclass = true;



        /// 这一段的断言的作用是: 在debug模式下热重载时
        /// 用户可能直接 更改了 build中前后 类型不同的 Widget
        //// 如 热重载前是 StatefulWidget
        ////  然后用户修为 StatelessWidget
        //// 然后按下热重载 element的widget指向的还是StatefulWidget, 则可能会crash, 所以这里在debug热重载作一个断言, 时时判断前后的类型是不是不一致
        /// ps: 在release下, 没有热重载功能, 所有的东西都是编译好的, 外界调用该函数都是  element.updateChild(element.child, element.widget.child, slot) 不会出现 element.child的类型 与 element.widget.child不对应问题
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      
      
        // 若类型对应, 并且 子element前后的widget并未改变
        /// 则只更新槽后直接返回
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot) {
          updateSlotForChild(child, newSlot);
        }
        newChild = child;



            // 若类型对应, 但不是同一个widget, 但它们的key相等Widget.canUpdate, 则也是更新槽, 同时要替换 child的widget
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot) {
          updateSlotForChild(child, newSlot);
        }
        
        
        
        ... 其他不相关的代码
        
        
        
        child.update(newWidget);            // 替换 child的widget
        
        
        ... 其他不相关的代码
        
        
        newChild = child;
      } else {
      
      
      /// 类型不对应 或对应但它们的key不同, 则将 child从Element-Tree中删除
      /// 然后创建新的 newChild, 后续的过程见单独的inflateWidget探究
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
    
    
    /// child为空时, 直接创建新的 Element
      newChild = inflateWidget(newWidget, newSlot);
    }

    .... 其他不相关代码

    return newChild;
  }

这里所谓的diff算法, 本质是通过前后Widget的类型和key是不是一样的(Widget.canUpdate), 若一样, 则不创建Element


updateChild时创建子Element(inflateWidget)

  • 该方法由Element实现, 也有对应的子类, 这里看公共处理
Element inflateWidget(Widget newWidget, Object? newSlot) {

    ... 其他不相关代码


    // 若当newWidegt是一个全局对象, 即它的key为GlobalKey类型 从全局缓存中取
    //// 有一个全局map, key为GlobalKey对象, value是该对象引用的Element
    // 若取到Global对象, 则找到对应的element则并挂载到this(_activateWithParent)下
    // 以取到的 
    try {
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        final Element? newChild = _retakeInactiveElement(key, newWidget);
        if (newChild != null) {
        
          newChild._activateWithParent(this, newSlot);
          final Element? updatedChild = updateChild(newChild, newWidget, newSlot);

          return updatedChild!;
        }
      }
      
      
      // 若newWidget不是全局对象, 则直接通过它来创建新的element
      final Element newChild = newWidget.createElement();
      
      // 将新element挂载到 当前elmement中
      // 此时因为Widget可能是不同类型的Widget, 所以调用mount方法后
      // 可能具体的实现也就不同,
      newChild.mount(this, newSlot);            // __code_0
      


      return newChild;
    } finally {
      if (isTimelineTracked) {
        Timeline.finishSync();
      }
    }
  }

__code_0表示新element的挂载, 从main开始到这里后, 方法中的newWidget实际就是最开始的被包装的Text, 所以这里实际调用的是StatelessElement.mount, 被包装的Text类型是StatelessWidget, 所以后面实际调用的代码如下:


// 当前this是 ComponentElement
 void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_lifecycleState == _ElementLifecycle.active);
    _firstBuild();          // 第1次build
    assert(_child != null);
  }
  
// build最后会调用到  ComponentElement.performRebuild


void performRebuild() {
    Widget? built;
    try {
      ..

        // 这里就是调用 最常见的 用户的 build方法, 返回Widget
      built = build();
      
    } catch (e, stack) {
      ...
    } finally {
     
           super.performRebuild(); // clears the "dirty" flag
    }
    try {
        // 拿到 built(Widget)后, 将它更新到 Element(_child)中
        // updateChild后面的过程就会又根据情况创建 built对应的Element对象
        /// 又调用mount, 然后就形成的递归
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      ...
    }
  }

至此main函数基本流程已经完毕, 总结一点就是:

  1. 最先调用用户的Widget的构造方法
  2. 创建根结点
  3. 由根结点(RenderObjectWidget)创建对应的RenderObjectElement
  4. 开始mount
  5. mount先形成树中的结点关系, 再更新子Element
  6. 更新子Element的过程, 会做各种优化, 如diff, 如要不要创建子element
  7. 若第6步创建了子element, 则又会调用对应的mount, 而若是用户的element(StatelessWidget等)就会调用到build方法
  8. 再根据第7步build得到的Widget, update子Element, 回到第6步形成树的递归调用


StatefulWidget

  • 如上面的分析, 当调用到mount时, 若当前的Element是StatefulElement, 则发生的动作有所不同
@override
  void _firstBuild() {
      ...

      // 生命周期函数 initState
    final Object? debugCheckForReturnedFuture = state.initState() as dynamic;


    ...

    // 生命周期函数 didChangeDependencies
    state.didChangeDependencies();

    ..
    super._firstBuild();
  }

很明显, 通过引用的State<StatefulWidget>调用对应的生命周期函数
通过父类的build再调用到State的build方法


StatefulWidget的注意点1

  • 在开发中, 很多种情况下都会使用StatefulWidget, 因为业务的问题在适当的时间点可能会更新状态.
  • StatefulElement中在进行到build时, 调用的是引用的state的build方法. 当用户调用刷新状态(setState)的方法时, state并不会重建, 重建的只是widget. 所以在某些场景下就要注意一些问题
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';

 void main(List<String> args) {
   return runApp(MaterialApp(home: Scaffold(body: HttpDemo1("init"),appBar: AppBar(title: Text("hello"),),),));
 }

 class HttpDemo1 extends StatefulWidget {
   String _data;
   HttpDemo1(String data):_data = data;

  @override
  State<HttpDemo1> createState() => _HttpDemo1State();
}

class _HttpDemo1State extends State<HttpDemo1> {
   final bgc = Colors.red;
   @override
  void initState() {
    super.initState();
    Timer(Duration(seconds: 5), () {
        setState(() {
          this.widget._data = "change";
        });
    });
  }
  @override
  Widget build(BuildContext context) {
    return Container(child: Text(this.widget._data),height: 50, width: double.infinity, decoration: BoxDecoration(color:  this.bgc),);
  }
}


class MYItem extends StatefulWidget {
   final String name;
  const MYItem({this.name = ""});

  @override
  State<MYItem> createState() => _MYItemState();
}

class _MYItemState extends State<MYItem> {
  @override
  Widget build(BuildContext context) {
    return Container(color: Color(Random().nextInt(0x100000000)), child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
  }
}

上述程序中, 过了5秒后, 调用刷新方法时, 修改了数据, 最后渲染时, state并未重建, 但container重建了, 但前后重建的背景颜色却始终是红色.
下面来看看setState后发生了什么


setState方法实现

  • setState提供了一个回调方法, 由Flutter内部调用
void setState(VoidCallback fn) {

    ... 其他不相关的代码

    final Object? result = fn() as dynamic;         // 回调用户的操作


    ... 其他不相关的代码

    _element!.markNeedsBuild();                     // 里面会标记 当前StatefulElement要刷新, 此时的element是 StatefulElement
  }


标记刷新的方法

// this是StatefulElement
void markNeedsBuild() {

    ... 不相关的代码

    if (dirty) {                // 若当前 已经标记了, 则直接返回
      return;
    }
    _dirty = true;              
    owner!.scheduleBuildFor(this);                  // 这里面有核心的一步, 标记 element.dirty = true;
                                                    // true的意义表示要build, 但不是当前立即build, 而是等待系统的drawFrame的回调()
                                                    // 并将自己添加到 buildOwner._dirtyElements的数组中, 这样只用遍历该数组中的记录去作对应的布局刷新
                                                    /// 效率就更高
}

这里只简单描述: 当调用setState后 1.Flutter标记elment.dirty = true; 2. 添加到buildOwner.dirtyElements数组中. 然后等待系统的drawFrame的回调, 后续调用到buildOwner.buildScope(_rootElement), 该函数内部取到buildOwenr.dirtyElements数组, 一个一个调用elment相关的build方法. 而对于StatefulElement来说, build意味着不会创建State


Element-Tree中的element

  • 前面说过, element树结构相对Widget-Tree要稳定, 所以从实现上来看树中的结点element

 void main(List<String> args) {
   return runApp(MaterialApp(home: Scaffold(body: HttpDemo2(),appBar: AppBar(title: Text("hello"),),),));
 }
 
 
 
 class HttpDemo2 extends StatefulWidget {
   const HttpDemo2({super.key});
 
   @override
   State<HttpDemo2> createState() => _HttpDemo2State();
 }
 
 class _HttpDemo2State extends State<HttpDemo2> {
   @override
   Widget build(BuildContext context) {
      return Column(
        children: [
          TestItem2(show_str: DateTime.timestamp().toString(),),
          TextButton(onPressed: (){
            setState(() {
              
            });
          }, child: Text("点我"))
        ],
      );
   }
 }
 
 
 class TestItem2 extends StatelessWidget {
   final String _show_str;

    TestItem2({show_str,super.key}): _show_str = show_str{
     print("construct:${this.runtimeType}--${this.hashCode}");
   }
 
   @override
   Widget build(BuildContext context) {
     print("widget-type:${context.widget.runtimeType}\t\twidget-code:${context.widget.hashCode}\t\tcontext-code${context.hashCode}");
     return Container(width: 300, height: 300, color: Colors.red, child: Text(this._show_str),);
   }
 }

测试结果

Performing hot restart...
Syncing files to device iPhone 14 Pro Max...
Restarted application in 293ms.
flutter: construct:TestItem2--247503628
flutter: widget-type:TestItem2      widget-code:247503628       context-code496134088
flutter: construct:TestItem2--313661942
flutter: widget-type:TestItem2      widget-code:313661942       context-code496134088
flutter: construct:TestItem2--760305741
flutter: widget-type:TestItem2      widget-code:760305741       context-code496134088
flutter: construct:TestItem2--114051490
flutter: widget-type:TestItem2      widget-code:114051490       context-code496134088
flutter: construct:TestItem2--1004922686
flutter: widget-type:TestItem2      widget-code:1004922686      context-code496134088
flutter: construct:TestItem2--925506391
flutter: widget-type:TestItem2      widget-code:925506391       context-code496134088

可以发现, 不管点击按钮更新多少次, Element-Tree中的一个element不会有变动, 但它所绑定的widget在不断的创建和销毁. 并且在调用build前就已经绑定了. 这一点在前面的源码探索中已经看过了


StatefulWidget注意点2

  • 在StatefulWidget中的State调用setState后, 对应的state在build时发现上下文没有变(StatelessElement), 那这个elment中的state属性改变了没有呢?

    前面可能没有细说到, StatefulWidget在创建element时,创建的类型是StatefulElement, 这个element在被创建时,会指定state引用到StatefulWidget中的state对象

  • 因为state

 void main(List<String> args) {
   return runApp(MaterialApp(home: Scaffold(body: HttpDemo1(),appBar: AppBar(title: Text("hello"),),),));
 }




 class HttpDemo1 extends StatefulWidget {
  @override
  State<HttpDemo1> createState() => _HttpDemo1State();
}

class _HttpDemo1State extends State<HttpDemo1> {

   final List datas = ["hello"];
   
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      ...this.datas.map((e){
        return MYItem(name: e);
      }),
        TextButton(onPressed: (){
          setState(() {
            this.datas[0] = DateTime.timestamp().toString();                // __code_0
          });
        }, child: Text("点我"))

    ],);
  }
}


class MYItem extends StatefulWidget {
   final String name;
  const MYItem({this.name = ""});

  @override
  State<MYItem> createState() => _MYItemState();
}

class _MYItemState extends State<MYItem> {
   final bgc = Color(Random().nextInt(0x100000000));

   @override
  void initState() {
    super.initState();
    print("initstate:${this.runtimeType}----${this.hashCode}");
  }

  @override
  Widget build(BuildContext context) {
    print("${context.runtimeType}---------${context.hashCode} --- ${(context as StatefulElement).state}");      // __code_1
    var result = Container(color: this.bgc, child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
    return result;
  }

上述程序其实很简单, 就是改变父State时(__code_0), 重新构建子StatefulWidget时, element下绑定的state有没有重建(__code_1). 测试结果如下:

flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a
flutter: StatefulElement---------647177149 --- _MYItemState#89d6a

说明setState后, 其子节点下的StateElemet并不会重新创建state, 从这一点来看, Flutter内部的确是在保持Element-Tree的稳定. 从视图层面来说, 就是在复用视图. 但在程序不断运行的过程中, StatefulWidget可能在不断的被重建, 所以Flutter必定会做 新Widget和旧Widget的对比, 然后对Element-Tree的节点做更新操作
这一点已经在结点更新的源码中跟踪了


复用的一个demo

import 'dart:math';
import 'package:flutter/material.dart';

 void main(List<String> args) {
   return runApp(MaterialApp(home: Scaffold(body: HttpDemo1(),appBar: AppBar(title: Text("hello"),),),));
 }




 class HttpDemo1 extends StatefulWidget {
  @override
  State<HttpDemo1> createState() => _HttpDemo1State();
}

class _HttpDemo1State extends State<HttpDemo1> {

   final List datas = ["hello", "world", "nice"];

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      ...this.datas.map((e){
        return MYItem(name: e);                                             // _code_key
      }),
        TextButton(onPressed: (){
          setState(() {
            this.datas.removeAt(0);
          });
        }, child: Text("点我"))

    ],);
  }
}


class MYItem extends StatefulWidget {
   final String name;
  const MYItem({this.name = "",super.key});

  @override
  State<MYItem> createState() => _MYItemState();
}

class _MYItemState extends State<MYItem> {
   final bgc = Color(Random().nextInt(0x100000000));

  @override
  Widget build(BuildContext context) {
    return Container(color: this.bgc, child: Text(this.widget.name), alignment: Alignment.center, width: double.infinity, height: 50,);
  }
}

该程序测试了视图复用的一个问题:
删除数组中的第0个元素后, 由于state未重新创建, 所以颜色是对不上的, 自己可以测试一下

    假设初始化的状态是:
        红 --> hello
        绿 --> world
        蓝 --> nice

    第1次点击按钮后, 结果是:
        红 --> world
        绿 --> nice

    第2次点击后:
        红 --> nice

    想要的效果是:
        第1次后:
            绿 --> world
            蓝 --> nice

        第2次后:
            蓝 --> nice

    
    原因:
        1. setState

        2. 标记 dirty = true, 并添加到 buildOwner.dirtyElements, 其实添加的是 StatefulElement

        3. 等待系统的回调, 然后到 buildOwner.buildScope(_rootElement)被回调

        4. 再通过 dirtyElements的数组取到  _HttpDemo1State对应的StatefulElement

        5. 调用 _HttpDemo1State._rebuild, 再调用到 build

        6. 获取到 build返回的新 Column的Widget, Column的children是2个 _MyItemState

        7. 然后调用 StatefulElement.updateChild方法, 其中this就是_HttpDemo1State对应的elment, 传递的参数是:
            > child --> 就是已经存在Element-Tree中的Column对应的elment
            > newWidget-> 第6步返回的 新 Column

        8. 在updateChild中:
            因为 child和newWidget的类型是对应的, 并且child.widget的key和newWidget的key也是一样的
            所以并不会创建新的 Column对应的element
            但会重新绑定 child的widget为newWidget
        
        9. 第8步会将 child的widget更新为 newWidget,
            >child.update(newWidget);
            
        10. 因为child对应的Element实际是Column所对应的Element, 即MultiChildRenderObjectWidget, 所以会调用到该类的实现中

        11. 第10步的实现中比较复杂, 但实现的结果就是上面演示的结果
                内部其实是在作 新旧比较, 从左到右遍历旧的, 然后比对新的

解决上面的方案是在创建MyItem时指定key, 这样在第11步比对过程中, 就不是单纯的从左到右, 还要看key是不是一样的, 若是一样的才会复用, 即将__code_key这一行改为

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

推荐阅读更多精彩内容