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

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

推荐阅读更多精彩内容