一.Flutter 的简介
Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android等多个平台。 Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 扩展。下面我们整体介绍一下 Flutter 技术的主要特点。
1. 跨平台自绘引擎
Flutter 与用于构建移动应用程序的其他大多数框架不同,因为 Flutter 既不使用 WebView,也不使用操作系统的原生控件。 相反,Flutter 使用自己的高性能渲染引擎来绘制 Widget(组件)。这样不仅可以保证在 Android 和iOS 上 UI 的一致性,也可以避免对原生控件依赖而带来的限制及高昂的维护成本。
Flutter 底层使用 Skia 作为其 2D 渲染引擎,Skia 是 Google的一个 2D 图形处理函数库,包含字型、坐标转换,以及点阵图,它们都有高效能且简洁的表现。Skia 是跨平台的,并提供了非常友好的 API,目前 Google Chrome浏览器和 Android 均采用 Skia 作为其 2D 绘图引擎。
目前 Flutter 已经支持 iOS、Android、Web、Windows、macOS、Linux、Fuchsia(Google新的自研操作系统)等众多平台,但本书的示例和介绍主要是基于 iOS 和 Android 平台的,其他平台读者可以自行了解。
2. 高性能
Flutter 高性能主要靠两点来保证:
第一:Flutter APP 采用 Dart 语言开发。Dart 在 JIT(即时编译)模式下,执行速度与 JavaScript 基本持平。但是 Dart 支持 AOT,当以 AOT模式运行时,JavaScript 便远远追不上了。执行速度的提升对高帧率下的视图数据计算很有帮助。
第二:Flutter 使用自己的渲染引擎来绘制 UI ,布局数据等由 Dart 语言直接控制,所以在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以 JavaScript 需要和 Native 之间不停的同步布局信息,这和在浏览器中JavaScript 频繁操作 DOM 所带来的问题是类似的,都会导致比较可观的性能开销。
3. 采用Dart语言开发
这个是一个很有意思但也很有争议的问题,在了解 Flutter 为什么选择了 Dart 而不是 JavaScript 之前我们先来介绍一下之前提到过的两个概念:JIT 和 AOT。
程序主要有两种运行方式:静态编译与动态解释。静态编译的程序在执行前程序会被提前编译为机器码(或中间字节码),通常将这种类型称为AOT (Ahead of time)即 “提前编译”。而解释执行则是在运行时将源码实时翻译为机器码来执行,通常将这种类型称为JIT(Just-in-time)即“即时编译”。
AOT 程序的典型代表是用 C/C++ 开发的应用,它们必须在执行前编译成机器码;而JIT的代表则非常多,如JavaScript、python等,事实上,所有脚本语言都支持 JIT 模式。但需要注意的是 JIT 和 AOT 指的是程序运行方式,和编程语言并非强关联的,有些语言既可以以 JIT 方式运行也可以以 AOT 方式运行,如Python,它可以在第一次执行时编译成中间字节码,然后在之后执行时再将字节码实施转为机器码执行。也许有人会说,中间字节码并非机器码,在程序执行时仍然需要动态将字节码转为机器码,这不应该是 JIT 吗 ? 是这样,但通常我们区分是否为AOT 的标准就是看代码在执行之前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT。在此,读者不必纠结于概念,概念就是为了传达精神而发明的,只要读者能够理解其原理即可,得其神忘其形。
现在我们看看 Flutter 为什么选择 Dart 语言?笔者根据官方解释以及自己对 Flutter 的理解总结了以下几条(由于其他跨平台框架都将 JavaScript 作为其开发语言,所以主要将 Dart 和 JavaScript 做一个对比):
1.开发效率高
Dart 运行时和编译器支持 Flutter 的两个关键特性的组合:
基于 JIT 的快速开发周期:Flutter 在开发阶段采用,采用 JIT 模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;
基于 AOT 的发布包: Flutter 在发布时可以通过 AOT 生成高效的机器码以保证应用性能。而 JavaScript 则不具有这个能力。
2.高性能
Flutter 旨在提供流畅、高保真的的 UI 体验。为了实现这一点,Flutter 中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现会丢帧的周期性暂停,而 Dart 支持 AOT,在这一点上可以做的比 JavaScript 更好。
3.快速内存分配
Flutter 框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。因此,拥有一个能够有效地处理琐碎任务的内存分配器将显得十分重要,在缺乏此功能的语言中,Flutter 将无法有效地工作。当然 Chrome V8 的 JavaScript 引擎在内存分配上也已经做的很好,事实上 Dart 开发团队的很多成员都是来自Chrome 团队的,所以在内存分配上 Dart 并不能作为超越 JavaScript 的优势,而对于Flutter来说,它需要这样的特性,而 Dart 也正好满足而已。
4.类型安全和空安全
由于 Dart 是类型安全的语言,且 2.12 版本后也支持了空安全特性,所以 Dart 支持静态类型检测,可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript 是一个弱类型语言,也因此前端社区出现了很多给 JavaScript 代码添加静态类型检测的扩展语言和工具,如:微软的 TypeScript 以及Facebook 的 Flow。相比之下,Dart 本身就支持静态类型,这是它的一个重要优势。
5.flutter开发团队与Dart社区密切合作
看似不起眼,实则举足轻重。由于有 Dart 团队的积极投入,Flutter 团队可以获得更多、更方便的支持,正如Flutter 官网所述我们正与 Dart 社区进行密切合作,以改进 Dart 在 Flutter 中的使用。例如,当我们最初采用 Dart 时,该语言并没有提供生成原生二进制文件的工具链(这对于实现可预测的高性能具有很大的帮助),但是现在它实现了,因为 Dart 团队专门为 Flutter 构建了它。同样,Dart VM 之前已经针对吞吐量进行了优化,但团队现在正在优化 VM 的延迟时间,这对于 Flutter 的工作负载更为重要。
二.Flutter框架结构
Flutter框架是一个分层的结构,每个层都建立在前一层之上。
简单来讲,Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层,下面我们分别介绍:
1. 框架层
Flutter Framework,即框架层。这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
底下两层(Foundation 和 Animation、Painting、Gestures)在 Google 的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是 Flutter Engine 暴露的底层UI库,提供动画、手势及绘制能力。
Rendering 层,即渲染层,这一层是一个抽象的布局层,它依赖于 Dart UI 层,渲染层会构建一棵由可渲染对象的组成的渲染树,当动态更新这些对象时,渲染树会找出变化的部分,然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分,它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制(调用底层 dart:ui )。
Widgets 层是 Flutter 提供的的一套基础组件库,在基础组件库之上,Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库,它们分别实现了 Material 和 iOS 设计规范。
Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camera(opens new window)和 webview(opens new window),以及和平台无关的功能,例如 animations(opens new window)。
我们进行Flutter 开发时,大多数时候都是和 Flutter Framework 打交道。
2. 引擎层
Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。
Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。其已作为Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows, macOS, iOS,Android,Ubuntu等。
Dart 部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的话,还包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)编译成了原生的arm代码,并不存在JIT部分。
Text 即文本渲染,其渲染层次如下:衍生自 Minikin的libtxt库(用于字体选择,分隔行);HartBuzz用于字形选择和成型;Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体。
3. 嵌入层
Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。
二.Flutter 的安装和使用
1.安装流程
2.集成
将 Flutter module 集成到 Android 项目
四.平台相关及Stateful 组件的生命周期
1.Widget说明
在 Flutter 中一切皆 组件,而组件又分为 StatefulWidget(有状态) 和StatelessWidget(无状态)组件 ,他们之间的区别是 StatelessWidget 组件发生变化时必须重新创建新的实例,而 StatefulWidget 组件则可以直接改变当前组件的状态而无需重新创建新的实例。
生命周期一:createState
classStatefulWidgetDemoextendsStatefulWidget{@override_StatefulWidgetDemoStatecreateState()=>_StatefulWidgetDemoState();}class_StatefulWidgetDemoStateextendsState{@overrideWidgetbuild(BuildContext context){returnContainer(); }}
当 StatefulWidget 组件插入到组件树中时 createState 函数由 Framework 调用,此函数在树中给定的位置为此组件创建 State
createState 函数执行完毕后表示当前组件已经在组件树中,此时有一个非常重要的属性 mounted 被 Framework 设置为 true。
生命周期二:initState
initState 函数在组件被插入树中时被 Framework 调用(在 createState 之后),此函数只会被调用一次,子类通常会重写此方法,在其中进行初始化操作,比如加载网络数据,重写此方法时一定要调用 super.initState(),如下:
@override
void initState() {
super.initState();
//初始化...
}
如果此组件需要订阅通知,比如 ChangeNotifier 或者 Stream,则需要在不同的生命周期内正确处理订阅和取消订阅通知。
在 initState 中订阅通知。
在 didUpdateWidget 中,如果需要替换旧组件,则在旧对象中取消订阅,并在新对象中订阅通知。
并在 dispose 中取消订阅。
生命周期三:didChangeDependencies
didChangeDependencies 方法在 initState 之后由 Framework 立即调用。另外,当此 State 对象的依赖项更改时被调用,比如其所依赖的 InheritedWidget 发生变化时, Framework 会调用此方法通知组件发生变化。
didChangeDependencies 方法调用后,组件的状态变为 dirty,立即调用 build 方法。
生命周期四:build
此方法是我们最熟悉的,在方法中创建各种组件,绘制到屏幕上。 Framework会在多种情况下调用此方法:
调用 initState 方法后。
调用 didUpdateWidget 方法后。
收到对 setState 的调用后。
此 State 对象的依存关系发生更改后(例如,依赖的 InheritedWidget 发生了更改)。
调用 deactivate 之后,然后将 State 对象重新插入树的另一个位置。
此方法可以在每一帧中调用,此方法中应该只包含构建组件的代码,不应该包含其他额外的功能,尤其是耗时任务。
(InheritedWidget是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式)
生命周期五:didUpdateWidget
当组件的 configuration 发生变化时调用此函数,当父组件使用相同的 runtimeType 和 Widget.key 重新构建一个新的组件时,Framework 将更新此 State 对象的组件属性以引用新的组件,然后使用先前的组件作为参数调用此方法。
生命周期六:deactivate
当框架从树中移除此 State 对象时将会调用此方法,在某些情况下,框架将重新插入 State 对象到树的其他位置(例如,如果包含该树的子树 State 对象从树中的一个位置移植到另一位置),框架将会调用 build 方法来提供 State 对象适应其在树中的新位置。
生命周期七:dispose
当框架从树中永久移除此 State 对象时将会调用此方法,与 deactivate 的区别是,deactivate 还可以重新插入到树中,而 dispose 表示此 State 对象永远不会在 build。调用完 dispose后,mounted 属性被设置为 false,也代表组件生命周期的结束,此时再调用 setState 方法将会抛出异常。
子类重写此方法,释放相关资源,比如动画等。
一、生命周期阶段
flutter生命周期大体上可以分为三个阶段:初始化、状态变化、销毁。
1、初始化阶段
对应执行构造方法和initState时候
2、状态变化阶段
开新的widget或者调用setState方法的时候
3、销毁阶段
deactivate 和 dispose
二、生命周期阶段执行的函数
1、initState
调用次数:1次
插入渲染树时调用,只调用一次,widget创建执行的第一个方法,这里可以做一些初始化工作,比如初始化State的变量。
2、didChangeDependencies
调用次数:多次
初始化时,在initState()之后立刻调用
当依赖的InheritedWidget rebuild,会触发此接口被调用
实测在组件可见状态变化的时候会调用
3、build
调用次数:多次
初始化之后开始绘制界面
setState触发的时候会
4、didUpdateWidget
调用次数:多次
组件状态改变时候调用
5、deactivate
当State对象从树中被移除时,会调用此回调,会在dispose之前调用。
页面销毁的时候会依次执行:deactivate > dispose
6、dispose
调用次数:1次
当State对象从树中被永久移除时调用;通常在此回调中释放资源。
7、reassemble
在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用
三、App生命周期
通过WidgetsBindingObserver的didChangeAppLifecycleState 来获取。通过该接口可以获取是生命周期在AppLifecycleState类中。
class_MyHomePageStateextendsState<MyHomePage>withWidgetsBindingObserver{// initState 里添加监听@overridevoidinitState(){super.initState();WidgetsBinding.instance.addObserver(this);}//dispose 里移除监听@overridevoiddispose(){// TODO: implement disposesuper.dispose();WidgetsBinding.instance.removeObserver(this);}//重写didChangeAppLifecycleState@overridevoiddidChangeAppLifecycleState(AppLifecycleState state){super.didChangeAppLifecycleState(state);switch(state){caseAppLifecycleState.resumed:print(AppLifecycleState.resumed);break;caseAppLifecycleState.inactive:print(AppLifecycleState.inactive);break;caseAppLifecycleState.paused:print(AppLifecycleState.paused);break;caseAppLifecycleState.suspending:print(AppLifecycleState.suspending);break;}}}
下面是生命周期:
初次打开widget时,不执行AppLifecycleState的回调;
从后台到前台:AppLifecycleState inactive--->ApplifecycleState resumed
back键退出应用: AppLifecycleState inactive--->AppLifecycleState pause
五.Flutter渲染之Widget、Element 和 RenderObject
提出问题
createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?
build 方法是在什么时候调用的?
BuildContext 是什么?
Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
要理解Flutter的渲染原理,那么就必须了解Widget、RenderObject 和 Element及其作用。总的来说,Flutter调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树,如下图所示。
从上面的介绍中,我们隐约知道了Widget、RenderObject 和 Element的作用,简单的介绍一下。 - Widget:Widget 的主要作用是用来保存 Element 信息的(包括布局、渲染属性、事件响应等信息),本身是不可变的,Element 也是根据 Widget 里面保存的配置信息来管理渲染树,以及决定自身是否需要执行渲染。 - RenderObject:RenderObject 做为渲染树中的对象存在,主要作用是处理布局、绘制相关的事情,而绘制的内容是Widget传入的内容。 - Element:Element 可以理解为是其关联的 Widget 的实例,存放视图构建的上下文数据,可以通过遍历Element来查看视图树,Element同时持有Widget和RenderObject对象。
Flutter通过Widget树中的每个控件创建不同类型的渲染对象,组成渲染对象树,而渲染对象树在Flutter中的展示分为四阶段:布局、绘制、合成及渲染。其中,布局和绘制由RenderObject负责完成,Flutter采用深度优先机制遍历渲染树对象,确定树中每个对象的位置和尺寸,并把他们绘制到不同的图层上,而合成及渲染则交给Skia完成。
三棵树
首先先了解三棵树,这是我们的核心,需要首先建立一个概念。
Widget 树
我们平时用 Widget 使用声明式的形式写出来的界面,可以理解为 Widget 树,这是要介绍的第一棵树。
RenderObject 树
Flutter 引擎需要把我们写的 Widget 树的信息都渲染到界面上,这样人眼才能看到,跟渲染有关的当然有一颗渲染树 RenderObject tree,这是第二颗树,渲染树节点叫做 RenderObject,这个节点里面处理布局、绘制相关的事情。这两个树的节点并不是一一对应的关系,有些 Widget是要显示的,有些 Widget ,比如那些继承自 StatelessWidget & StatefulWidget 的 Widget 只是将其他 Widget 做一个组合,这些 Widget 本身并不需要显示,因此在 RenderObject 树上并没有相对应的节点。
Element 树
Widget 树是非常不稳定的,动不动就执行 build方法,一旦调用 build 方法意味着这个 Widget 依赖的所有其他 Widget 都会重新创建,如果 Flutter 直接解析 Widget树,将其转化为 RenderObject 树来直接进行渲染,那么将会是一个非常消耗性能的过程,那对应的肯定有一个东西来消化这些变化中的不便,来做cache。因此,这里就有另外一棵树 Element 树。Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。
这三棵树如下图所示,是我们讨论的核心内容。
从上图可以看出,widget 树和 Element 树节点是一一对应关系,每一个 Widget 都会有其对应的 Element,但是 RenderObject 树则不然,只有需要渲染的 Widget 才会有对应的节点。Element 树相当于一个中间层,大管家,它对 Widget 和 RenderObject 都有引用。当 Widget 不断变化的时候,将新 Widget 拿到 Element 来进行对比,看一下和之前保留的 Widget 类型和 Key 是否相同,如果都一样,那完全没有必要重新创建 Element 和 RenderObject,只需要更新里面的一些属性即可,这样可以以最小的开销更新 RenderObject,引擎在解析 RenderObject 的时候,发现只有属性修改了,那么也可以以最小的开销来做渲染。
以上只是引出了非常重要的三棵树和他们之间的关系,简而言之,Widget 树就是配置信息的树,我们平时写代码写的就是这棵树,RenderObject 树是渲染树,负责计算布局,绘制,Flutter 引擎就是根据这棵树来进行渲染的,Element 树作为中间者,管理着将 Widget 生成 RenderObject和一些更新操作。
从源码来了解 Widget、Element 和 RenderObject
Widget
下面对 Widget 的概述截图来自官网
Widget 是描述 Element 的配置信息,是 Flutter 框架里的核心类层次结构,一个 Widget 是用户界面某一部分的不可变描述。Widgets 可以转为 Elements,Elements 管理着底层的渲染树。
有这么多 Widget,我们来简单分各类吧,前面已经提到 Widget 有可渲染和不可渲染的分别了。可渲染里面分为 child 或 children,在不可渲染的 Widgets 里面又分为有状态和无状态,也就是 StatefullWidget 和 StatelessWidget。我们选择四个典型的Widgets来看看吧,如 Padding、RichText、Container、TextField。通过查阅源码,我们看到这几个类的继承关系如下图所示。
来到Widget 类里面可以看到有以下方法
@protectedElementcreateElement();
Widget 是个抽象类,所有的 Widgets 都是它的子类,其抽象方法 createElement 需要子类实现,这里体现了之前我们说的 Widget 和 Element 的一一对应关系。来到 StatelessWidget、StatefulWidget、MultiChildRenderObjectWidget、SingleChildRenderObjectWidget 里面我们可以找到 createElement 的实现。
SingleChildRenderObjectWidget
@overrideSingleChildRenderObjectElementcreateElement()=>SingleChildRenderObjectElement(this);
MultiChildRenderObjectWidget
@overrideMultiChildRenderObjectElementcreateElement()=>MultiChildRenderObjectElement(this);
StatefulWidget
@overrideStatefulElementcreateElement()=>StatefulElement(this);
StatelessWidget
@overrideStatelessElementcreateElement()=>StatelessElement(this);
可以发现规律,创建 Element 都会传入 this,也就是当前 Widget,然后返回对应的 Element,这些 Element 都是继承自 Element,Element 会有引用指向当前 Widget。
我们继续来到 RichText 和 Padding 类定义里面,他们都是继承自 RenderObjectWidget,可以看到他们都有 createRenderObject 方法,如下
Padding
@overrideRenderPaddingcreateRenderObject(BuildContextcontext){returnRenderPadding(padding:padding,textDirection:Directionality.of(context),);}
RichText
@overrideRenderParagraphcreateRenderObject(BuildContextcontext){assert(textDirection!=null||debugCheckHasDirectionality(context));returnRenderParagraph(text,textAlign:textAlign,textDirection:textDirection??Directionality.of(context),softWrap:softWrap,overflow:overflow,textScaleFactor:textScaleFactor,maxLines:maxLines,strutStyle:strutStyle,textWidthBasis:textWidthBasis,locale:locale??Localizations.localeOf(context,nullOk:true),);}
RenderPadding 和 RenderParagraph 最终都是继承自 RenderObject。通过以上源码分析,我们可以看出来 Widget 里面有生成 Element 和 RenderObject 的方法,所以我们平时只需要埋头写好 Widget 就行,Flutter 框架会帮我们生成对应的 Element 和 RenderObject。
Element
以下对 Element 描述来自官网
直接翻译过来就是,Element 是 树中特定位置 Widget 的一个实例化对象。这句话有两层意思:1. 表示 Widget 是一个配置,Element 才是最终的对象;2. Element 是通过遍历 Widget 树时,调用 Widget 的方法创建的。Element 承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
上面从源码里面介绍 Widget 都会生成对应的 Element,这里我们也对 Element 简单做一个分类,和 Widget 相对应,如下图所示。
首先还是进入 Element 类里面看看,这是个抽象类,可以看到一些关键的方法和属性。
/// Typically called by an override of [Widget.createElement].Element(Widget widget):assert(widget!=null),_widget=widget;
上面介绍Widget 里面 createElement 方法的时候可以看到会传入 this,这里从 Element 的构造方法中可以看到,this 最后传给了 Element 里面的 _widget。也就是说每个 Element 里面都会有一个 Widget 的引用。_widget 在 Element 里面定义如下
/// The configuration for this element.@overrideWidgetgetwidget=>_widget;Widget _widget;
从源码里面知道 Element 里面的 widget 是一个 get 方法,直接返回 _widget。从上面的注释信息也再一次提到 Widget 和 Element 的关系,Widget 是 Element 的配置。
对于 Element 的构造方法,StatelessfulElement 有一些特殊的地方,如下
classStatefulElementextendsComponentElement{/// Creates an element that uses the given widget as its configuration.StatefulElement(StatefulWidgetwidget):_state=widget.createState(),super(widget){...省略断言...assert(_state._element==null);_state._element=this;...省略断言..._state._widget=widget;assert(_state._debugLifecycleState==_StateLifecycle.created);}/// The [State] instance associated with this location in the tree.////// There is a one-to-one relationship between [State] objects and the/// [StatefulElement] objects that hold them. The [State] objects are created/// by [StatefulElement] in [mount].State<StatefulWidget>get state=>_state;State<StatefulWidget>_state;}
StatefulElement 的构造方法中还调用了对应 Widget 的 createState 方法,并赋值给 _state,这也解答了我们在文章开头提出的问题(createState 方法在什么时候调用?)。StatefulElement 里面不仅有对 Widget 的引用,也有对 StatefulWidget 的 State 的引用。并且在构造函数里面还将 widget 赋值给了 _state 里面的 _widget。所以我们在 State 里面可以直接使用 widget 就可以拿到 State 对应的 Widget。原来是在 StatefulElement 构造函数的时候赋值的。解释了开头提到的问题(state 里面为啥可以直接获取到 widget 对象?)。
Element 还有一个关键的方法 mount,如下
@mustCallSupervoidmount(Element parent,dynamicnewSlot){...省略断言..._parent=parent;_slot=newSlot;_depth=_parent!=null?_parent.depth+1:1;_active=true;if(parent!=null)// Only assign ownership if the parent is non-null_owner=parent.owner;if(widget.keyisGlobalKey){finalGlobalKey key=widget.key;key._register(this);}_updateInheritance();...省略断言...}
Flutter 框架会根据 Widget 创建对应的 Element,Element 生成以后会调用 Element 的 mount 方法,将生成的 Element 挂载到 Element 树上。这里的 createElement 和 mount 都是 Flutter 框架自动调用的,不需要开发者手动调用。
这里一步一步看源码,发现执行链路如下:
_firstBuild()【ComponentElement】 -> rebuild() 【Element】-> performRebuild()【ComponentElement】 -> build()【StatelessElement】
StatelessElement build() 的源码
@overrideStatelessWidgetgetwidget=>super.widget;@overrideWidgetbuild()=>widget.build(this);
StatefulElement 的 build() 的源码如下
@overrideWidgetbuild()=>state.build(this);
可以看出ComponentElement 的 mount 最后执行的是 build 方法。不过 StatelessElement 和 StatefulElement 是有区别的,StatelessElement 执行的是 Widget 里的 build 方法,而 StatefulElement 里面执行的是 state 的 build 方法。因此,这里也解决了文章开始提到的一个问题(build 方法是在什么时候调用的?)。也知道了 StatefulWidget 和 它的 State 是如何联系起来的。
另外,我们看到上面执行执行build 方法传递的参数 this,也就是当前 Element,而我们在写代码的时候 build 方法是这样的
@overrideWidgetbuild(BuildContextcontext){}
因此我们知道了,这个 BuildContext 其实就是这个 Widget 所对应的 Element。看看 Element 的定义就更清楚了。这也解释了开始提到的问题(BuildContext 是什么?)。
abstractclassElementextendsDiagnosticableTreeimplementsBuildContext{}
再来看看 RenderObjectElement 里的 mount 方法
@overridevoidmount(Elementparent,dynamicnewSlot){...省略断言..._renderObject=widget.createRenderObject(this);...省略断言...attachRenderObject(newSlot);_dirty=false;}
对比一下 ComponentElement 和 RenderObjectElement 里面的 mount 方法,前面介绍过,ComponentElement 是非渲染 Widget 对应的 Element,而 RenderObjectElement 是渲染 Widget 对应的 Element,前者的mount 方法主要是负责执行 build 方法,而后者的 mount 方法主要是调用 Widget 里面的 createRenderObject 方法生成 RenderObject,然后赋值给自己的 _renderObject。
因此可以总结,ComponentElement 的 mount 方法主要作用是执行 build,而 RenderObjectElement 的 mount 方法主要作用是生成 RenderObject。
Widget 类里面有一个很重要的静态方法,本来可以放到上面讲 Widget 的时候说,但是还是放到 Element 里面吧。就是这个
/// Whether the `newWidget` can be used to update an [Element] that currently/// has the `oldWidget` as its configuration.////// An element that uses a given widget as its configuration can be updated to/// use another widget as its configuration if, and only if, the two widgets/// have [runtimeType] and [key] properties that are [operator==].////// If the widgets have no key (their key is null), then they are considered a/// match if they have the same type, even if their children are completely/// different.staticboolcanUpdate(Widget oldWidget,Widget newWidget){returnoldWidget.runtimeType==newWidget.runtimeType&&oldWidget.key==newWidget.key;}
Element 里面有一个 _widget 作为其配置信息,当widget变化或重新生成以后,Element 要不要销毁重建呢,还是直接将新生成的 Widget 替换旧的 Widget。答案就是通过这个方法判断的,上面的注释可以翻译如下
判断新 Widget 是否可以用来取代 Element 当前的配置信息 _widget。
Element 使用特定的 widget 作为其配置信息,如果 runtimeType 和 key 和之前的 widget 相同,那么可以使用一个新的 widget 更新 Element 里面旧的 widget。
如果这两个widget 都没有赋值 key,那么只要 runtimeType 相同也可以更新,即使这两个 widget 的孩子 widget 都完全不一样。
因此可以看出,即使外面的 widget 树经常变换重建,我们的 Element 可以维持相对稳定,不会重复创建,当然也就不会重复 mount, 生成 RenderObject,只需要以最小代价更新相关属性即可,最大可能减小了性能消耗。Widget 本身只是一些配置信息,简单的对象,它的变更重建不直接影响渲染,对性能影响很小。这就解决了上文提到的另外一个问题(Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?)。
RenderObject
从 RenderObject 的名字,我们就能很直观地知道,RendreObject 是主要负责实现视图渲染的对象。从上文中我们知道了一下几点
RenderObject 和 widget 并不是一一对应的,只有继承自 RenderObjectWidget 的 widget 才有对应的 RenderObject;
生成 RenderObject 的方法 createRenderObject 是在 Widget 里面定义的;
在 RenderObjectElement 执行 mount 方法的时候调用的 widget 里面的 createRenderObject 方法的;
RenderObjectElement 里面既有对 Widget 的引用也有对 RenderObject 的引用,它作为中间者,管理着双方。
RenderObject 在 Flutter 的展示分为四个阶段,即布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制在不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。
总结
上面通过源码讲解了一下 Widget、Element、RenderObject 的联系。下面简单来个总结。
我们写好 Widget 树后,Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时 Element 里面也有了对 Widget 的引用。特别的是当 StatefulElement 创建的时候也执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget。Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,而对于渲染的 RenderObjectElement 来说 mount 里面会调用 widget 里面的 createRenderObject 方法 生成 RenderObject,并赋值给 RenderObjectElement 里的相应属性。StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。
如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发该 Element 树的更新,通过 canUpdate 方法来判断是否可以使用新的 Widget 来更新 Element 里面的配置,还是重新生成 Element。并使用最新的 Widget 数据更新自身以及关联的 RenderObject对象。布局和绘制完成后,接下来的事情交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。
回答开头提出的问题
createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?
答:Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget,state 里面有个 widget 的get 方法可以获取到 _widget 对象。
build 方法是在什么时候调用的?
答:Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是常见的 BuildContext
BuildContext 是什么?
答:StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。简而言之 BuidContext 就是 Element。
Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
答:不会影响性能,widget 只是简单的配置信息,并不直接涉及布局渲染相关。Element 层通过判断新旧 widget 的runtimeType 和 key 是否相同决定是否可以直接更新之前的配置信息,也就是替换之前的 widget,而不必每次都重新创建新的 Element。