Flutter框架详解-程序员宅基地

技术标签: flutter  android  Flutter  

单页面应用

要了解跨平台框架,首先要知道,大部分的移动端跨平台框架都是“单页面”应用。

什么是“单页面”应用?
也就是对于原生 Android 和 iOS 而言,整个跨平台 UI 默认都是运行在一个 ​​Activity​​ / ​​ViewController​​ 上面,默认情况下只会有一个 ​​Activity​​​ / ​​ViewController​​, Flutter、 ReactNative 、Weex 、Ionic 默认情况下都是如此,所以一般情况下框架的路由和原生的路由是没有直接关系。

跨平台应用默认情况下作为单页面应用,他们的路由堆栈是和原生层存在不兼容的隔离。

当然这里面重复用了一个词:“默认”,也就是其实可以支持自定义混合堆栈的,比如官方的 ​​FlutterEngineGroup​​​ ,第三方框架 ​​flutter_boost​​​ 、 ​​mix_stack​​​ 、​​flutter_thrio​​ 等等。

跨平台框架的设计理念

端上的开发无外乎三件事,“数据获取”“状态管理”“页面渲染”
而在跨端领域的竟争,是 “虚拟机”“渲染引擎”“原生交互”“开发环境” 的竟争

Flutter使用了 Dart VM,Dart 支持 JIT 与 AOT 两种编译模式。
  • 在开发阶段使用 JIT 编译,实现热更新预览,动态加载等,
  • 发布阶段使用 AOT 模式编译为机器码,保证启动速度和跨端信息的传递效率。
Flutter 使用了 Skia 渲染引擎进行视图绘制,
  • 避开了不同平台上控件渲染差异。
  • 少了这一层的交互,使得效率也得到提升。
Flutter在原生交互上,与原生交互的效率非常高。
  • Dart 本身跨平台的特性,底层 C++ 可以直接访问到原生的 API,
  • 加上信息使用机器码进行传递 (BinaryMessage)
RN虚拟机使用的是 JSC (Javascript Core) 执行运算
  • 在早期的架构上虚拟机使用的是 JSC (Javascript Core) 执行运算,这样它可以充分复用 JS 生态,吸引大量前端开发者参与。
RN在渲染引擎上 ,直接复用了原生的渲染通道,没有直接使用 WebKit 或其它 Web 引擎,
  • 因为之前 Web 在构建复杂页面时带来的计算消耗,远比不上纯原生引擎的渲染。所以它直接复用了原生的渲染通道,这样就可以带来与原生近乎一致的体验。
  • 虽然早期的 RN 架构充分利用了现有生态,但毕竟不像 Flutter 那样从头到尾都自己来,那么的撤底。带来的问题就是,在 JSC 到原生渲染这一层,用了非常多的 Bridge,并通过 JSON 序列化在多个线程里来回传递信息,这样的消耗在简单的交互过程中可能不明显,而在大量的交互与渲染上会有明显的卡顿,这也成为广为诟病的一点。

跨平台框架的架构

Flutter 核心架构

在这里插入图片描述

framework层中的每一个组件均是可选的和可以代替的。从上图可知,Flutter系统总共可以分为三层。上层的框架(Framework),中层的引擎(Engine),以及底层的嵌入层(Embedder)。

Flutter Framework

  • 框架(Framework):Flutter framework 框架层是纯dart语言实现的一个响应式框架,由许多抽象的层级组成。在这些层级的最顶端是我们经常用到的 Material 和 Cupertino Widgets。我们大多数情况下使用的就是这两类 Widget。比如:UI/文本/图片/按钮等基础 Widgets。 在 Widget 层下面,你会发现 Rendering 层。Rendering层简化了布局和绘制过程。它是 dart:ui 的的抽象化。dart:ui 是框架的最底层,它负责处理与 Engine 层的交流沟通。 此部分的核心代码是: flutter 仓库下的flutter package,以及 sky_engine 仓库下的 io, async, ui (dart:ui 库提供了 Flutter 框架和引擎之间的接口)等package。

    • 通常情况下,开发人员通过Flutter Framework 与 Flutter 进行交互,Flutter 框架提供了一个用 Dart 语言编写的现代、反应式框架。它包括一套丰富的平台、布局和基础库,由一系列的层组成。从底层到顶层有:
    • 基础类和构件服务,如动画,绘画和手势,在底层基础上提供了常用的抽象。
    • 渲染层提供了一个处理布局的抽象。通过这一层,你可以建立一个可渲染对象的树。你可以动态地操作这些对象,树会自动更新布局以反映你的变化。
    • widgets 层是一个组成抽象。渲染层中的每个渲染对象在 widgets 层中都有一个对应的类。此外,widgets层还允许你定义可以重用的类的组合。这是引入反应式编程模型的一层。
    • Material和Cupertino库提供了全面的控件集,这些控件使用 widget 层的组合基元来实现 Material 或 iOS 设计语言。

