美文网首页
Flutter入门07 -- 渲染原理与Key的使用

Flutter入门07 -- 渲染原理与Key的使用

作者: zyc_在路上 | 来源:发表于2022-01-21 16:45 被阅读0次

    Flutter的渲染流程

    Widget
    • 在Flutter中万物皆为Widget,构成一棵Widget树,Widget可以理解为 UI界面的状态描述文件,这些描述文件在我们进行状态改变时会不断的重新build,也就是说Widget树的状态是十分不稳定的,当其状态发生变化时,就需要重新Build,那么Flutter渲染引擎渲染Widget树是非常损耗性能的;
    Element
    • Element是Widget的实例,是构成Element树的元素,Element相当于虚拟的DOM,Widge描述和配置子树的样子,而Element实际去配置在Element树中特定的位置。Element最大的意义在于以最小的开销来更新RenderObject;
    RenderObject
    • 是渲染树上的对象,是渲染库的核心;
    • 主要负责布局与绘制,同时也是构成渲染树的元素;

    下面给一张图,详细描述了Widget,Element与RenderObject之间的关系:

    image.png
    第一点:PaddingRowTextTextField的继承链路
    • 从图中看出PaddingRowTextTextField四种组件的继承关系,最终都是继承自Widget,所以说在Flutter中万物皆为Widget;
    • Padding -> singleChildRenderObjectWidget -> RenderObjectWidget -> Widget
    • Row -> Flex -> singleChildRenderObjectWidget -> RenderObjectWidget -> Widget
    • Text -> statelessWidget -> Widget
    • TextField -> statefulWidget -> Widget
    • PaddingRow属于一类,都是继承自RenderObjectWidget属于渲染对象组件
    • TextTextField属于一类,没有继承自RenderObjectWidget
    • Widget类中有Element createElement()抽象方法,其作用是为当前Widget创建一个Element对象,所有继承自Widget类的组件,都可实现这个方法,为自己创建一个Element对象,也就是说一个Widget对象必定会有一个对应的Element对象;
    • 针对Padding组件,是在其父类SingleChildRenderObjectWidget中调用createElement()方法,返回一个singleChildRenderObjectElement对象;
    第二点:PaddingRowTextTextField调用createElement()返回Element对象
    • 针对Padding组件,是在其父类SingleChildRenderObjectWidget中调用createElement()方法,返回一个singleChildRenderObjectElement对象;
    • 针对Row组件,是在其父类multiChildRenderObjectWidget中调用createElement()方法,返回一个mutilChildRenderObjectElement对象;
    • 针对Text组件,是在其父类statelessWidget中调用createElement()方法,返回一个statelessElement对象;
    • 针对TextField组件,是在其父类statefulWidget中调用createElement()方法,返回一个statefulElement对象;
    • 这四种组件调用createElement()方法,创建的Element对象类型均不同,最终都是继承自Element,其继承关系如下:
    • PaddingsingleChildRenderObjectElement -> RenderObjectElement -> Element
    • RowmutilChildRenderObjectElement -> RenderObjectElement -> Element
    • TextstatelessElement -> ComponmentElement -> Element
    • TextFieldstatefulElement -> ComponmentElement -> Element
    第三点:Element调用mount方法
    • 当Widget的Element创建完成时,系统会自动调用Element的mount方法;
    • 针对Padding,创建的singleChildRenderObjectElement,调用其mount方法,实现如下:
    @override
      void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _child = updateChild(_child, widget.child, null);
      }
    
    • 内部调用其父类RenderObjectElementmount方法,实现如下:
     @override
      void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        assert(() {
          _debugDoingBuild = true;
          return true;
        }());
        _renderObject = widget.createRenderObject(this);
        assert(() {
          _debugDoingBuild = false;
          return true;
        }());
        assert(() {
          _debugUpdateRenderObjectOwner();
          return true;
        }());
        assert(_slot == newSlot);
        attachRenderObject(newSlot);
        _dirty = false;
      }
    
    • 其中_renderObject = widget.createRenderObject(this)即根据Element对象来创建渲染对象,看到这里就明确了Widget -> Element -> RenderObject之间关系了;

    • 针对RowPadding类似,最终也是调用父类RenderObjectElementmount方法,创建对应的渲染对象;

    • 针对TextTextField,创建的Element对象statelessElementstatefulElement对象,最终都是在其父类ComponmentElement中调用mount方法,实现如下:

    @override
      void mount(Element parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        assert(_child == null);
        assert(_active);
        _firstBuild();
        assert(_child != null);
      }
    
    • 内部调用_firstBuild()方法,实现如下:
    void _firstBuild() {
        rebuild();
      }
    
    • 内部调用rebuild()方法,实现如下:
    void rebuild() {
        assert(_debugLifecycleState != _ElementLifecycle.initial);
        if (!_active || !_dirty)
          return;
        assert(() {
          if (debugOnRebuildDirtyWidget != null) {
            debugOnRebuildDirtyWidget(this, _debugBuiltOnce);
          }
          if (debugPrintRebuildDirtyWidgets) {
            if (!_debugBuiltOnce) {
              debugPrint('Building $this');
              _debugBuiltOnce = true;
            } else {
              debugPrint('Rebuilding $this');
            }
          }
          return true;
        }());
        assert(_debugLifecycleState == _ElementLifecycle.active);
        assert(owner._debugStateLocked);
        Element debugPreviousBuildTarget;
        assert(() {
          debugPreviousBuildTarget = owner._debugCurrentBuildTarget;
          owner._debugCurrentBuildTarget = this;
          return true;
        }());
        performRebuild();
        assert(() {
          assert(owner._debugCurrentBuildTarget == this);
          owner._debugCurrentBuildTarget = debugPreviousBuildTarget;
          return true;
        }());
        assert(!_dirty);
      }
    
    • 内部的核心方法调用为performRebuild(),此方法是一个抽象方法,选中Command+Alt+B查看实现类有ComponmentElement,实现如下:
    @override
      void performRebuild() {
        if (!kReleaseMode && debugProfileBuildsEnabled)
          Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);
    
        assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
        Widget built;
        try {
          assert(() {
            _debugDoingBuild = true;
            return true;
          }());
          built = build();
          assert(() {
            _debugDoingBuild = false;
            return true;
          }());
          debugWidgetBuilderValue(widget, built);
        } catch (e, stack) {
          _debugDoingBuild = false;
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $this'),
              e,
              stack,
              informationCollector: () sync* {
                yield DiagnosticsDebugCreator(DebugCreator(this));
              },
            ),
          );
        } finally {
          // We delay marking the element as clean until after calling build() so
          // that attempts to markNeedsBuild() during build() will be ignored.
          _dirty = false;
          assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
        }
        try {
          _child = updateChild(_child, built, slot);
          assert(_child != null);
        } catch (e, stack) {
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $this'),
              e,
              stack,
              informationCollector: () sync* {
                yield DiagnosticsDebugCreator(DebugCreator(this));
              },
            ),
          );
          _child = updateChild(null, built, slot);
        }
    
        if (!kReleaseMode && debugProfileBuildsEnabled)
          Timeline.finishSync();
      }
    
    • 各种断言与逻辑判断,其核心调用为built = build()build()是ComponmentElement类的一个抽象方法,其子类有statelessElementstatefulElement,其实现分别如下:
    @override
      Widget build() => widget.build(this);
    
    @override
      Widget build() => _state.build(this);
    
    • 可以看出无状态的statelessWidget的build方法的调用流程:Widget创建完Element -> 调用Element的mount方法 -> _firstBuild() - rebuild() -> performRebuild() -> build() -> widget.build(this)
    • 有状态的statefulWidget的build方法的调用流程:Widget创建完Element -> 调用Element的mount方法 -> _firstBuild() - rebuild() -> performRebuild() -> build() -> _state.build(this)
    • 这也就解释了有状态的statefulWidget的build方法是在State中的;
    • widget.build(this)与_state.build(this)中的参数this,就是Element,也就是说Widget build(BuildContext context),中的BuildContext本质就是Element
    第四点:statefulWidget的底层探索
    • statefulWidget创建的Element为StatefulElement,其构造方法如下:
    StatefulElement(StatefulWidget widget)
          : _state = widget.createState(),
            super(widget) {
        assert(() {
          if (!_state._debugTypesAreRight(widget)) {
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary('StatefulWidget.createState must return a subtype of State<${widget.runtimeType}>'),
              ErrorDescription(
                'The createState function for ${widget.runtimeType} returned a state '
                'of type ${_state.runtimeType}, which is not a subtype of '
                'State<${widget.runtimeType}>, violating the contract for createState.'
              ),
            ]);
          }
          return true;
        }());
        assert(_state._element == null);
        _state._element = this;
        assert(
          _state._widget == null,
          'The createState function for $widget returned an old or invalid state '
          'instance: ${_state._widget}, which is not null, violating the contract '
          'for createState.',
        );
        _state._widget = widget;
        assert(_state._debugLifecycleState == _StateLifecycle.created);
      }
    
    • 可以看到statefulWidget的State是在statefulElement的构造函数中创建的;
    • _state._widget = widget,将Widget绑定给State,这也就解释了在State中可以访问到Widget对象;
    第五点:Element的源码探索
    • Element类的构造方法如下:
    Element(Widget widget)
        : assert(widget != null),
          _widget = widget;
    
    • 可以看出Element引用了widget
    • RenderObjectElement继承自Element,其内部有访问renderObject渲染对象的setter与getter方法,如下所示:
    @override
      RenderObject get renderObject => _renderObject;
      RenderObject _renderObject;
    
    • 可以看出Element引用了renderObject
    • 如果是statefulElement,还可以引用State
    总结:
    • Widget树是配置信息,状态不断变化中,不稳定;
      • 内部调用createElement方法,创建与之对应的Element;
    • Element树是Widget的实例,真正保存Widget结构数据的对象;
      • Element创建完成之后,由系统的framework调用mount方法,继承自RenderObjectWidget会调用createRenderObject方法,创建渲染对象,继承自statelessWidgetstatefulWidget不会调用createRenderObject方法,最终会调用widget的build方法与state的build方法;
      • Element对widget与RenderObject以及state都有引用;
    • RenderObject渲染树是真正的渲染对象;
      • 内部包含渲染对象的布局与绘制操作;

    Widget的属性 Key的应用

    • Widget的构造方法会传入一个可选参数Key,如下:
    const Widget({ this.key });
    
    • 可选参数Key的作用是什么,现在我们通过案例来探讨一下:
    案例代码一 -- StatelessWidget
    import 'dart:math';
    import 'package:flutter/material.dart';
    
    void main() => runApp(SFMyApp());
    
    class SFMyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: SFHomePage());
      }
    }
    
    class SFHomePage extends StatefulWidget {
      @override
      _SFHomePageState createState() => _SFHomePageState();
    }
    
    class _SFHomePageState extends State<SFHomePage> {
      final List<String> names = ["1111", "2222", "3333"];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("基础widget")),
          body: ListView(
              children: names.map((name) {
                return ListItemLess(name);
              }).toList()),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.delete),
            onPressed: () {
              setState(() {
                names.removeAt(0);
              });
            },
          ),
        );
      }
    }
    
    class ListItemLess extends StatelessWidget {
      final String 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(
          child: Text(name,style: TextStyle(color: Colors.white,fontSize: 25)),
          height: 80,
          color: randomColor,
        );
      }
    }
    
    • 效果图如下所示:
    image.png
    • 当每次点击右下角的按钮时,都会删除第一条数据;
    • 现象:每删除一个,剩余的ListItemLess的颜色都会发生变化;
    • 原因:删除之后会调用setState方法,会重新build,重新build出来的新的ListItemLess会重新生成一个新的随机颜色;
    案例代码二 -- StatefulWidget
    • 将上面的ListItemLess改成ListItemful继承自StatefulWidget,代码如下:
    import 'dart:math';
    import 'package:flutter/material.dart';
    
    void main() => runApp(SFMyApp());
    
    class SFMyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: SFHomePage());
      }
    }
    
    class SFHomePage extends StatefulWidget {
      @override
      _SFHomePageState createState() => _SFHomePageState();
    }
    
    class _SFHomePageState extends State<SFHomePage> {
      final List<String> names = ["1111", "2222", "3333"];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("基础widget")),
          body: ListView(
              children: names.map((name) {
                return ListItemful(name);
              }).toList()),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.delete),
            onPressed: () {
              setState(() {
                names.removeAt(0);
              });
            },
          ),
        );
      }
    }
    
    class ListItemful extends StatefulWidget {
      final String name;
      ListItemful(this.name);
    
      @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(
          child: Text(widget.name),
          height: 80,
          color: randomColor,
        );
      }
    }
    
    • 现象:每删除一个,都是最后一个被删除,剩余的颜色不会变化;
    • 删除之前的Widget Tree 与 Element Tree如下所示:
    Snip20211029_70.png
    • 删除第一个青色widget,整个widget树会重建,而对应的Element树不会重建,Flutter SDK会根据新建widget树上对应位置的新widget与Element树中Element保存引用的旧widget,进行比对,决定当前Element是更新还是重建,判断方法为canUpdate
    static bool canUpdate(Widget oldWidget, Widget newWidget) {
        return oldWidget.runtimeType == newWidget.runtimeType
            && oldWidget.key == newWidget.key;
      }
    
    • 当新建的widget与Element引用的旧widget的runtimeTypekey值都相同时,那么当前的Element只需要更新数据,不需要重建,可提升性能;
    • 删除第一个青色widget之后,widget树与element树如下:
    image.png
    • 然后进行新旧widget的比对,将widget与Element树中Element进行遍历比较:
      • fulWidget2与element青引用的widget进行比较,发现类型与key(没有设置key)是相同的,那么element青(原来引用fulWidget1)会保留并更新引用fulWidget2,
      • fulWidget3与element粉引用的widget进行比较,发现类型与key(没有设置key)是相同的,那么element粉(原来引用fulWidget2)保留并更新引用fulWidget3
      • element绿在widget树中没有对应的widget了,直接删除;
    案例代码三 -- StatefulWidget传可选参数key
    import 'dart:math';
    import 'package:flutter/material.dart';
    
    void main() => runApp(SFMyApp());
    
    class SFMyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: SFHomePage());
      }
    }
    
    class SFHomePage extends StatefulWidget {
      @override
      _SFHomePageState createState() => _SFHomePageState();
    }
    
    class _SFHomePageState extends State<SFHomePage> {
      final List<String> names = ["1111", "2222", "3333"];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("基础widget")),
          body: ListView(
              children: names.map((name) {
                return ListItemful(name,key: ValueKey(name));
              }).toList()),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.delete),
            onPressed: () {
              setState(() {
                names.removeAt(0);
              });
            },
          ),
        );
      }
    }
    
    class ListItemful extends StatefulWidget {
      final String name;
    
      // ListItemful(this.name);
      ListItemful(this.name,{Key key}) : super(key: key);
    
      @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(
          child: Text(widget.name),
          height: 80,
          color: randomColor,
        );
      }
    }
    
    • 创建ListItemful传入可选参数ValueKey(name),即key为数据内容,现在再删除item,正常删除,不会出现颜色变化与删除最后一条;
    • 判断Element更新/重建的逻辑与上面的相同,只不过现在是每一个widget都绑定一个key,重新build之后widget树中的每个 单个widget与Element树中element所引用的widget进行比对,若存在相同的runtimeType,key,那么这个element就可以重用,无需销毁重建;
    image.png
    • 重建后的widget树,由于key是数据内容,所以在element树中element引用的widget的key都能匹配到,
    案例代码四 -- StatefulWidget传可选参数key -- UniqueKey()
    • UniqueKey是独一无二的,也就是说每调用一次都会生成一个不带重复的key;
    • 根据上面的原理,ListItemful创建若传入UniqueKey,那么重建后的widget的key肯定在element树中element引用的widget的key都不能匹配到,所以整个Element树都会重建,所以每删除一次,所有item的颜色都会发生变化;
    widget可选参数Key的分类
    • Key是一个抽象类,其有一个工厂构造器,子类有:
      • LocalKey:应用于具有相同父Element的widget进行比较,是diff算法的核心所在;
      • GlobalKey:通常使用于某个widget,然后访问其widget本身与state的;
    • LocalKey有三个子类:
      • ValueKey:我们以特定的值作Widget的key,比如字符串,数字等;
      • ObjectKey:以模型对象作为Widget的key;
      • UniqueKey:可确保key为唯一的;
    GlobalKey
    • 先上案例代码,如下所示:
    import 'package:flutter/material.dart';
    
    void main() => runApp(SFMyApp());
    
    class SFMyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: SFHomePage());
      }
    }
    
    class SFHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text("基础widget")),
            body: SFHomeContent(key: homeKey),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.gesture),
            onPressed: (){
              
            },
          ),
        );
      }
    }
    
    class SFHomeContent extends StatefulWidget {
      final String name = "2222";
    
      @override
      _SFHomeContentState createState() => _SFHomeContentState();
    }
    
    class _SFHomeContentState extends State<SFHomeContent> {
      final String message = "1111";
    
      @override
      Widget build(BuildContext context) {
        return Text(message);
      }
    
      void test(){
        print("_SFHomeContentState test");
      }
    }
    
    • 现在要实现,在点击按钮的时候 能访问SFHomeContent的name属性,能访问_SFHomeContentState的message属性,以及调用test方法,可通过GlobalKey来实现;
    • GlobalKey类的定义是:abstract class GlobalKey<T extends State<StatefulWidget>> extends Key,易知GlobalKey是一个抽象类,从泛型可以看出其本质是一个State,
    • 通过创建一个GlobalKey<_SFHomeContentState>,然后传参给SFHomeContent,修改后的代码如下:
    import 'package:flutter/material.dart';
    
    void main() => runApp(SFMyApp());
    
    class SFMyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(home: SFHomePage());
      }
    }
    
    class SFHomePage extends StatelessWidget {
    
      final GlobalKey<_SFHomeContentState> homeKey = GlobalKey();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text("基础widget")),
            body: SFHomeContent(key: homeKey),
          floatingActionButton: FloatingActionButton(
            child: Icon(Icons.gesture),
            onPressed: (){
              print(homeKey.currentState.message);
              print(homeKey.currentState.widget.name);
              homeKey.currentState.test();
            },
          ),
        );
      }
    }
    
    class SFHomeContent extends StatefulWidget {
      final String name = "2222";
    
      SFHomeContent({Key key}) : super(key: key);
    
      @override
      _SFHomeContentState createState() => _SFHomeContentState();
    }
    
    class _SFHomeContentState extends State<SFHomeContent> {
      final String message = "1111";
    
      @override
      Widget build(BuildContext context) {
        return Text(message);
      }
    
      void test(){
        print("_SFHomeContentState test");
      }
    }
    

    相关文章

      网友评论

          本文标题:Flutter入门07 -- 渲染原理与Key的使用

          本文链接:https://www.haomeiwen.com/subject/uxlehrtx.html