本文梳理了从 Widget 基本构建流程、平台差异、渲染流程和机制、布局和算法的优化等内容
一、Flutter 架构概览
| 层级/组件 | 描述 | 涉及技术/语言 |
|---|---|---|
| 底层操作系统 | - Flutter应用与底层OS交互的接口 | - |
| 嵌入层 | 提供程序入口,协调与OS的服务,管理事件循环队列 | |
| - Flutter代码可以集成到现有应用或作为主体 | - | |
| Flutter引擎 | - Flutter核心,使用C++编写 | C++ |
| 功能 | - 栅格化场景,提供核心API的底层实现(当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化) | |
| - 图形(在 iOS 和 Android 上通过 Impeller,在其他平台上通过 Skia),文本布局,文件及网络IO,辅助功能,插件架构 | ||
| - Dart运行环境及编译环境的工具链 | ||
| dart:ui | - 引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层 | |
| Flutter框架层 | - 开发者交互层,现代响应式框架,使用Dart编写 | Dart |
| foundational | - 基础类及构建块服务,如animation、painting、gestures | |
| 渲染层 | - 提供操作布局的抽象,构建渲染对象树 | |
| widget层 | - 组合抽象,每个渲染对象对应widgets层的一个类,响应式编程模型 | |
| Material和Cupertino库 | - 提供Material和iOS设计规范的widgets组合 | |
| 附加软件包 | - 更高层级功能,拆分为不同软件包 | Dart, Flutter核心库,平台插件,与平台无关的功能,生态系统中的软件包。【其中包括平台插件,例如 camera 和 webview;与平台无关的功能,例如 characters、 http 和 animations。还有一些软件包来自于更为宽泛的生态系统中,例如 应用内支付、 Apple 认证 和 Lottie 动画。】 |
通过 flutter create 命令创建的应用的结构概览:
| 组件 | 描述 | 备注 |
|---|---|---|
| Dart 应用 | ||
| widget 合成 | 将 widget 合成预期的 UI | 由应用开发者进行管理 |
| 业务实现 | 实现对应的业务逻辑 | 由应用开发者进行管理 |
| 框架(源代码) | ||
| API 封装 | 提供上层的 API 封装,用于构建高质量的应用(例如 widget、触摸检测、手势竞技、无障碍和文字输入) | |
| Scene 构建 | 将应用的 widget 树构建至一个 Scene 中 | |
| 引擎(源代码) | ||
| Scene 栅格化 | 将已经合成的 Scene 进行栅格化 | |
| 核心 API 封装 | 对 Flutter 的核心 API 进行了底层封装(例如图形图像、文本布局和 Dart 的运行时) | |
| dart:ui API | 将其功能通过 dart:ui API 暴露给框架 | |
| 嵌入层 API | 使用嵌入层 API 与平台进行整合 | |
| 嵌入层(源代码) | ||
| 操作系统服务协调 | 协调底层操作系统的服务,例如渲染层、无障碍和输入 | |
| 事件循环管理 | 管理事件循环体系 | |
| 平台 API 暴露 | 将特定平台的 API 暴露给应用集成嵌入层 | |
| 运行器 | ||
| 应用包合成 | 将嵌入层暴露的平台 API 合成为目标平台可以运行的应用包 | 部分内容由 flutter create 生成,由应用开发者进行管理 |
二、VM(程序虚拟机)
Flutter应用会在一个VM(程序虚拟机)中运行,这一机制是Flutter开发框架的一个重要组成部分。以下是对这一现象的详细解释:
Flutter与VM的关系
Flutter是一款移动应用程序跨平台框架,它允许开发者使用Dart语言编写代码,然后生成高性能、高保真的iOS和Android应用程序。在这个过程中,Flutter应用会在一个虚拟机(VM)中运行。这个VM提供了有状态的热重新加载功能,使得开发者可以在不完全重新编译应用的情况下,快速预览代码更改的效果。
Dart VM的特性
- 语言支持:Dart语言同时支持AOT(Ahead-Of-Time,提前编译)和JIT(Just-In-Time,即时编译)两种运行方式。在开发阶段,JIT模式使得热刷新成为可能,大大提高了开发效率。
- 性能优化:尽管在开发阶段使用JIT模式,但在发布阶段,Flutter应用会被编译为机器代码,无论是Intel x64还是ARM指令集,以确保最佳性能。
- 跨平台能力:Dart VM的跨平台特性使得Flutter应用能够在多种操作系统和设备上运行,而无需针对每个平台进行单独的编译。
Flutter VM的工作原理
- 代码执行:在Flutter VM中,Dart代码被编译并执行。VM负责处理代码的执行、内存管理、垃圾回收等底层任务。
- 热刷新:在开发过程中,当开发者对代码进行修改后,Flutter VM能够捕获这些更改,并快速地将它们应用到正在运行的应用中,而无需重新启动应用或完全重新编译。
- 状态管理:Flutter应用中的状态管理是通过Widget树和各自的状态来实现的。当某个Widget的状态发生变化时,Flutter框架会负责对比前后状态差异,并以最小代价来更新渲染结果。
Flutter VM的优势
- 提高开发效率:热刷新功能使得开发者可以即时看到代码更改的效果,大大缩短了开发周期。
- 保证应用性能:尽管在开发阶段使用JIT模式,但在发布阶段,Flutter应用会被编译为高效的机器代码,确保最佳性能。
- 跨平台一致性:Flutter VM的跨平台特性使得开发者能够编写一次代码,即可在多种设备和操作系统上运行应用,而无需针对每个平台进行单独的适配。
Flutter应用在一个VM中运行是其开发框架的一个重要组成部分。这个VM提供了强大的语言支持、性能优化和跨平台能力,使得Flutter应用能够在多种设备和操作系统上高效、一致地运行。同时,VM中的热刷新功能也大大提高了开发效率。
三、Widgets
1、widget
在 Flutter 里,widgets(类似于 React 中的组件)是用来配置对象树的不可变类,每个 widget 都是一部分不可变的 UI 声明。
这些 widgets 会管理单独的布局对象树,接着参与管理合成的布局对象树。
Flutter 的核心就是一套高效的遍历树的变动的机制,它会将对象树转换为更底层的对象树,并在树与树之间传递更改。
Flutter 拥有其自己的 UI 控制实现,而不是由系统自带的方法进行托管:
- 提供了无限的扩展性。当开发者想要一个 Switch 的改装时,他们可以以任意方式创建一个,而不被系统提供的扩展所限制。
- Flutter 可以直接合成所有的场景,而无需在 Flutter 与原生平台之间来回切换,从而避免了明显的性能瓶颈。
- 将应用的行为与操作系统的依赖解耦。在任意一种系统平台上体验应用,都将是一致的,就算某个系统更改了其控件的实现,也是如此。
2、widget组成
Widget可以表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画及导航等
比如:
动画层:Animation和Tween
渲染层:RenderObject 用来描述布局、绘制、触摸判断及可访问性
没有视觉内容功能:包含了布局、绘制、定位和大小的功能的Container 是由 LimitedBox、 ConstrainedBox、 Align、 Padding、 DecoratedBox 和 Transform 组成的
3、widget构建
通过重写 build() 方法,返回一个新的元素树定义视觉UI
build() 是将状态转化为 UI 的方法,widget 通过重写该方法来声明 UI 的构造:
UI = f(state)
build() 方法在框架需要时都可以被调用(每个渲染帧可能会调用一次),从设计角度来看,它应当能够快速执行且没有额外影响的。
这样的实现设计依赖于语言的运行时特征(特别是对象的快速实例化和清除)。幸运的是,Dart 非常适合这份工作。
每个渲染帧,Flutter 都可以根据变化的状态,调用 build() 方法重建部分 UI,确保build 方法轻量且能快速返回 widget 是非常关键的,繁重的计算工作应该通过一些异步方法完成,并存储在状态中,在 build 方法中使用
4、widget状态
InheritedWidget:
通过 build() 方法可以确保子 widget 使用其所需的数据进行实例化,随着 widget 树层级逐渐加深,依赖树形结构上下传递状态信息会变得十分麻烦。这时需要用到InheritedWidget。
主题:主题是典型的状态共享示例,调用 of(context) 会根据当前构建的上下文(即当前 widget 位置的句柄)逐级向上一级遍历直到找到对应的状态:
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.titleLarge,
),
);
更高级的InheritedWidget封装 provider 用于状态管理更方便。
5、widget 层次结构
示例代码:
Container(
color: Colors.yellow,
child: Row(
children: [
Image.network('xxx.png'),
const Text('风景图'),
],
),
);
代码绘制流程:
1、调用 build() 方法构建 widget 子树(返回一棵基于当前应用状态来绘制 UI 的 widget 子树)
2、插入节点 Container 的 Element
3、判断背景 color 属性不为空,ColoredBox (处理颜色)会被加入
4、插入节点Row (对应child)
5、开始处理child的子树 Image和 Text
7、插入子节点 RawImage 和 RichText(分别对应Image 和 Text )
整个流程(widget 子树)如下图左:
其中左图:
1、蓝色实线圆:表示 UI 生命周期 的Element 宿主(用户可见)
2、灰色虚线圆:参与布局或绘制阶段的Element(用户不可见)
而图左的本质是图右:包含 ComponentElement和RenderObjectElement
1、ComponentElement
- 定义与角色:ComponentElement是其他Element的宿主,它允许Widget的构建逻辑和渲染逻辑分离。这意味着ComponentElement能够处理更为复杂的生命周期事件,如状态更新、子树重绘等。
- 关键方法:
- mount:当Widget被添加到元素树中时调用,用于初始化任何必要的资源或状态。
- unmount:当Widget从元素树中移除时调用,用于释放任何已分配的资源。
- rebuild:如果Widget被替换,ComponentElement会调用rebuild方法来创建一个新的Widget并替换旧的Widget。
- update:当Widget树中需要更新时调用,用于处理Widget的变化并决定是否需要重新构建子树。
2、RenderObjectElement
- 定义与角色:RenderObjectElement是参与布局或绘制阶段的Element。它负责将Widget的描述转化为具体的RenderObject,后者负责实际的布局和绘制工作。
- 特点:
- RenderObjectElement持有与之关联的Widget的实例,当Widget发生变化时,它会触发更新过程,导致RenderObject重新布局和绘制。
- 它还负责将用户交互事件从RenderObject传递回Widget,以便Widget可以响应这些事件
Flutter中的大部分widget都是通过继承自RenderBox的类来渲染的。RenderBox是Flutter渲染树中的一个基础类,它提供了一个盒子模型(box model),这个模型定义了widget在二维笛卡尔空间(即屏幕)中的位置和大小。
RenderBox 和 盒子模型
- 盒子模型:在Flutter中,
RenderBox提供了一个简单的盒子模型,其中每个盒子(即widget的渲染表示)都有一个位置、大小以及可选的边距(margin)、边框(border)、内边距(padding)和内容区域。这个模型与Web开发中常用的盒子模型非常相似。 - 位置和大小:每个
RenderBox都有一个父坐标系中的位置(通常是通过其左上角的坐标来定义的)和一个固定的大小(宽度和高度)。这些属性共同决定了盒子在屏幕上的可见区域。 - 最小和最大约束:
RenderBox还允许为其关联的widget设置最小和最大的宽度和高度约束。这些约束在布局过程中非常重要,因为它们告诉父widget如何调整子widget的大小以满足布局要求。
6、Flutter UI三个核心
在Flutter中,widget、Element和RenderObject这三个核心概念各自扮演着不同的角色,但它们共同构建了一个灵活的UI框架,允许开发者以高度定制化的方式创建用户界面。
6.1 Widget
Widget是Flutter中的UI构建块,它们描述了UI的结构和外观。大多数widget都有一个或多个子widget,这些子widget通过child或children属性暴露出来。Widget本身并不直接参与布局和渲染,而是作为UI蓝图存在。
- 单一子节点:有些widget只接受一个子节点,比如Container,它通过child属性接收。
- 多个子节点:有些widget可以接受任意数量的子节点,比如Column或Row,它们通过children属性接收一个子节点列表。
- 无子节点:还有一些widget没有子节点,比如Text或Icon,它们直接描述了一个具体的UI元素。
6.2 Element
Element是widget在Flutter框架中的实例化表示。当widget树被构建时,每个widget都会对应一个Element。Element负责在运行时管理widget的状态和生命周期。虽然开发者通常不直接与Element打交道,但它们是Flutter框架内部实现UI更新和状态管理的重要部分。
6.3 RenderObject
RenderObject是负责布局和渲染的具体实现类。它们与Element相关联,但直接与底层的渲染引擎交互。RenderObject定义了如何在屏幕上绘制UI元素以及它们如何相互布局。
- 叶子节点:像RenderImage这样的RenderObject没有子节点,它们直接渲染一个图像。
- 单一子节点:RenderPadding这样的RenderObject有一个子节点,它用于在子节点周围添加内边距。
- 多个子节点:RenderFlex可以接受任意数量的子节点,并通过链表管理它们,实现灵活的布局,如弹性盒子布局。
定制化子模型
Flutter允许每个RenderObject对适用于该对象的子模型进行定制化。这意味着不同的RenderObject可以有不同的方式来管理它们的子节点。例如,RenderTable使用二维数组来存储子节点,以适应表格布局的需求。
专用子模型和通用子模型
- 专用子模型:有些widget,如Chip和InputDecoration,具有与其控制中的插槽相匹配的字段。这些字段允许开发者以更具语义化的方式添加子节点,比如将第一个子节点定义为前缀,第二个子节点定义为后缀。
- 通用子模型:大多数widget使用child或children属性来接收子节点,这种方式更加通用,但可能缺乏专用子模型所提供的语义化。
极端情况:RenderParagraph和TextSpan
RenderParagraph是一个特殊的RenderObject,它的子节点是TextSpan对象,而不是其他RenderObject。这意味着在RenderParagraph的边界内,RenderObject树被转换为TextSpan树。这种情况展示了Flutter框架的灵活性,允许开发者以最适合特定UI元素的方式来组织子节点。
琐碎Widget的存在
Flutter还提供了一些琐碎的widget,如Expanded、SizedBox和Visibility,它们封装了常见的UI模式,使开发者能够更容易地实现特定的UI效果。这些widget的存在简化了开发过程,让开发者能够更快地找到并解决问题。
7、RenderObject树和Element(Widget)树的关系
在Flutter中,RenderObject树和Element树是同构的,但RenderObject树实际上是Element树的一个子集。这意味着每一个RenderObject在Element树中都有一个对应的Element节点,但并非每一个Element节点都会有一个对应的RenderObject。这种设计允许Flutter在处理布局和绘制时更加灵活和高效。
分离的好处:
- 性能优化
- 当布局发生改变时,Flutter只需要遍历与布局相关的RenderObject树,而无需遍历整个Element树。由于Widget组合的原因,Element树中可能包含许多与布局无关的额外节点,这些节点在布局计算中可以被安全地忽略。
- 这种分离使得Flutter能够更精确地定位到需要更新的部分,从而减少不必要的计算和渲染,提高应用的性能。
- 清晰的职责分离
- RenderObject树和Element树各自承担不同的职责。RenderObject树专注于布局和绘制,而Element树则负责Widget的生命周期管理和状态更新。
- 这种清晰的职责分离使得Flutter的API更加简洁明了,降低了学习和使用的难度。同时,它也有助于减少bug的发生,因为开发者可以更加专注于自己熟悉的领域。
- 类型安全
- RenderObject树在运行时能够保证子节点具有合适的类型。由于每个坐标系都有自己的RenderObject类型,这使得Flutter能够在布局时准确地验证和匹配子节点的类型。
- 相比之下,Element树中的Widget组合更加灵活,可以不受布局坐标系的限制。因此,在Element树中验证RenderObject的类型需要额外的遍历和检查,这增加了复杂性和潜在的错误风险。
8、RenderView
在Flutter中,RenderObject树的根节点是RenderView。这个根节点代表了整个渲染树的输出,它是连接Flutter框架和底层渲染引擎(如Skia)的桥梁。RenderView负责协调整个渲染过程,确保每一帧的内容都能够正确地显示在屏幕上。
当平台需要渲染新的一帧内容时,这通常是由一些外部事件触发的,比如垂直同步信号(vsync)或者纹理的更新完成。在这些事件发生时,Flutter框架会调用RenderView的compositeFrame()方法。
compositeFrame()方法是渲染过程的核心,它负责创建一个SceneBuilder对象。SceneBuilder是一个辅助类,它用于构建和描述当前要渲染的场景(即一帧的内容)。在这个过程中,SceneBuilder会遍历整个RenderObject树,收集所有需要渲染的信息,比如各个widget的位置、大小、颜色、纹理等。
一旦SceneBuilder完成了对当前场景的描述,它就会将这些信息传递给dart:ui库中的Window.render()方法。Window是Flutter与底层操作系统和硬件进行交互的接口,它提供了与屏幕、输入设备等进行交互的能力。
Window.render()方法接收SceneBuilder构建的场景信息,并将其传递给GPU进行渲染。GPU是专门用于图形处理的硬件加速器,它能够高效地处理复杂的图形计算任务,并将渲染结果输出到屏幕上。
总的来说,RenderView、SceneBuilder和Window.render()共同构成了Flutter的渲染管道。这个管道负责将Flutter框架中的widget树转换为屏幕上的像素,从而实现了用户界面的动态更新和渲染。
四、平台嵌入层
1、平台嵌入层的功能
- 提供入口和初始化:当启动一个Flutter应用时,嵌入层会提供一个入口点,用于初始化Flutter引擎。这包括获取UI和栅格化线程,以及创建Flutter可以写入的纹理。
- 管理应用生命周期:嵌入层负责管理Flutter应用的生命周期,包括处理输入操作(如鼠标、键盘和触控)、窗口大小的变化等。
- 线程管理和消息传递:嵌入层还负责线程的管理和平台消息的传递,确保Flutter应用能够高效地与底层操作系统进行交互。
- Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层
2、不同平台的嵌入层实现
- iOS和macOS:
- Flutter通过
UIViewController(iOS)和NSViewController(macOS)载入到嵌入层。 - 嵌入层会创建一个
FlutterEngine,作为Dart VM和Flutter运行时的宿主。 FlutterViewController关联对应的FlutterEngine,传递UIKit(iOS)或Cocoa(macOS)的输入事件到Flutter。- Flutter引擎渲染的帧内容通过Metal或OpenGL进行展示。
- Flutter通过
- Android:
- Flutter默认作为一个
Activity加载到嵌入层中。 - 视图通过
FlutterView进行控制。 - 根据Flutter内容的合成和z排列(z-ordering)的要求,将Flutter的内容以视图模式或纹理模式进行呈现。
- Flutter默认作为一个
- Windows:
- Flutter的宿主是一个传统的Win32应用。
- 内容通过ANGLE库进行渲染,该库将OpenGL API调用转换成DirectX 11的等价调用。
- 正在尝试将UWP应用作为Windows的一种嵌入层,并考虑通过DirectX 12直接调用GPU。
3、自定义嵌入层
Flutter允许开发者创建自定义的嵌入层。例如,已经存在通过VNC风格的帧缓冲区支持远程Flutter的例子,以及支持树莓派运行的Flutter例子。这些自定义嵌入层展示了Flutter在不同环境和设备上的灵活性和可扩展性。
4、Flutter嵌套原生view
Flutter通过引入平台widget(AndroidView和UiKitView)确实解决了在不同平台上显示原生视图的问题。这两个widget允许Flutter应用在需要时嵌入和显示原生平台的视图组件,从而充分利用平台特定的功能和特性。
AndroidView
- 作用:
AndroidView允许在Flutter应用中嵌入和显示Android的原生视图。这对于需要在Flutter应用中集成特定的Android组件或服务(如地图、视频播放器等)时非常有用。 - 实现原理:
AndroidView通过Flutter的平台通道与Android原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的Android视图。这个视图会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。
UIKitView
- 作用:与
AndroidView类似,UiKitView允许在Flutter应用中嵌入和显示iOS的原生视图。这对于需要在Flutter应用中集成特定的iOS组件或服务时非常有用。 - 实现原理:
UiKitView的实现原理与AndroidView相似,也是通过Flutter的平台通道与iOS原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的iOS视图。这个视图同样会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。
性能开销:
嵌入原生视图可能会引入一定的性能开销,特别是在频繁更新或动画效果较多的情况下。因此,开发者需要在使用时权衡性能和功能之间的平衡。
5、Flutter引擎的Web实现
Flutter引擎原本是用C++编写的,主要用于与底层操作系统进行交互。然而,在Web平台上,由于不存在直接的操作系统API访问,Flutter需要重新实现其引擎部分。Flutter在Web上使用了浏览器的标准API来重新实现引擎的功能,这使得Flutter应用能够在Web浏览器上运行。
Dart语言从设计之初就支持直接编译成JavaScript,这为Flutter在Web上的运行提供了基础。Flutter框架本身是用Dart编写的,因此将其编译成JavaScript并在Web浏览器上运行是相对简单的。Dart编译器会生成高效的JavaScript代码,从而确保Flutter应用在Web上的性能
Web上的呈现选项
在Web平台上,Flutter提供了两种呈现内容的选项:HTML和WebGL。
- HTML模式
- 在HTML模式下,Flutter使用HTML、CSS、Canvas和SVG等Web技术进行渲染。
- 这种模式提供了较小的代码大小,因为HTML、CSS和Canvas等Web技术本身就是浏览器原生支持的,无需额外的库或框架。
- 然而,HTML模式的渲染性能可能不如WebGL模式。
- WebGL模式
- 在WebGL模式下,Flutter使用了一个编译为WebAssembly的Skia版本,名为CanvasKit。
- WebGL提供了更强大的图形渲染能力,因此CanvasKit能够为Flutter应用提供更高的图形保真度和更好的性能。
- 但是,WebGL模式会增加应用的代码大小,因为需要包含WebGL和CanvasKit的额外代码。
五、编译器
开发阶段:dartdevc 编译器
在开发 Flutter Web 应用时,dartdevc(Dart Development Compiler)是主要的编译器。这个编译器支持增量编译,这意味着它只会重新编译发生变化的代码部分,而不是整个应用。这个特性对于提升开发体验至关重要,因为它大大减少了每次代码修改后的编译时间。
- 热重启(Hot Restart):尽管 Flutter Web 目前还不完全支持热重载(Hot Reload),它支持热重启。热重启会在应用运行时替换整个 Dart 运行时环境,从而允许开发者在不重启整个浏览器的情况下看到最新的代码更改。这同样依赖于
dartdevc编译器的快速编译能力。
生产阶段:dart2js 编译器
当准备好将 Flutter Web 应用部署到生产环境时,dart2js(Dart to JavaScript Compiler)是首选的编译器。dart2js 将 Dart 代码深度优化并编译成高效的 JavaScript 代码,这是部署到浏览器环境的标准格式。
- 代码优化:
dart2js会对 Dart 代码进行深度优化,包括代码混淆(minification)、死代码消除(dead code elimination)和其他多种优化技术,以确保生成的 JavaScript 代码尽可能小且运行速度快。 - 部署选项:生成的 JavaScript 代码可以打包成一个单一的文件,便于部署。然而,为了改善应用的加载时间和性能,也可以将代码拆分成多个文件,使用延迟加载库(code splitting)技术。这允许浏览器在需要时才加载某些代码段,从而减少初始加载时间。
部署到服务器
一旦使用 dart2js 编译了 Flutter Web 应用,生成的文件就可以部署到任何能够托管静态文件的服务器上。这包括云服务器、内容分发网络(CDN)以及像 Firebase Hosting、GitHub Pages 这样的托管服务
六、渲染机制
1、渲染流程
Flutter 的渲染机制确实遵循了简单快速的首要原则,并且它通过一系列高效的步骤确保用户界面的流畅和响应性。
- User Input:用户通过键盘、触摸屏等输入设备进行操作。
- Responses to Input Gestures:系统响应用户的输入手势,这些手势可以是点击、滑动、长按等。
- Animation:Flutter 支持丰富的动画效果,可以响应用户输入或内部状态变化来触发动画。
- User Interface Changes Triggered by Timer:定时器(如帧刷新定时器)可以触发用户界面的定时变化,例如动画的连续帧更新。
- Build:应用代码构建屏幕上的 Widgets。Widgets 是 Flutter 中用户界面的基本构建块,它们描述了 UI 的结构。
- Layout:布局阶段确定每个元素在屏幕上的位置和大小。Flutter 使用一种称为约束传递的布局模型,从根 Widget 开始向下传递布局约束。
- Paint:绘制阶段将布局后的元素转换为视觉表示。这包括绘制形状、文本、图像等。
- Composition:组合阶段负责按照绘制顺序将视觉元素叠加在一起。这是确保正确显示层次关系的关键步骤。
- Rasterize:光栅化阶段将组合后的图像转换为 GPU 可以理解的渲染指令。这一步骤将二维图像转换为像素数据,准备在屏幕上显示。
- GPU Render:最后,GPU 渲染指令被发送到图形处理器(GPU),GPU 负责高效地将像素数据渲染到屏幕上。
这个流程确保了 Flutter 应用能够快速响应用户输入,同时保持高质量的视觉效果。Flutter 的这种渲染机制得益于其底层架构,特别是其使用 Dart 语言和 Skia 、Impeller图形库,共同为 Flutter 提供了高性能和跨平台的渲染能力
Flutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件
2、渲染引擎
Impeller是Flutter的新一代渲染引擎,其核心职责是绘制应用的界面,这包括布局计算、纹理映射、动画处理等一系列任务。它负责将代码转换为像素、颜色和形状,因此会直接影响应用的性能和渲染效果。以下是对Impeller渲染引擎的详细介绍:
一、Impeller的替换背景
尽管Skia是一个优秀的通用2D图形库,被广泛应用于Google Chrome、Android、Firefox等设备,但由于其通用性,它无法专门针对Flutter的要求进行优化。Skia附带的许多功能超出了Flutter的需求,其中一些可能会导致不必要的开销,导致渲染时间变慢。因此,Skia的通用性给Flutter带来了性能瓶颈。相比之下,Impeller是专门为Flutter设计的,旨在优化Flutter架构的渲染过程。
二、Impeller的渲染优势
- 高效的GPU利用:Impeller的渲染方法在Flutter上可以比Skia更有效地利用GPU,让设备的硬件以更少的工作量来渲染动画和复杂的UI元素,从而提高渲染速度。
- 提前优化图形渲染:Impeller采用tessellation和着色器编译来分解和提前优化图形渲染。这种策略可以减少设备上的硬件工作负载,从而实现更快的帧速率和更流畅的动画。
- 预编译着色器:与Skia动态编译着色器不同,Impeller会提前编译大部分着色器。这种预编译策略可以显著降低动画过程中的卡顿现象,因为GPU不必在渲染帧时暂停来编译着色器。预编译发生在Flutter应用的构建过程中,确保着色器在应用启动后立即可用。
- 分层架构设计:Impeller采用了新的分层架构来简化渲染过程。这种设计使引擎更加高效,并且更易于维护和更新。每一层都建立在下一层的基础上执行专门的功能,分离了不同的关注点。
三、Impeller的架构组成
- Aiks:Impeller架构的顶层,主要作为绘图操作的高级接口。它接受来自Flutter框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的“Entities”,然后转给下一层。
- Entities Framework:Impeller架构的核心组件。当Aiks处理完命令并生成Entities后,每一个Entity都是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息。每个Entity都带有transformation矩阵(编码位置、旋转、缩放)等属性,以及保存渲染所需GPU指令的content object。
- HAL(Hardware Abstraction Layer):构成了Impeller架构的基础,为底层图形硬件提供了统一的接口,抽象了不同图形API的细节。该层确保了Impeller的跨平台能力,将高级渲染命令转换为低级GPU指令,充当Impeller渲染逻辑和设备图形硬件之间的桥梁。
四、Impeller的其他优化
- 抗锯齿优化:Impeller通过多重采样抗锯齿(MSAA)来解决抗锯齿问题。MSAA的工作原理是在像素内的不同位置对每个像素进行多次采样,然后对这些样本进行平均以确定最终颜色,从而将对象的边缘与其背景平滑地融合,减少其锯齿状外观。
- 裁剪操作优化:Impeller利用模板缓冲区(stencil buffer,GPU的一个组件)来管理剪切过程。当Impeller渲染UI时,它会先让GPU使用模板缓冲区,该缓冲区主要充当过滤器,根据clipping蒙版确定应显示哪些像素。通过优化模板缓冲区,Impeller可确保快速执行剪切操作。
Impeller作为Flutter的新一代渲染引擎,在性能、渲染效果和跨平台能力等方面都表现出色。随着Flutter团队的不断优化和完善,Impeller有望成为未来Flutter应用的默认渲染引擎。
七、布局、算法与优化
1、遍历树
布局过程:
1、父widget会向其子widget提供一组布局约束(通常是最小和最大宽度和高度的限制)。
2、子widget然后根据这些约束来决定自己的大小,并通过调用父widget的layout方法来告知父widget自己的最终大小。
这个过程会递归地在整个widget树中进行,直到所有的widget都被正确地布局。
遍历节点:
1、Flutter 会以 DFS(深度优先遍历)方式遍历渲染树,并 将限制以自上而下的方式 从父节点传递给子节点。
2、子节点若要确定自己的大小,则 必须 遵循父节点传递的限制。子节点的响应方式是在父节点建立的约束内 将大小以自下而上的方式 传递给父节点。
3、遍历完一次树后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用 paint() 进行渲染
4、盒子限制模型对象布局的时间复杂度是 O(n)
2、布局算法优化(次线性)
Flutter 的目标是实现布局的线性性能初始化,以及在更新现有布局时的次线性性能。这意味着,在大多数情况下,布局操作应该比对象渲染更快。为了达到这一目标,Flutter 采用了高效的布局算法,这些算法在单次传递中完成布局,从而避免了多次测量和布局传递的开销。时间复杂度是 O(n)
单次传递布局
Flutter 对每一帧执行一次布局操作,且这个操作在单个传递中完成。在这个过程中,父节点向下传递约束信息,子节点根据这些约束递归地执行布局操作,并返回几何信息给父节点。这种策略确保了每个渲染对象在布局过程中最多被访问两次:一次在向下传递约束时,一次在向上传递几何信息时。
RenderBox 布局模型
RenderBox 是 Flutter 中最常用的布局模型,它使用二维笛卡尔坐标进行运算。在 RenderBox 布局中,约束以最小和最大宽高的形式传递给子节点。子节点根据这些约束选择自己的大小,并在布局完成后返回给父节点。父节点随后根据子节点返回的大小信息来确定子节点在父坐标系中的位置。
优化布局性能的策略
为了优化布局性能,Flutter 采用了多种策略:
- 避免不必要的布局传递:如果父节点对子节点使用了与上一次布局相同的约束,且子节点没有将自己的布局标记为脏(即需要重新布局),那么该子节点可以立即从布局中返回,无需进行任何计算。
- 利用严格约束:严格约束是指恰好由一个有效几何满足的约束。如果父节点提供了严格约束,即使父节点在布局中使用了子节点的大小,子节点重新计算布局时也不会影响父节点的布局,因为子节点无法在没有父节点新约束的情况下更改其大小。
- 声明性布局:渲染对象可以声明它们仅使用父节点提供的约束来确定其几何信息。这种声明可以通知框架,即使约束不是严格的,父节点的布局也不需要在子节点重新计算布局时重新计算。
- 局部更新:当渲染树中包含脏节点时,Flutter 的布局算法只会访问这些节点以及它们周围子树的有限节点。这种局部更新策略减少了不必要的计算,从而提高了布局性能。
3、无限滚动列表布局
通常实现无限滚动列表是比较困难的。Flutter 支持基于 构造器 模式实现的简单无限滚动列表界面,该功能需要 视窗感知布局 及 按需构建 widget 的
1、视窗感知布局
Viewport
Viewport是可滚动widget的外部容器,它提供了一个可以滚动的视窗口。Viewport本身并不直接渲染内容,而是包含一个或多个sliver,这些sliver负责实际的布局和渲染工作。Viewport的大小通常与屏幕大小相匹配,但它的内部空间可以远大于屏幕,从而允许用户滚动查看更多内容。
Sliver
Sliver是实现了视窗感知协议(Viewport-aware protocol)的RenderObject子类。与RenderBox不同,sliver不是直接填充其父容器的整个空间,而是根据Viewport提供的可见空间来进行布局。这意味着sliver可以处理超出视窗口边界的内容,并根据用户的滚动操作来动态地显示或隐藏这些内容。
Sliver的布局协议
在sliver布局协议中,父节点(通常是Viewport)向下传递给子节点(即sliver)一组约束信息,这些约束信息描述了视窗口的大小、滚动位置以及滚动方向等。子节点(sliver)根据这些约束信息来计算自己的布局,并返回一组几何信息来描述自己的位置和大小。
与盒子布局(如RenderBox)不同,sliver布局协议中的约束和几何数据更加复杂,因为它们需要考虑滚动和视窗口的变化。例如,一个sliver可能需要知道它还有多少可见空间来继续布局子节点,或者它是否已经滚动到了视窗口的底部。
Sliver的组合
Flutter允许开发者通过组合不同的sliver来创建复杂的滚动布局和效果。例如,一个Viewport可以包含一个折叠标题sliver、一个线性列表sliver和一个网格sliver。这些sliver将按照sliver布局协议进行协作,共同填充Viewport提供的可见空间。
由于sliver知道还有多少可见空间可用,它们可以智能地生成有限的子节点,即使这些子节点在理论上可能是无限的(例如,一个无限长的列表)。这种能力使得Flutter能够高效地处理大量数据,并为用户提供流畅的滚动体验。
2、按需构建新的widget
构建与布局的交叉执行
在Flutter中,构建(build)阶段通常用于创建widget树,而布局(layout)阶段则用于计算widget的位置和大小。然而,在处理无限滚动列表等场景时,如果严格按照构建到布局再到绘制的顺序执行,可能会导致性能问题,因为只有在布局阶段才能确定视窗口的可用空间,而这时再构建用于填充空间的widget可能已经太迟了。
为了解决这个问题,Flutter采用了构建和布局交叉执行的方式。这意味着在布局阶段的任意时刻,只要这些widget是当前布局的渲染对象的子节点,框架就可以按需构建新的widget。这种方式允许Flutter在布局过程中动态地调整widget树,以适应用户滚动和视图变化的需求。
消息传递和算法控制
为了确保构建和布局的交叉执行能够正确进行,Flutter严格控制了构建及布局中消息传播的算法。在构建过程中,消息只能沿构建树向下传递,以确保每个widget都能够正确地接收其父widget的状态和配置。同时,在布局遍历过程中,渲染对象不会访问其子树的构建状态,以避免在布局计算过程中使已构建的widget失效。
此外,一旦布局从某个渲染对象返回,在当前布局过程中,该渲染对象将不会被再次访问。这意味着后续布局计算生成的任何信息都不会影响已经构建的渲染对象的子树。这种设计确保了布局的确定性和一致性。
线性协调和树结构优化
在处理滚动和动态内容加载时,线性协调和树结构优化也是至关重要的。线性协调允许Flutter在滚动过程中有效地更新element树,以确保只有视窗口内的内容被重新构建和布局。这有助于减少不必要的计算和渲染,提高应用的性能。
同时,树结构优化也是提高滚动性能的关键。通过优化widget树的结构,Flutter可以减少不必要的节点和渲染工作,从而进一步提高滚动效率。
4、构建widget优化
Flutter使用一种高效的机制来构建和管理其界面,这种机制依赖于widget、element和state等关键概念。
-
Widget:在Flutter中,widget是界面构建的基本单元。它们是不可变的,这意味着一旦创建,它们的属性就不能改变。由于这种不可变性,Flutter框架可以高效地重用和比较widget,从而优化构建过程。
-
Element:Element是widget在界面树中的实例化表示。每个widget在构建时都会创建一个对应的element。Element树保留了用户页面的逻辑结构,并且是动态更新的。与widget不同,element可以记住与其他element的父或子关系,并且可以变脏(即需要更新)。
-
State:对于Stateful widget,它们的状态(state)是与特定的element实例相关联的。当widget的状态发生变化时,可以通过调用setState()方法来通知框架该widget需要重新构建。
-
构建过程:当某个element变脏时,Flutter框架会将其添加到脏element列表中。在构建过程中,框架会遍历这个列表,并跳过干净的element,只更新脏的element。这种机制使得构建过程非常高效,因为每个element在构建阶段最多只会被访问一次。
-
优化策略:
-
Widget比较:由于widget是不可变的,因此可以通过比较widget对象的引用来确定它们是否相同。如果父节点使用相同的widget来重新构建element,并且该element没有将自己标记为脏,那么可以直接从构建中返回,切断构建的向下传递。
-
投影模式:开发者可以利用widget的不可变性和构建过程的优化来实现投影模式。在这种模式下,widget可以包含预先构建的子widget作为成员变量,从而在构建过程中避免不必要的重复工作。
-
InheritedWidget:为了避免父链的遍历,Flutter框架使用InheritedWidget来向下传递信息。通过在每个element上维护一个InheritedWidget哈希表,框架可以高效地访问和更新这些信息,从而避免O(N²)的复杂度。
-
5、widget重用(不可变)
在Flutter中,element是widget树中的一个实例,它持有Stateful widget的状态对象以及底层的渲染对象。当框架能够重用element时,这意味着它不需要销毁和重新创建这些对象,从而保留了用户界面的逻辑状态信息和之前计算的布局信息。这不仅可以避免不必要的遍历整棵子树,还可以显著提高应用的性能,特别是在处理大量动态内容时。
关于全局树更新和GlobalKey的使用:
- GlobalKey:在Flutter中,GlobalKey是一种特殊的key,它允许开发者在整个应用中唯一地标识一个widget。当widget与GlobalKey相关联时,无论它在element树中的位置如何变化,框架都能够通过查找哈希表来找到并重用现有的element。这意味着即使widget被移动到了树的不同位置,它的状态和布局信息也会被保留下来。
- 全局树更新:通过使用GlobalKey,开发者可以实现全局树更新。这允许开发者在构建过程中将widget移动到element树的任意位置,而无需重新构建该widget的element。这不仅可以保留整棵子树的状态和布局信息,还可以避免不必要的重建和重绘。
- 布局约束的传递:在Flutter的布局过程中,布局约束是从父节点传递到子节点的。当子列表发生变化时,父节点可能会被标记为脏,并需要重新布局。但是,如果新的父节点传递给子节点的布局约束与该子节点从旧的父节点接收到的相同,那么子节点可以立即从布局中返回,而不需要进行任何实际的布局计算。这可以进一步减少不必要的布局工作,提高应用的性能。
开发者广泛使用全局key和全局树更新来实现各种高级效果,如hero transition(英雄动画)和导航等。这些效果通常需要在不同的widget树之间共享状态和布局信息,而全局key和全局树更新提供了一种高效且灵活的方式来实现这一点。
6、协调算法(更新树比较算法)
在Flutter中,当需要更新列表中的widget时,传统的做法可能是对整个列表进行树差异比较,这种方法的复杂度通常是O(N^2),其中N是列表中的widget数量。然而,Flutter采用了一种更高效的算法,其复杂度为O(N),这种算法通过独立地检查每个element的子节点来决定是否重用该element。
这种子列表协调算法针对几种特定情况进行了优化:
- 旧的子列表为空:这种情况下,如果新的子列表不为空,那么框架将简单地遍历新的子列表并创建新的element。
- 两个列表完全相同:如果新旧两个列表的widget完全相同(包括顺序和key),那么框架将不会进行任何更新,直接重用现有的element。
- 在列表的某个位置插入或删除:当在列表的某个位置插入或删除一个或多个widget时,框架会尽可能重用周围的element,只更新受影响的部分。
- 使用key来匹配widget:如果新旧列表都包含相同key的widget,那么这两个widget就会被认为是相同的。在这种情况下,框架会尝试重用与旧widget相关联的element,并用新的widget进行更新。这有助于在列表项的顺序发生变化时保持状态的连续性。
子列表协调算法的具体实现通常涉及以下步骤:
- 从新旧子列表的头部和尾部开始,对每一个widget的运行时类型和key进行匹配。
- 如果找到不匹配的widget,就确定了两个列表中所有不匹配子节点的范围。
- 将旧子列表中该范围内的子项根据它们的key放入一个哈希表中。
- 遍历新的子列表,对于每个widget,检查它的key是否在哈希表中。
- 如果找到匹配的key,就重用与该key相关联的element,并用新的widget进行更新。
- 如果找不到匹配的key,就丢弃旧的element并从头开始创建新的element。
这种算法的优点是能够高效地处理列表的更新,特别是在列表项的顺序频繁变化时。它避免了不必要的重建和重绘,从而提高了应用的性能和响应速度。同时,通过使用key来匹配widget,开发者可以更好地控制列表项的更新行为,并保持状态的连续性。
7、恒定因子优化
-
子模型无关性:
Flutter的渲染树设计得不会记住特定的子模型,这意味着它不会依赖于具体的子列表结构。例如,
RenderBox类有一个抽象的visitChildren()方法,而不是具体的firstChild和nextSibling接口。这种设计允许子类以更高效的方式处理其子项,特别是当子类只支持单个子节点时(如RenderPadding)。这种灵活性使得Flutter能够根据不同的布局需求进行优化,而不必受限于固定的子项结构。 -
视觉渲染树与Widget逻辑树的分离:
Flutter中的渲染树在与设备无关的视觉坐标系中运行,而Widget树则在逻辑坐标中运行。这种分离使得布局和绘制计算可以更加高效地进行,因为渲染树中的这些计算比Widget到渲染树的切换更加频繁。此外,逻辑坐标到视觉坐标的转换是在Widget树和渲染树之间的切换中完成的,这避免了重复的坐标转换,从而提高了性能。
-
专门的渲染对象处理文本:
Flutter使用专门的渲染对象
RenderParagraph来处理文本布局。这种设计使得文本布局可以更加高效地进行,因为RenderParagraph作为渲染树中的一个叶子节点,可以避免在父节点提供相同布局约束下的重复计算。此外,开发者可以通过组合形式将文本并入到用户界面中,而不必使用文本感知渲染对象进行子类化,这进一步简化了文本处理流程。 -
可观察对象在渲染树中的应用:
Flutter使用模型观察及响应设计模式,但在某些叶子节点的数据结构上使用了可观察对象。例如,
Animation对象会在值发生变化时通知观察者列表。Flutter将这些可观察对象从Widget树转移到渲染树中,使得渲染树可以直接监听这些对象的变化,并在它们改变时仅重绘管道的相关阶段。这种设计减少了不必要的重建和重绘,提高了应用的性能。