Flutter框架相对较小;许多开发者可能会用到的更高级别的功能都是以包的形式实现的,包括像摄像头和 webview 这样的平台插件,以及像字符、http 和动画这样的平台无关的功能,这些都是建立在核心Dart和Flutter库的基础上的。其中一些包来自更广泛的生态系统,涵盖应用内支付、苹果认证和动画等服务。​

dart:ui library

dart:ui library暴露出最底层的服务,这些服务被用来引导Application,例如用来驱动输入、绘制文字、布局和渲染子系统。

所以你可以仅仅通过使用实例化dart:ui库中的类(例如Canvas、Paint和TextField)来构建一个 Flutter App。但是如果你对于直接在 canvas 上绘制比较熟悉,就会知道使用这些底层 api 绘制一个图案是既难又繁琐的。 接下来考虑一些不是绘制的东西吧,例如布局和命中测试。 这些意味着什么呢? 这意味着你必须手动的计算所有在你布局中使用的坐标,然后混合一些绘制和命中测试来捕获用户的输入。对每一帧进行上述操作并追踪它们。这个方法对于那些比较简单的APP,比如一个在蓝色区域内展示文字这种比较适用。如果对于那些比较复杂的APP或者简单的游戏来说可够你受的了。更不用说产品经理最喜爱的动画、滚动和一些酷炫的UI效果了。

Rendering library

Flutter 的 Rendering tree(渲染树)。RenderObject的层级结构被Flutter Widgets库使用来实现其布局和后台的绘制。通常来说,尽管你可能会使用RenderBox来在你的应用中实现自定义的效果,但是大多数情况下我们唯一与RenderObject的交互就是在调试布局信息的时候。

Rendering library 是 dart:ui library 上第一个抽象层。它替你做了所有繁重的数学计算工作(例如跟踪需要不断计算的坐标)。它使用 RenderObjects 来处理这些工作。你可以把 RenderObjects 想象成一个汽车的发动机,它承担了所有把你的APP展示到屏幕的工作。Rendering tree 中的所有 RenderObjects 都会被Flutter分层和绘制。为了优化这个复杂的过程,Flutter 使用了一个智能算法来缓存这些实例化很耗费性能的对象从而实现在性能最优化。 大多数情况,你会发现 Flutter 使用 RenderBox 而不是 RenderObject。这是因为项目的构建者发现使用一个简单和盒布局约束就能够成功的构建出有效稳定的UI。想象一下所有的 Widget 都被放置在它们的盒中。这个盒中的相关参数都计算好了,然后被放置到其他已经整理好的盒中间。所以如果在你的布局中仅有一个 Widget 改变了,只需要装载其的盒被系统重新计算即可。

Widget library

Flutter Widgets框架

Widget 库或许是最有意思的库。它是另外一个用来提供开箱即用的 Widget 的抽象层。这个库中所有的 Widget 都属于以下三种使用适当的 RenderObject 处理的 Widget 之一。

  • Layout,例如 Column 和 Row Widgets 用来帮助我们轻松的处理其他 Widget 的布局。
  • Painting,例如 Text 和 Image Widgets 允许我们展示(绘制)一些内容在屏幕上。
  • Hit-Testing,例如 GestureDetector 允许我们识别出不同的手势,例如点击和滑动。

大多数情况下我们会使用一些“基础” Widget 来组成我们需要的 Widget。例如我们使用 GestureDetector 来包裹Container,Container 中包裹 Button 来处理按钮点击。这叫做组合而不是继承。 然而除了自己构建每个UI组件,Flutter团队还创建了两个包含常用的 Material 和 Cupertino 风格的 Widgets 的库。

Material & Cupertino library

使用 Material 和 Cupertino 设计规范的Widgets库。

Flutter 为了减少开发者的负担,创建了这个拥有 Material 和 Cupertino 风格的 Widgets 层。

Flutter Engine

  • 引擎(Engine):Flutter 的核心是 Flutter 引擎。引擎层绝大部分是用C++实现的,支持所有 Flutter 应用所需的基元。每当需要绘制新的帧时,该引擎负责对合成场景进行光栅化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过Skia)、文本布局、文件和网络I/O、可访问性支持、插件架构以及 Dart 运行时和编译工具链,是连接框架和系统(Andoird/iOS)的桥梁。主要包括: Skia, Dart 和 Text。
    • Flutter引擎通过dart:ui暴露给Flutter框架,它将底层的C++代码封装在Dart类中。这个库暴露了最底层的基元,例如用于驱动输入、图形和文本渲染子系统的类。
    • 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来渲染字体。

