概述
- Flutter的渲染流程
- 对象的创造过程
- 小部件的键
一、Flutter的渲染流程
- 1.1、
Widget的Element和RenderObject
的关系
1.2、Widget(窗口小部件) 是什么?
- 官方对Widget的说明:
- Flutter的Widgets的灵感来自React,中心思想是构造你的UI使用这些Widgets。
- Widget使用配置和状态,描述这个View(界面)应该长什么样的子。
- 当一个小部件发生改变时,小部件就会重新构建它的描述,框架会和之前的描述进行对比,来决定使用最小的改变(最小变化)在渲染树中,从一个状态到另一个状态。
- 自己的理解:
- 口小部件就是一个个描述文件,这些描述文件在我们进行状态改变时会不断的构建。
- 但是对于渲染对象而言,只会使用最小的增量来更新渲染界面。
- 1.3、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 是真正渲染的对象:其中有
markNeedsLayout
、performLayout
、markNeedsPaint
、paint
等方法
三、小部件的键
在我们创造的小部件的时候,总是会看到一个关键的参数,它又是做什么的呢?
- 3.1、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就不合适了;我们可以创建出一个学生对象,使用对象来作为keyUniqueKey
唯一键:如果我们要确保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(); } }