Flutter中Widget的生命周期和渲染原理
- 原文博客地址: Flutter中Widget的生命周期和渲染原理
- 之前的
Flutter
系列文章中都有介绍一些常用的Widget
这里就主要了解Flutter
的渲染原理和Widget
的生命周期
Flutter
中Widget
的生命周期
StatelessWidget
是通过构造函数(Constructor
)接收父Widget
直接传入值,然后调用build
方法来构建,整个过程非常简单- 而
StatefulWidget
需要通过State
来管理其数据,并且还要监控状态的改变决定是否重新build
整个Widget
- 这里主要讨论
StatefulWidget
的生命周期,就是它从创建到显示再到更新最后到销毁的整个过程 StatefulWidget
本身由两个类组成的:StatefulWidget
和State
- 在
StatefulWidget
中的相关方法主要就是- 执行
StatefulWidget
的构造函数(Constructor
)来创建出StatefulWidget
- 执行
StatefulWidget
的createState
方法,来创建一个维护StatefulWidget
的State
对象 - 所以我们探讨
StatefulWidget
的生命周期, 最终是探讨State
的生命周期
- 执行
- 那么为什么
Flutter
在设计的时候,StatefulWidget
的build
方法要放在State
中而不是自身呢- 首先
build
出来的Widget
是需要依赖State
中的变量(数据/自定义的状态)的 Flutter
在运行过程中,Widget
是不断的创建和销毁的, 当我们自己的状态改变时, 我们只希望刷新当前Widget
, 并不希望创建新的State
- 首先
上面图片大概列出了StatefulWidget
的简单的函数调用过程
constructor
调用createState
创建State
对象时, 执行State
类的构造方法(Constructor
)来创建State
对象
initState
initState
是StatefulWidget
创建完后调用的第一个方法,而且只执行一次- 类似于
iOS
的viewDidLoad
,所以在这里View
并没有完成渲染 - 我们可以在这个方法中执行一些数据初始化的操作,或者发送网络请求
1 |
|
- 这个方法是重写父类的方法,必须调用
super
,因为父类中会进行一些其他操作 - 另一点在源码中, 会看到这个方法中有一个
mustCallSuper
的注解, 这里就限制了必须调用父类的方法
1 |
|
didChangeDependencies
didChangeDependencies
在整个过程中可能会被调用多次, 但是也只有下面两种情况下会被调用
- 在
StatefulWidget
第一次创建的时候didChangeDependencies
会被调用一次, 会在initState
方法之后会被立即调用 - 从其他对象中依赖一些数据发生改变时, 比如所依赖的
InheritedWidget
状态发生改变时, 也会被调用
build
build
同样也会被调用多次- 在上述
didChangeDependencies
方法被调用之后, 会重新调用build
方法, 来看一下我们当前需要重新渲染哪些Widget
- 当每次所依赖的状态发生改变的时候
build
就会被调用, 所以一般不要将比较好使的操作放在build
方法中执行
didUpdateWidget
执行didUpdateWidget
方法是在当父Widget
触发重建时,系统会调用didUpdateWidget
方法
dispose
- 当前的
Widget
不再使用时,会调用dispose
进行销毁 - 这时候就可以在
dispose
里做一些取消监听、动画的操作 - 到这里, 也就意味着整个生命周期的过程也就结束了
setState
setState
方法可以修改在State
中定义的变量- 当我们手动调用
setState
方法,会根据最新的状态(数据)来重新调用build
方法,构建对应的Widgets
setState
内部其实是通过调用_element.markNeedsBuild();
实现更新Widget
整个过程的代码如下:
1 | class HomeScreen extends StatefulWidget { |
打印结果如下:
1 | flutter: 1. 调用HomeScreen---constructor |
Flutter渲染原理
在Flutter
中渲染过程是通过Widget
, Element
和RenderObject
实现的, 下面是FLutter
中的三种树结构
Widget
这是Flutter
官网对Widget
的说明
Flutter widgets are built using a modern framework that takes inspiration from React. The central idea is that you build your UI out of widgets. Widgets describe what their view should look like given their current configuration and state. When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.
Flutter
的Widgets
的灵感来自React
,中心思想是使用这些Widgets
来搭建自己的UI界面- 通过当前
Widgets
的配置和状态描述这个页面应该展示成什么样子 - 当一个
Widget
发生改变时,Widget
就会重新build
它的描述,框架会和之前的描述进行对比,来决定使用最小的改变在渲染树中,从一个状态到另一个状态 - 从这段说明中大概意思也就是
Widgets
只是页面描述层面的, 并不涉及渲染层面的东西, 而且如果所依赖的配置和状态发生变化的时候, 该Widgets
会重新build
- 而对于渲染对象来说, 只会使用最小的开销重新渲染发生改变的部分而不是全部重新渲染
Widget Tree
树结构- 在整个
Flutter
项目结构也是由很多个Widget
构成的, 本质上就是一个Widget Tree
- 在上面的类似
Widget Tree
结构中, 很可能会有大量的Widget
在树结构中存在引用关系, 而且每个Widget
所依赖的配置和状态发生改变的时候,Widget
都会重新build
,Widget
会被不断的销毁和重建,那么意味着这棵树非常不稳定 - 所以
Flutter Engin
也不可能直接把Widget
渲染到界面上, 这事极其损耗性能的, 所以在渲染层面Flutter
引用了另外一个树结构RenderObject Tree
- 在整个
RenderObject
下面是Flutter
官网对RenderObject
的说明
An object in the render tree.
The RenderObject class hierarchy is the core of the rendering library’s reason for being.
RenderObjects have a parent, and have a slot called parentData in which the parent RenderObject can store child-specific data, for example, the child position. The RenderObject class also implements the basic layout and paint protocols.
- 每一个
RenderObject
都是渲染树上的一个对象 RenderObject
层是渲染库的核心, 最终Flutter Engin
是把RenderObject
真正渲染到界面上的RenderObject Tree
- 在渲染过程中, 最终都会把
Widget
转成RenderObject
,Flutter
最后在解析的时候解析的也是我们的RenderObject Tree
, 但是并不是每一个Widget
都会有一个与之对应的RenderObject
- 因为很多的Widget都不是壳渲染的Widget, 而是类似于一个盒子的东西, 对其他Widget进行包装的作用
- 在渲染过程中, 最终都会把
Element
下面是Flutter
官网对Element
的说明
An instantiation of a Widget at a particular location in the tree.
Widgets describe how to configure a subtree but the same widget can be used to configure multiple subtrees simultaneously because widgets are immutable. An Element represents the use of a widget to configure a specific location in the tree. Over time, the widget associated with a given element can change, for example, if the parent widget rebuilds and creates a new widget for this location.
Elements form a tree. Most elements have a unique child, but some widgets (e.g., subclasses of RenderObjectElement) can have multiple children.
Element
是Widget
在树中具有特定位置的是实例化Widget
描述如何配置子树和当前页面的展示样式, 每一个Element
代表了在Element Tree
中的特定位置- 如果
Widget
所依赖的配置和状态发生改变的时候, 和Element
关联的Widget
是会发生改变的, 但是Element
的特定位置是不会发生改变的 Element Tree
中的每一个Element
是和Widget Tree
中的每一个Widget
一一对应的Element Tree
类似于HTML
中的虚拟DOM
, 用于判断和决定哪些RenderObject
是需要更新的- 当
Widget Tree
所依赖的状态发生改变(更新或者重新创建Widget
)的时候,Element
根据拿到之前所保存的旧的Widget
和新的Widget
做一个对比, 判断两者的Key
和类型是否是相同的, 相同的就不需要重新创建, 有需要的话, 只需要更新对应的属性即可
对象的创建过程
Widget
- 在
Flutter
中Widget
有可渲染的和不可渲染的(组件Widget
)- 组件
Widget
: 类似Container
….等等 - 可渲染
Widget
: 类似Padding
…..等等
- 组件
- 下面我们先看一下组件
Widget
(Container
)的实现过程和继承关系
1 | // 继承关系Container --> StatelessWidget --> Widget |
- 从上面的代码可以看到, 继承关系比较简单, 并没有创建
RenderObject
对象 - 我们经常使用
StatelessWidget
和StatefulWidget
,这种Widget
只是将其他的Widget
在build
方法中组装起来,并不是一个真正可以渲染的Widget
RenderObject
这里来看一下可渲染Widget
的继承关系和相关源代码, 这里以Padding
为例
1 | // 继承关系: Padding --> SingleChildRenderObjectWidget --> RenderObjectWidget --> Widget |
- 在
Padding
的类中,我们找不到任何和渲染相关的代码,这是因为Padding仅仅作为一个配置信息,这个配置信息会随着我们设置的属性不同,频繁的销毁和创建 - 所以真正的渲染相关的代码那就只能在
RenderObject
里面了 - 上面代码中, 在
Padding
类里面有一个核心方法createRenderObject
是用于创建一个RenderObject
的 - 而且方法
createRenderObject
是来源于RenderObjectWidget
这个抽象类里面的一个抽象方法 - 抽象方法是必须被子类实现的,但是它的子类
SingleChildRenderObjectWidget
也是一个抽象类,所以可以不实现父类的抽象方法 - 但是
Padding
不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现
1 | // 这里目的是为了创建一个RenderPadding |
上面这段代码中, 最终是创建了一个RenderPadding
, 而这个RenderPadding
又是什么呢? 下面看看他的继承关系和相关源代码
1 | // 继承关系: RenderPadding --> RenderShiftedBox --> RenderBox --> RenderObject |
RenderObject
又是如何实现布局和渲染的呢
1 | // 当外面修改padding时 |
Element
- 在上面介绍
Widget
中提到过我们写的大量的Widget
在树结构中存在引用关系,但是Widget
会被不断的销毁和重建,那么意味着这棵树非常不稳定 - 如果
Widget
所依赖的配置和状态发生改变的时候, 和Element
关联的Widget
是会发生改变的, 但是Element
的特定位置是不会发生改变的 Element
是Widget
在树中具有特定位置的是实例化, 是维系整个Flutter
应用程序的树形结构的稳定- 接下来看下
Element
是如何被创建和引用的, 这里还是以Container
和Padding
为例
1 | // 在Container的父类StatelessWidget中, 实例化了其父类的一个抽象方法 |
- 在每一次创建
Widget
的时候,会创建一个对应的Element
,然后将该元素插入树中 - 上面代码
SingleChildRenderObjectWidget
实例化了父类的抽象方法createElement
创建一个Element
, 并把当前Widget(this)
作为SingleChildRenderObjectElement
构造方法的参数传入 - 这也就意味着创建出来的
Element
保存了对当前Widget
的引用 - 在创建完一个
Element
之后,Framework
会调用mount
方法来将Element
插入到树中具体的位置 - 这是在
Element
类中的mount
方法, 这里主要的作用就是把自己做一个挂载操作
1 | /// Add this element to the tree in the given slot of the given parent. |
StatelessElement
Container
创建出来的是StatelessElement
, 下面我们探索一下StatelessElement
创建完成后, framework
调用mount
方法的过程, 这里只留下了相关核心代码
1 | abstract class ComponentElement extends Element { |
上面的代码看着有点乱, 下面就理一下
- 这里我们创建的是
StatelessElement
, 在创建完一个Element
之后,Framework
会调用mount
方法 - 在
ComponentElement
类中重写了mount
方法, 所以framwork
会调用这里的mount
方法 - 在
mount
方法中直接调用的_firstBuild
方法(第一次构建) - 在
_firstBuild
方法又是直接调用的rebuild
方法(重新构建) - 然而在
ComponentElement
类中没有重写rebuild
方法, 所以还是要调用父类的rebuild
方法 - 在
rebuild
方法会调用performRebuild
方法, 而且是调用ComponentElement
内重写的performRebuild
方法 - 在
performRebuild
方法内, 会调用build
方法, 并用Widget
类型的build
接收返回值 - 而这个
build
方法在StatelessElement
中的实现如下 - 也就是说, 在创建
Element
之后, 创建出来的elment
会拿到传过来的widget
, 然后调用widget
自己的build
方法, 这也就是为什么所有的Widget
创建出来之后都会调用build
方法的原因
1 | Widget build() => widget.build(this); |
所以在
StatelessElement
调用mount
烦恼歌发最主要的作用就是挂在之后调用_firstBuild
方法, 最终通过widget
调用对应widget
的build
方法构建更多的东西
RenderObjectElement
- 下面看一下可渲染的
Widget
又是如何创建Element
的, 这里还是以Padding
为例 - 之前有提到
Padding
是继承自SingleChildRenderObjectWidget
的, 而createElement
方法也是在这个类中被实现的
1 | abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { |
- 上面的代码中
Padding
是通过父类创建了一个SingleChildRenderObjectElement
对象 SingleChildRenderObjectElement
是继承自RenderObjectElement
RenderObjectElement
继承自Element
- 接下来就是看一下
mount
方法的调用过程
1 | /// 以下源码并不全, 这里只是拷贝了一些核心方法和相关源码 |
- 从上面的代码看
SingleChildRenderObjectElement
类中的mount
方法核心是调用父类(RenderObjectElement
)的mount
方法 - 而
RenderObjectElement
中的mount
方法, 主要就是通过widget
调用它的createRenderObject
方法创建一个renderObject
- 所以对于
RenderObjectElement
来说,fromework
调用mount
方法, 其目的就是为了创建renderObject
- 这也就意味着
Element
对_renderObject
也会有一个引用 - 也就是说
Element
不但对_widget
有一个引用, 对_renderObject
也会有一个引用
StatefulElement
- 上面提到
StatefulWidget
是由两部分构成的StatefulWidget
的State
- 而
StatefulWidget
是通过createState
方法,来创建一个维护StatefulWidget
的State
对象
1 | class StatefulElement extends ComponentElement { |
- 在
StatefulElement
内定义了一个_state
变量, 并且存在对_widget
的引用 - 而在
StatefulElement
的构造方法中, 直接通过参数widget
调用其内部的createState
方法, 这个是StatefulWidget
中的一个抽象方法(子类必须实现), 相信这个方法都比较熟悉
1 | class HomeScreen extends StatefulWidget { |
StatefulElement
创建完成之后,fromework
就会调用mount
方法挂载, 这个过程就和上面StatelessElement
中的mount
方法的调用过程基本一样了- 两者不同的是:
StatelessElement
中最后是通过widget
调用widget.build(this)
方法StatefulElement
中最后是通过_state
调用_state.build(this)
方法, 也就是上面_HomeScreenState
的build
方法
1 |
|
BuildContext
上面多次提到的build
方法是有参数的, 而且不管是StatelessWidget
还是State
, 他们build
方法的参数都是BuildContext
1 | // StatelessWidget |
在ComponentElement
创建完成之后, 会调用mount
方法, 最终都会调用对应的build
方法
1 | class StatelessElement extends ComponentElement { |
- 上面的
build
方法传入的参数都是Element
, 所以本质上BuildContext
就是当前的Element
BuildContext
主要的作用就是知道我当前构建的这个Widget
在这个Element Tree
上面的位置信息, 之后就可以沿着这这个Tree
喜爱那个上查找相关的信息- 下面是两者的继承关系
1 | abstract class Element extends DiagnosticableTree implements BuildContext {} |
小总结
StatelessElement
- 在
Widget
创建出来之后,Flutter
框架一定会根据这个Widget
创建出一个对应的Element
, 每一个Widget
都有一个与之对应的Element
Element
对对当前Widget
产生一个引用_widget
element
创建完成后,fromework
会调用mount
方法, 最终调用_widget.build(this)
方法
StatefulElement
- 在
Widget
创建出来之后,Flutter
框架一定会根据这个Widget
创建出一个对应的Element
, 每一个Widget
都有一个与之对应的Element
- 在
StatefulElement
构造函数中会调用widget.createState()
创建一个_state
, 并引用_state
- 并且会把
widget
赋值给_state
的一个引用_widget
:_state._widget = widget;
, 这样在State
类中就可以通过this.state
拿到当前的Widget
element
创建完成后,fromework
会调用mount
方法, 最终调用_state.build(this)
方法
RenderObjectElement
- 在
Widget
创建出来之后,Flutter
框架一定会根据这个Widget
创建出一个对应的Element
, 每一个Widget
都有一个与之对应的Element
element
创建完成后,fromework
会调用mount
方法, 在mount
方法中会通过widget
调用widget.createRenderObject(this)
创建一个renderObject
, 并赋值给_renderObject
- 所以创建的
RenderObjectElement
对象也会对RenderObject
产生一个引用
Widget的key
我们之前创建的每一个Widget
, 在其构造方法中我们都会看到一个参数Key
, name这个Key
到底有何作用又何时使用呢
1 | const Scaffold({ Key key, ... }) |
我们先看一个示例需求代码如下: 希望每次点击删除按钮删除数组的元素后, ListView
中其他item
的展示信息不变(包括颜色和字体)
1 | class _HomeScreenState extends State<HomeScreen> { |
我们吧ListView
的item
分别使用StatelessWidget
和StatefulWidget
实现, 看看两者区别
StatelessWidget
我们先对ListItem
使用一个StatelessWidget
进行实现:
1 | class ListItemLess extends StatelessWidget { |
- 通过实践很明显, 每次删除第一个元素后, 虽然也能删除第一个
ListItem
, 剩余的每一个ListItem
展示的信息也是对的, 但是他们的颜色却是每次都会发生变化 - 这主要就是因为, 每次删除之后都会调用
setState
,也就会重新build
,重新build出来的新的
StatelessWidget`会重新生成一个新的随机颜色
StatefulWidget
现在对ListItem
使用StatefulWidget
实现同样的功能
1 | class ListItemFul extends StatefulWidget { |
- 我们发现一个很奇怪的现象, 信息展示正常(删除了第一条数据),但是从颜色上看, 是删除了最后一条
- 在我们每次调用
setState
的时候,Widget
都会调用一个canUpdate
函数判断是否需要重建element
1 | static bool canUpdate(Widget oldWidget, Widget newWidget) { |
- 在删除第一条数据的时候,
Widget
对应的Element
并没有改变 - 而目前是没有设置
Key
的, 所以Element
中对应的State
引用也没有发生改变 - 在更新
Widget
的时候,Widget
使用了没有改变的Element
中的State
, 也就是之前创建的三个element
中的前两个 - 这也就是为什么删除之后, 从颜色上看, 删除的是最后一条
添加Key
在上面ListItemFul
的基础上, 为每一个ListItemFul
加上一个key
1 | class ListItemFulKey extends StatefulWidget { |
- 最终这就是我们想要实现的效果了
- 上述代码中, 为每一个
ListItemFulKey
添加了一个key
值, 而且每一个的Key
值都是不一样的 - 在删除一个元素调用
setState
方法后, 会重新build
的一个Widget Tree
Element
会拿到新的Widget Tree
和原来保存的旧的Widget Tree
做一个diff
算法- 根据
runtimeType
和key
进行比对, 和新的Widget Tree
相同的会被继续复用, 否则就会调用unnmount
方法删除
Key的分类
Key
本身是一个抽象,不过它也有一个工厂构造器,创建出来一个ValueKey
- 直接子类主要有:
LocalKey
和GlobalKey
LocalKey
,它应用于具有相同父Element
的Widget
进行比较,也是diff
算法的核心所在;GlobalKey
,通常我们会使用GlobalKey
某个Widget
对应的Widget
或State
或Element
1 |
|
LocalKey
LocalKey
有三个子类
ValueKey
:
ValueKey
是当我们以特定的值作为key
时使用,比如一个字符串、数字等等
ObjectKey
:
- 如果两个学生,他们的名字一样,使用
name
作为他们的key
就不合适了 - 我们可以创建出一个学生对象,使用对象来作为
key
UniqueKey
:
- 如果我们要确保
key
的唯一性,可以使用UniqueKey
1 | class ValueKey<T> extends LocalKey { |
GlobalKey
GlobalKey
可以帮助我们访问某个Widget
的信息,包括Widget
或State
或Element
等对象, 有点类似于React
中的ref
- 比如我们想在
HomePage
中访问HomeContenet
中的widget
1 | class HomePage extends StatelessWidget { |