Flutter Embedder

  • 嵌入层(Embedder):平台嵌入层是用于呈现所有 Flutter 内容的原生系统应用,它充当着宿主操作系统和 Flutter 之间的粘合剂的角色。当启动一个 Flutter 应用时,嵌入层会提供一个入口,初始化 Flutter 引擎,获取 UI 和栅格化线程,创建 Flutter 可以写入的纹理。嵌入层同时负责管理应用的生命周期,包括输入的操作(例如鼠标、键盘和触控)、窗口大小的变化、线程管理和平台消息的传递。 Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层,
    Flutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件。获取纹理和联动应用底层的生命周期的方法,不可避免地会根据平台特性而改变。 Flutter 引擎本身是与平台无关的,它提供了一个稳定的 ABI(应用二进制接口),包含一个 平台嵌入层,可以通过其方法设置并使用 Flutter。

    • 对底层操作系统而言,Flutter应用程序与其他本地应用程序一样,以相同的方式进行打包。
    • 一个平台特定的嵌入器提供了一个入口点;与底层操作系统协调,以访问服务,如渲染表面、可访问性和输入;并管理消息事件循环。
    • 嵌入器是用适合平台的语言编写的:目前 Android 的 Java 和 C++,iOS 和 macOS 的 Objective-C/Objective-C++,Windows 和 Linux 的 C++。
    • 使用嵌入器,Flutter 代码可以作为一个模块集成到现有的应用程序中,也可以是应用程序的全部内容。Flutter 包含了许多针对常见目标平台的嵌入器,但也存在其他嵌入器。

    每一个平台都有各自的一套 API 和限制。以下是一些关于平台简短的说明:

    • 在 iOS 和 macOS 上, Flutter 分别通过 UIViewController 和 NSViewController 载入到嵌入层。这些嵌入层会创建一个 FlutterEngine,作为 Dart VM 和您的 Flutter 运行时的宿主,还有一个 FlutterViewController,关联对应的 FlutterEngine,传递 UIKit 或者 Cocoa 的输入事件到 Flutter,并将 FlutterEngine 渲染的帧内容通过 Metal 或 OpenGL 进行展示。
    • 在 Android 上,Flutter 默认作为一个 Activity 加载到嵌入层中。此时视图是通过一个 FlutterView 进行控制的,基于 Flutter 内容的合成和 z 排列 (z-ordering) 的要求,将 Flutter 的内容以视图模式或纹理模式进行呈现。
    • 在 Windows 上,Flutter 的宿主是一个传统的 Win32 应用,内容是通过一个将 OpenGL API 调用转换成 DirectX 11 的等价调用的库 ANGLE 进行渲染的。目前正在尝试将 UWP 应用作为 Windows 的一种嵌入层,并将 ANGLE 替换为通过 DirectX 12 直接调用 GPU 的方式。

从架构图可以看出,Flutter 的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,Flutter 从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。

RN核心架构

RN 核心架构
React架构

  • JS Bundle 不再依赖于 JSC(Javascript Core)。换句话说,它可以编译和应用在任何 JS 引擎 (V8等)。
  • 引入 JSI 标准,基于 JSI 协议实现各自方法,使得 JS 可以直接引用 C++ 对象,反之亦然。与原生之间的交互不再用 Bridge 去做粘合。
  • 渲染引擎仍是依赖原生的管道。猜测可能 FB 没有像 Google 那样,有这么多年的 Web 渲染引擎经验,轮子就不用再花时间再造了

跨平台框架的渲染逻辑

在渲染层面 Flutter 和其他跨平台框架存在较大差异,但是理论基本一样,先是构建一颗平台无关性的虚拟树 (Virtual Dom Tree),然后通过各自不同的实现自已渲染或交给原生进行渲染。

在这里插入图片描述
在这里插入图片描述

  • 原生 Android ,是原生代码经过 skia 最后到 GPU 完成渲染绘制,Android 原生系统本身自带了 skia;

  • Flutter ,Dart 代码里的控件经过 skia 最后到 GPU 完成渲染绘制,这里在 Andriod 上使用的系统的 skia ,而在 iOS 上使用的是打包到项目里的 skia ;

    • Flutter​​​ 与之不同的地方就是渲染直接利用 skia 和 GPU 交互,在 Android 和 iOS 平台上实现了平台无关的控件,简单说就是 ​​Flutter​​​ 里的 ​​Widget​​ 大部分都是和 Android 和 iOS 没有关系。
    • 本质上原生平台是提供一个类似 ​​Surface​​ 的画板,之后剩下的只需要由 Flutter 来渲染出对应的控件
    • 一般是使用 ​​FlutterView​​​ 作为渲染承载,它在 Android 上内部使用可以是 ​​SurfaceView​​​ 、 ​​TextureView​​​ 或者 ​​FlutterImageView​​​ ;在 iOS 上是 ​​UIView​​​ 通过 ​​Layer​​ 实现的渲染。
    • 所以 Flutter 的控件在不同平台可以得到一致效果,但是和原生控件进行混合也会有较高的成本和难度,在接入原生控件的能力上,Flutter 提供了 ​​PlatformView​​​ 的机制来实现接入, ​​PlatformView​​ 本身的实现会比较容易引发内存和键盘等问题,所以也带来了较高的接入成本。
  • ReactNative/Weex 等类似的项目,它们是运行在各自的 JS 引擎里面,最后通过映射为原生的控件,利用原生的渲染能力进行渲染;

​ReactNative/Weex​​ 这类跨平台和原生平台存在较大关联:
  • 好处就是:如果需要使用原生平台的控件能力,接入成本会比较低;
  • 坏处自然就是: 渲染严重依赖平台控件的能力,耦合较多,不同系统之间原生控件的差异,同个系统的不同版本在控件上的属性和效果差异,组合起来在后期开发过程中就是很大的维护成本。

例如:在 iOS 上调试好的样式,在 Android 上出现了异常;在 Android 上生效的样式,在 iOS 上没有支持;在 iOS 平台的控件效果,在 Android 上出现了不一样的展示,比如下拉刷新,Appbar等;

Flutter 的布局和渲染

既然 Flutter 是一个跨平台的框架,那么它如何提供与原生平台框架相当的性能?它是如何 从widget 层级结构转换成屏幕上绘制的实际像素?需要经历那些步骤?

让我们从安卓原生应用的角度开始思考。当你在编写绘制的内容时,你需要调用 Android 框架的 Java 代码。 Android 的系统库提供了可以将自身绘制到 Canvas 对象的组件,接下来 Android 就可以使用由 C/C++ 编写的 Skia 图像引擎,调用 CPU 和 GPU 完成在设备上的绘制。

通常来说,跨平台框架都会在 Android 和 iOS 的 UI 底层库上创建一层抽象,该抽象层尝试抹平各个系统之间的差异。这时,应用程序的代码常常使用 JavaScript 等解释型语言来进行编写,这些代码会与基于 Java 的 Android 和基于 Objective-C 的 iOS 系统进行交互,最终显示 UI 界面。所有的流程都增加了显著的开销,在 UI 和应用逻辑有繁杂的交互时更为如此。

Flutter 通过绕过系统 UI 组件库,使用自己的 widget 内容集,削减了抽象层的开销。用于绘制 Flutter 图像内容的 Dart 代码被编译为机器码,并使用 Skia 进行渲染。 Flutter 同时也嵌入了自己的 Skia 副本,让开发者能在设备未更新到最新的系统时,也能跟进升级自己的应用,保证稳定性并提升性能。

从用户操作到 GPU

在这里插入图片描述
在这里插入图片描述

构建:从 Widget 到 Element

首先观察以下的代码片段,它代表了一个简单的 widget 结构:

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当 Flutter 需要绘制这段代码片段时,框架会调用 build() 方法,返回一棵基于当前应用状态来绘制 UI 的 widget 子树。在这个过程中,build() 方法可能会在必要时,根据状态引入新的 widget。在上面的例子中,Container 的 color 和 child 就是典型的例子。我们可以查看 Container 的 源代码,你会看到当 color 属性不为空时,ColoredBox 会被加入用于颜色布局。

if (color != null)
  current = ColoredBox(color: color!, child: current);

与之对应的,Image 和 Text 在构建过程中也会引入 RawImage 和 RichText。如此一来,最终生成的 widget 结构比代码表示的层级更深,在该场景中如下图

在这里插入图片描述
这就是为什么在使用 Dart DevTools 的 Flutter inspector 调试 widget 树结构时,会发现实际的结构比你原本代码中的结构层级更深。

在构建的阶段,Flutter 会将代码中描述的 widgets 转换成对应的 Element 树,每一个 Widget 都有一个对应的 Element。每一个 Element 代表了树状层级结构中特定位置的 widget 实例。目前有两种 Element 的基本类型:

  • ComponentElement,其他 Element 的宿主。
  • RenderObjectElement,参与布局或绘制阶段的 Element。

在这里插入图片描述
RenderObjectElement 是底层 RenderObject 与对应的 widget 之间的桥梁,我们晚些会介绍它。
任何 widget 都可以通过其 BuildContext 引用到 Element,它是该 widget 在树中的位置的句柄。类似 Theme.of(context) 方法调用中的 context,它作为 build() 方法的参数被传递。

由于 widgets 以及它上下节点的关系都是不可变的,因此,对 widget 树做的任何操作(例如将 Text(‘A’) 替换成 Text(‘B’))都会返回一个新的 widget 对象集合。但这并不意味着底层呈现的内容必须要重新构建。 Element 树每一帧之间都是持久化的,因此起着至关重要的性能作用, Flutter 依靠该优势,实现了一种好似 widget 树被完全抛弃,而缓存了底层表示的机制。 Flutter 可以根据发生变化的 widget,来重建需要重新配置的 Element 树的部分。

布局和渲染

很少有应用只绘制单个 widget。因此,有效地排布 widget 的结构及在渲染完成前决定每个 Element 的大小和位置,是所有 UI 框架的重点之一。

在渲染树中,每个节点的基类都是 RenderObject,该基类为布局和绘制定义了一个抽象模型。这是再平凡不过的事情:它并不总是一个固定的大小,甚至不遵循笛卡尔坐标规律(根据该 极坐标系的示例 所示)。每一个 RenderObject 都了解其父节点的信息,但对于其子节点,除了如何 访问 和获得他们的布局约束,并没有更多的信息。这样的设计让 RenderObject 拥有高效的抽象能力,能够处理各种各样的使用场景。

在构建阶段,Flutter 会为 Element 树中的每个 RenderObjectElement 创建或更新其对应的一个从 RenderObject 继承的对象。 RenderObject 实际上是原语:渲染文字的 RenderParagraph、渲染图片的 RenderImage 以及在绘制子节点内容前应用变换的 RenderTransform 是更为上层的实现。
在这里插入图片描述
大部分的 Flutter widget 是由一个继承了 RenderBox 的子类的对象渲染的,它们呈现出的 RenderObject 会在二维笛卡尔空间中拥有固定的大小。 RenderBox 提供了 盒子限制模型,为每个 widget 关联了渲染的最小和最大的宽度和高度。

在进行布局的时候,Flutter 会以 DFS(深度优先遍历)方式遍历渲染树,并 将限制以自上而下的方式 从父节点传递给子节点。子节点若要确定自己的大小,则 必须 遵循父节点传递的限制。子节点的响应方式是在父节点建立的约束内 将大小以自下而上的方式 传递给父节点。
在这里插入图片描述
在遍历完一次树后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用 paint() 进行渲染。
盒子限制模型十分强大,它的对象布局的时间复杂度是 O(n):

父节点可以通过设定最大和最小的尺寸限制,决定其子节点对象的大小。例如,在一个手机应用中,最高层级的渲染对象将会限制其子节点的大小为屏幕的尺寸。(子节点可以选择如何占用空间。例如,它们可能在设定的限制中以居中的方式布局。)

父节点可以决定子节点的宽度,而让子节点灵活地自适应布局高度(或决定高度而自适应宽度)。现实中有一种例子就是流式布局的文本,它们常常会填充横向限制,再根据文字内容的多少决定高度。

这样的盒子约束模型,同样也适用于子节点对象需要知道有多少可用空间渲染其内容的场景,通过使用 LayoutBuilder widget,子节点可以得到从上层传递下来的约束,并合理利用该约束对象,使用方法如下:

Widget build(BuildContext context) {
    
  return LayoutBuilder(
    builder: (context, constraints) {
    
      if (constraints.maxWidth < 600) {
    
        return const OneColumnLayout();
      } else {
    
        return const TwoColumnLayout();
      }
    },
  );
}

更多有关约束和布局系统的信息,及可参考的例子,可以在 深入理解 Flutter 布局约束 文章中查看。

所有 RenderObject 的根节点是 RenderView,代表了渲染树的总体输出。当平台需要渲染新的一帧内容时(例如一个 vsync 信号或者一个纹理的更新完成),会调用一次 compositeFrame() 方法,它是 RenderView 的一部分。该方法会创建一个 SceneBuilder 来触发当前画面的更新。当画面更新完毕,RenderView 会将合成的画面传递给 dart:ui 中的 Window.render() 方法,控制 GPU 进行渲染。

有关渲染流程的合成和栅格化阶段的更多细节,将不在本篇深入文章中讨论,但可以在 关于 Flutter 渲染流程的讨论 中了解更多。

Widget树,Element树,RenderObject树,的联系

Flutter 是如何创建布局?RenderObject 又是如何与 Widgets 连接起来的呢?Element 又是什么呢?我们接下来看个简单的例子,简单了解它们之间的关系。

初始构建

我们构建的这个 APP 是非常简单的。它由三个 Stateless Widget 组成:SimpleApp、SimpleContainer、SimpleText。所以如果我们调用 Flutter 的 runApp() 方法会发生什么呢? 当 runApp() 被调用时,第一时间会在后台发生以下事件。

  1. Flutter 会构建包含这三个 Widget 的 Widgets 树。
  2. Flutter 遍历 Widget 树,然后根据其中的 Widget 调用 createElement() 来创建相应的 Element 对象,最后将这些对象组建成 Element 树。
  3. 第三个树被创建,这个树中包含了与 Widget 对应的 Element 通过 createRenderObject() 创建的RenderObject。 下图是 Flutter 经过这三个步骤后的状态:

在这里插入图片描述
Flutter创建了三个不同的树,一个对应着 Widget,一个对应着 Element,一个对应着 RenderObject。每一个Element中都有着相对应的 Widget 和 RenderObject 的引用。

Widget

Flutter内一切都是Widget 。Widget 是非常轻量级的,实例化耗费的性能很少,所以它是描述 APP 的状态(也就是 configuration)的最好工具

RenderObject

那什么是 RenderObject 呢?RenderObject 中包含了所有用来渲染实例 Widget 的逻辑。它负责 layout、painting 和 hit-testing。它的生成十分耗费性能,所以我们应该尽可能的缓存它。我们把它在内存中尽可能的保存更长的时间,甚至回收利用它们(因为它们的实例化真的很耗费资源),这个时候 Element 就需要登场了。

Element

Element 是存在于可变 Widget 树和不可变 RenderObject 树之间的桥梁。Element 擅长比较两个 Object,在Flutter里面就是 Widget 和 RenderObject。它的作用是配置好 Widget 在树中的位置,并且保持对于相对应的RenderObject 和 Widget 的引用。

性能优化——整个Flutter APP就像是一个 RecycleView。

为什么使用三个树而不是一个树呢? 简而言之是为了性能。当 Widget 树改变的时候,Flutter 使用 Element 树来比较新的 Widget 树和原来的 RenderObject 树。如果某一个位置的 Widget 和RenderObject 类型不一致,才需要重新创建 RenderObject。如果其他位置的 Widget 和 RenderObject 类型一致,则只需要修改 RenderObject 的配置,不用进行耗费性能的 RenderObject 的实例化工作了。

因为 Widget 是非常轻量级的,实例化耗费的性能很少,所以它是描述 APP 的状态(也就是 configuration)的最好工具。重量级的 RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用。就像 Simon 所说:整个Flutter APP就像是一个 RecycleView。 然而,在框架中,Element 是被抽离开来的,所以你不需要经常和它们打交道。每个 Widget 的 build(BuildContext context)方法中传递的 context 就是实现了 BuildContext 接口的Element,这也就是为什么相同类别的单个 Widget 不同的原因。

页面更新——三个树的变化

因为 Widget 是不可变的,当某个 Widget 的配置改变的时候,整个 Widget 树都需要被重建。例如当我们改变一个Container 的颜色为红色的时候,框架就会触发一个重建整个 Widget 树的动作。然后在 Element 的帮助下,Flutter 比较新的 Widget 树中的第一个 Widget 类型和 RenderObject 树中第一个 RenderObject 的类型。接下来比较 Widget 树中第二个 Widget 和 RenderObject 树中第二个 RenderObject 的类型,以此类推,直到 Widget 树和 RendObject 树比较完成。

Flutter遵循一个最基本的原则:判断新的 Widget 和老的 Widget 是否是同一个类型。 如果不是同一个类型,那就把 Widget、Element、RenderObject 分别从它们的树(包括它们的子树)上移除,然后创建新的对象。 如果是一个类型,那就仅仅修改 RenderObject 中的配置,然后继续向下遍历。

不改变类型仅修改属性

当我们不改变类型仅仅修改一个颜色属性,SimpleApp Widget 是和原来一样的类型,它的配置也是和原来的 SimpleAppRender 一样的,所以什么都不会发生。下一个 item 在 Widget 树中是 SimpleContainer Widget,它的类型和原来是一样的,但是它的颜色变化了,RenderObject的配置发生变化了。因为 SimpleObject 仍然需要一个 SimpleContainerRender 来渲染,Flutter只是更新了SimpleContainerRender 的颜色属性,然后要求它重新渲染。其他的对象都保持不变。
在这里插入图片描述

Widget 的类型发生改变了

Flutter 会对 Widget 树的顶端向下遍历,与 RenderObject 树中的 RenderObject 类型进行对比。
在这里插入图片描述
因为 SimpleButton 的类型与 Element 树中相对应位置的 Element 的类型不同(实际上还是与 RenderObject 的类型进行比较),Flutter 将会从各自的树上删除这个 Element 和相对应的 SimpleTextRender。然后 Flutter 将会重建与 SimpleButton 相对应的 Element 和 RenderObject。
在这里插入图片描述
RenderObject 树已经被重建,并将会计算布局,然后绘制在屏幕上面。

Flutter 打包调试

Flutter 运行之前都需要先执行 ​​flutter pub get​​​ 来先同步下载第三方代码,下载的第三方代码一般存在于(Mac) ​​/Users/你的用户名/.pub-cache​​ 目录下 。

下载依赖成功后,可以直接通过 ​​flutter run​​ 或者 IDE 工具点击运行来启动 Flutter 项目,这个过程会需要原生工程的一些网络同步工作,比如:

  • Android 上的 Gradle 和 aar 依赖包同步;
  • iOS 上需要 pod install 同步一些依赖包;

如果需要在项目同步过程中查看进度:

  • Android 可以到​​android/​​​ 目录下执行​​./gradlew assembleDebug​​ 查看同步进度;
  • iOS 可以到​​ios/​​​ 目录下执行​​pod install​​,查看下载进度;

同步的插件中,如果是 ​​Plugin​​​ 带有原生平台的代码逻辑,那么可以在项目根目录下看到一个叫做 ​​.flutter_plugins​​​ 和 ​​.flutter-plugins-dependencies​​ 的文件,它们是 git ignore 的文件,Android 和 iOS 中会根据这个文件对本地路径的插件进行引用,后面 Flutter 运行时会根据这个路径动态添加依赖。

  • 默认情况下 Flutter 在 debug 下是 JIT 的运行模式所以运行效率会比较低,速度相对较慢,但是可以 hotload。

  • 在 release 下是 AOT 模式,运行速度会快很多,同时 Flutter 在模拟器上一般默认会使用 CPU 运行,在真机上会使用 GPU 运行,所以性能表现也不同。

  • 另外 iOS 14 真机上 debug 运行,断后链接后再次启动是无法运行的。

  • 如果项目存在缓存问题,可以直接执行 ​​flutter clean​​ 来清理缓存。

Flutter 的为什么不支持热更新?
ReactNative 和 Weex 是通过将 JS 代码里的控件转化为原生控件进行渲染,所以本质上 JS 代码部分都只是文本而已,利用 ​​code-push​​ 推送文本内容本质上并不会违法平台要求。
而 Flutter 打包后的文件是二进制文件,推送二进制文件明显是不符合平台要求的。

release 打包后的 Android 会生成 ​​app.so​​​ 和 ​​flutter.so​​​ 两个动态库;
iOS 会生成 ​​App.framework​​​ 和 ​​Flutter.framework​​ 两个文件。

Flutter 主要优先了解这三点:响应式、​​Widget​​ 和状态管理 。

响应式

响应式编程也叫做声明式编程,这是现在前端开发的主流,当然对于客户端开发的一种趋势,比如 ​​Jetpack Compose​​​ 、​​SwiftUI​​ 。

Jetpack Compose 和 Flutter 的在某些表层上看真的很相似。

响应式简单来说其实就是你不需要手动更新界面,只需要把界面通过代码“声明”好,然后把数据和界面的关系接好,数据更新了界面自然就更新了。

从代码层面看,对于原生开发而言,没有 ​​xml​​ 的布局,没有 ​​storyboard​​,布局完全由代码完成,所见即所得,同时也不会需要操作界面“对象”去进行赋值和更新,你所需要做的就是配置数据和界面的关系。

响应式开发比数据绑定或者 MVVM 不同的地方是,它每次都是重新构建和调整整个渲染树,而不是简单的对 UI 进行 ​​visibility​​ 操作。

Widget

​​Widget​​ 是 Flutter 里的基础概念,也是我们写代码最直接接触的对象,Flutter 内一切皆 Widget ,Widget 是不可变的(immutable),每个 Widget 状态都代表了一帧。

所以 ​​Widget​​​ 作为一个 ​​immutable​​ 对象,它不可能是真正工作的 UI 对象,在 Flutter 里真正的 ​​View​​ 级别对象是 ​​Element​​ 和 ​​RenderObject​​ , 其中 ​​Element​​ 的抽象对象就是我们经常用到的 ​​BuildContext​​。

Flutter 中 ​​Widget​​ 更多只是配置文件的地位,用于描述界面的配置代码,

状态管理

Flutter 作为响应式开发框架,本质上它其实不再追求什么 MVC 、MVP、MVVVM 的设计模式,它更多是对界面状态的管理。

就是要抛弃以前在原生平台上,需要拿到 ​​View​​ 的对象,然后做对其进行 UI 设置这种思路。

Flutter 上更多需要管理数据的流向,比如:

  • 数据是从哪里发出,然后再到哪里消费;
  • 数据是单向还是双向;
  • 数据需要进过哪些中间转化;
  • 数据是从哪一层开始往下传递;
  • 数据绑定了哪些地方;
  • 如何实现多个地方的局部刷新;

因为对于界面来说,它只需要根据数据进行变化即可,我们不需要获取它去单独设置,所以 Flutter 中有各种数据管理和共享的框架,比较流行的有 ​​provider​​​ 、 ​​getx​​​ 、 ​​flutter_redex ​​​、​​flutter_mobx​​ 等等。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/rd_w_csdn/article/details/125873980

智能推荐

浅谈UDP(数据包长度,收包能力,丢包及进程结构选择)-程序员宅基地

文章浏览阅读210次。UDP数据包长度UDP数据包的理论长度udp数据包的理论长度是多少,合适的udp数据包应该是多少呢?从TCP-IP详解卷一第11章的udp数据包的包头可以看出,udp的最大包长度是2^16-1的个字节。由于udp包头占8个字节,而在ip层进行封装后的ip包头占去20字节,所以这个是udp数据包的最大理论长度是2^16-1-8-20=65507。然而这个只是udp数据包的最大理论长度。首..._udp接收数据 客户端处理时间太长

计算机毕业设计springboot基于JAVA的房产销售管理系统tf4839【附源码+数据库+部署+LW】-程序员宅基地

文章浏览阅读123次。选题背景:房地产行业一直是国民经济的重要支柱之一,对于城市发展和居民生活质量有着重要影响。为了提高房地产销售管理的效率和精度,基于JAVA的房产销售管理系统的设计与实现具有重要的背景意义。通过建立一个全面、高效的房产销售管理系统,可以帮助房地产企业更好地管理销售流程、优化资源配置,提升销售业绩和客户满意度。选题意义:首先,基于JAVA的房产销售管理系统可以提高销售流程的效率和精度。传统的房地产销售过程中,涉及到大量的信息记录、数据分析和报表生成等工作,需要耗费大量的人力和时间。而通过房产销售管理系统

3.3 SPI串行Flash配置模式-程序员宅基地

文章浏览阅读757次。SPI串行Flash配置模式1.SPI串行配置介绍串行Flash的特点是占用管脚比较少,作为系统的数据存贮非常合适,一般都是采用串行外设接口(SPI 总线接口)。Flash 存贮器与EEPROM根本不同的特征就是EEPROM可以按字节进行数据的改写,而Flash只能先擦除一个区间,然后改写其内容。一般情况下,这个擦除区间叫做扇区(Sector),也有部分..._外挂spi flash

monogame Unable to load DLL 'openal32.dll': The specified module could not be found_load openal32-程序员宅基地

文章浏览阅读2.5k次。monogame 新建一个项目,运行报错 Unable to load DLL 'openal32.dll': The specified module could not be found解决方法游戏需要安装下列运行库Game For Windows Live 3.0http://download.microsoft.com/download/8/1/D_load openal32

mac 修改mysql端口_mac下的一些mysql操作-程序员宅基地

文章浏览阅读1.2k次。#一、从终端进入mysql不同于windows下的mysql。mac下的mysql安装路径不同,所以操作上会略有不同;以下操作以默认安装mysql为前提。##一(1):打开终端后,先设置路径,后面就不用每步操作都指定路径了(大小写区分):输入:PATH=“$PATH”:/usr/local/mysql/bin回车确认;再输入:mysql -uroot -p;(从此开始的操作都与windows版的一..._mac mysql5.5修改端口号

Java stream操作toMap总结_stream tomap-程序员宅基地

文章浏览阅读1.3w次,点赞10次,收藏23次。1、map 对象本身,重复的key,放入List。Map<String, List<Working>> map = workings.stream().collect(Collectors.toMap(Working::getInvoicePage, e -> { ArrayList<Working> list = new Arr_stream tomap

随便推点

maven 如何查看jar在哪个pom引入_maven查看jar包从哪个pom引入-程序员宅基地

文章浏览阅读4.8k次,点赞3次,收藏6次。窗口,然后选中项目(非项目不会出现该图标),再点击查看依赖关系图 Icon,如图所示。2、进入该页面进行 Ctrl + F 搜索需要的 Jar 名称。3、我这里以“hutool” jar为例。5、此时就会跳转到对应的 pom 坐标。4、找到并双击该高亮地方。_maven查看jar包从哪个pom引入

handsontable合并项mergeCells应用及扩展-程序员宅基地

文章浏览阅读782次。由于我这个项目主要是配置多表头信息,主要使用了handsontabel合并项功能。但是,在该功能使用过程中发现了一些问题和一些自己根据需要做的一些扩展 $("#topFieldDiv").handsontable({ data: data, colHeaders: colHeadArr,//设置列头 manualRowRe..._handsontable mergecells

Object.requireNonNull_objects.requirenonnull-程序员宅基地

文章浏览阅读4.3k次,点赞3次,收藏6次。Object.requireNonNullObject.requireNonNull介绍java8中的优化写法Object.requireNonNull源码Object.requireNonNull介绍Object.requireNonNull是用于参数有效性检查的API。使用Object.requireNonNull方法的好处在于可以显式的指定在哪里抛出异常。举个栗子public class Foo { private List<Bar> bars; public Foo(Lis_objects.requirenonnull

python提取pdf中图片和文本_python原生代码,提取pdf图片中的文字-程序员宅基地

文章浏览阅读734次。【代码】python提取pdf中图片和文本。_python原生代码,提取pdf图片中的文字

计算机二级office考试题库操作题,计算机二级考试MSOffice考试题库ppt操作题附答案...-程序员宅基地

文章浏览阅读2.1k次。请在【答题】菜单下选择【进入考生文件夹】命令,并按照题目要求完成下面的操作。 注意:以下的文件必须保存在考生文件夹下文慧是新东方学校的人力资源培训讲师,负责对新入职的教师进行入职培训,其PowerPoint演示文稿的制作水平广受好评。最近,她应北京节水展馆的邀请,为展馆制作一份宣传水知识及节水工作重要性的演示文稿。节水展馆提供的文字资料及素材参见\水资源利用与节水(素材).docx\,制作..._标题页包含演示主题,制作单位和日期

unity 启动相机_Unity3D研究院之打开照相机与本地相册进行裁剪显示(三十三)...-程序员宅基地

文章浏览阅读255次。最近做项目需要用到这个功能,就是在Unity中调用Android本地相册或直接打开摄像机拍照并且裁剪一部分用于用户头像,今天研究了一下,那么研究出成果了MOMO一定要分享给大家。Unity与Android的交互还有谁不会?? 如果有不会的朋友请看MOMO之前的文章喔,Unity3D研究院之打开Activity与调用JAVA代码传递参数(十八)这里有关交互的方式就不详细说明,主要将如何在Unity中..._unity打开照相机与本地相册进行裁剪

推荐文章

热门文章

相关标签