写点什么

深入解析 Flutter 架构

用户头像
Android架构
关注
发布于: 33 分钟前

Reactive user interfaces

从表面上看,Flutter 是一个被动的、伪声明式的 UI 框架,开发者提供一个从应用状态到界面状态的映射,当应用状态发生变化时,框架在运行时承担更新界面的任务。这种模式的灵感来自于 Facebook 为自己的 React 框架所做的工作,其中包括对很多传统设计原则的重新思考。


在大多数传统的 UI 框架中,用户界面的初始状态被描述一次,然后由用户代码在运行时响应事件单独更新。这种方法的一个挑战是,随着应用程序的复杂性增加,开发人员需要意识到状态变化如何在整个 UI 中级联。例如,考虑以下 UI。



有很多地方可以改变状态:颜色框、色调滑块、单选按钮。当用户与用户界面交互时,变化必须反映在其他每个地方。更糟糕的是,除非小心翼翼,否则对用户界面的一个部分的微小改变可能会对看似不相关的代码产生涟漪效应。


一种解决方案是像 MVC 这样的方法,通过控制器将数据变化推送到模型,然后模型通过控制器将新的状态推送到视图。然而,这也是有问题的,因为创建和更新 UI 元素是两个独立的步骤,很容易不同步。


Flutter 与其他反应式框架一样,采用了另一种方法来解决这个问题,通过明确地将用户界面与其底层状态解耦。使用 React 风格的 API,你只需要创建 UI 描述,而框架则负责使用这一个配置来创建和/或适当更新用户界面。


在 Flutter 中,widget(类似于 React 中的组件)由不可变的类来表示,这些类用于配置对象树。这些 widgets 用于管理单独的对象树进行布局,然后用于管理单独的对象树进行合成。Flutter 的核心是一系列机制,用于有效地行走树的修改部分,将对象树转换为低级对象树,并在这些树上传播变化。


一个 widget 通过覆盖 build()方法来声明其用户界面,build()方法是一个将状态转换为 UI 的函数。


UI = f(state)


build()方法在设计上是快速执行的,并且应该没有副作用,允许框架在任何需要的时候都可以调用它(有可能每渲染一帧就调用一次)。


这种方法依赖于语言运行时的某些特性(特别是快速对象实例化和删除)。幸运的是,Dart 特别适合这个任务。

Widgets

如前所述,Flutter 强调 widget 是一个组成单位。Widget 是 Flutter 应用的用户界面的构件,每个 widget 都是用户界面的一部分不可改变的声明。


小组件形成了一个基于组成的层次结构。每个 widget 都嵌套在它的父体内部,并且可以从父体接收上下文。这种结构一直延续到根 widget(承载 Flutter 应用的容器,通常是 MaterialApp 或 CupertinoApp),正如这个琐碎的例子所示。


import 'package:flutter/material.dart';


在前面的代码中,所有实例化的类都是 widget。


应用程序通过告诉框架用另一个 widget 替换层次结构中的一个 widget 来响应事件(如用户交互)更新用户界面。然后,框架会比较新旧 widget,并有效地更新用户界面。


Flutter 对每个 UI 控件都有自己的实现,而不是服从于系统提供的控件:例如,iOS Switch 控件和 Android 对应的控件都有一个纯 Dart 的实现。


这种方法提供了几个好处:


  • 提供了无限的可扩展性。开发者如果想要 Switch 控件的变体,可以以任意方式创建一个,而不局限于操作系统提供的扩展点。

  • 通过允许 Flutter 一次性合成整个场景,避免了显著的性能瓶颈,而无需在 Flutter 代码和平台代码之间来回过渡。

  • 将应用行为与任何操作系统的依赖关系解耦。应用程序在所有版本的操作系统上看起来和感觉是一样的,即使操作系统改变了其控件的实现。

Composition

小部件通常由许多其他小的、单一用途的小部件组成,这些小部件组合起来可以产生强大的效果。


在可能的情况下,设计概念的数量保持在最低限度,同时允许总词汇量很大。例如,在 widgets 层中,Flutter 使用相同的核心概念(一个 Widget)来表示绘制到屏幕上、布局(定位和大小)、用户交互性、状态管理、主题、动画和导航。在动画层,一对概念 Animations 和 Tweens 覆盖了大部分的设计空间。在渲染层中,RenderObjects 用于描述布局、绘画、命中测试和可访问性。在每一种情况下,对应的词汇量最终都会很大:有数百个 widgets 和渲染对象,以及几十种动画和 Tweens 类型。


类的层次结构是刻意的浅而宽,以最大限度地增加可能的组合数量,专注于小型的、可组合的 widgets,每个 widgets 都能做好一件事。核心功能是抽象的,即使是基本的功能,如 padding 和 align,也是作为单独的组件实现的,而不是内置在核心中。(这也与传统的 API 形成了鲜明的对比,在传统的 API 中,像 padding 这样的功能是内置于每个布局组件的通用核心中的。)。所以,举例来说,要让一个小组件居中,而不是调整一个名义上的 Align 属性,你可以把它包裹在一个 Center 小组件中。


有用于填充、对齐、行、列和网格的小组件。这些布局部件没有自己的视觉表示。相反,它们的唯一目的是控制另一个部件的布局的某些方面。Flutter 还包括利用这种组合方法的实用工具部件。


例如,Container,一个常用的 widget,是由几个 widget 组成的,负责布局,绘画,定位和大小。具体来说,Container 是由 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform 小组件组成的,你可以通过阅读它的源代码看到。Flutter 的一个定义特性是,你可以钻进任何一个 widget 的源头并检查它。所以,你可以用新奇的方式将它和其他简单的 widget 组合起来,或者直接用 Container 作为灵感创建一个新的 widget,而不是通过子类 Container 来产生自定义的效果。

Building widgets

如前所述,您通过重载 build()函数来确定 widget 的视觉表现,以返回一个新的元素树。这个树以更具体的方式表示小组件在用户界面中的部分。例如,一个工具条小组件可能有一个构建函数,它返回一些文本和各种按钮的水平布局。根据需要,框架会递归地要求每个小组件进行构建,直到树完全由具体的可渲染对象来描述。然后,框架将这些可渲染对象缝合到一个可渲染对象树中。


一个 widget 的构建函数应该是没有副作用的。每当函数被要求构建时,widget 应该返回一个新的 widgets 树 1,不管 widget 之前返回的是什么。框架会做繁重的工作,根据渲染对象树来决定哪些构建方法需要被调用(后面会详细介绍)。关于这个过程的更多信息可以在 Inside Flutter 主题中找到。


在每个渲染帧上,Flutter 可以通过调用该 widget 的 build()方法,仅仅重新创建 UI 中状态已经改变的部分。因此,构建方法应该快速返回,重计算工作应该以某种异步方式完成,然后作为状态的一部分存储起来,供构建方法使用,这一点非常重要。


虽然这种自动对比的方法比较幼稚,但却相当有效,可以实现高性能、交互式的应用。而且,构建函数的设计通过专注于声明一个 widget 是由什么组成的,而不是将用户界面从一个状态更新到另一个状态的复杂性来简化你的代码。

Widget state

该框架引入了两大类 widget:有状态和无状态 widget。


许多 widget 没有可改变的状态:它们没有任何随时间变化的属性(例如,一个图标或一个标签)。这些 widget 是 StatelessWidget 的子类。


然而,如果一个小组件的独特特性需要根据用户交互或其他因素而改变,那么该小组件是有状态的。例如,如果一个小组件有一个计数器,每当用户点击一个按钮时就会递增,那么计数器的值就是该小组件的状态。当该值发生变化时,该小组件需要重新构建以更新其 UI 部分。这些 widget 是 StatefulWidget 的子类,(因为 widget 本身是不可变的)它们将可变的状态存储在一个单独的 State 子类中。StatefulWidgets 没有构建方法;相反,它们的用户界面是通过 State 对象构建的。


每当你突变一个 State 对象时(例如,通过递增计数器),你必须调用 setState()来向框架发出信号,通过再次调用 State 的构建方法来更新用户界面。


拥有独立的状态和 widget 对象,让其他 widget 以完全相同的方式对待无状态和有状态的 widget,而不用担心丢失状态。父对象不需要紧紧抓住一个子对象来保存它的状态,而是可以在任何时候创建一个新的子对象实例而不会丢失子对象的持久化状态。框架会在适当的时候完成所有寻找和重用现有状态对象的工作。

State management

那么,如果许多 widget 可以包含状态,那么如何管理状态并在系统中传递呢?


和其他类一样,你可以在 widget 中使用构造函数来初始化它的数据,所以 build()方法可以确保任何子 widget 被实例化时都有它需要的数据。


@override


然而,随着小组件树的深入,在树的层次结构中上下传递状态信息变得很麻烦。因此,第三种小组件类型 InheritedWidget 提供了一种从共享祖先中抓取数据的简单方法。您可以使用 InheritedWidget 来创建一个状态小组件,该小组件在小组件树中包装一个共同的祖先,如本例所示。



每当一个 ExamWidget 或 GradeWidget 对象需要来自 StudentState 的数据时,它现在可以通过一个命令来访问它,例如


final studentState = StudentState.of(context);


of(context)调用接收构建上下文(当前小组件位置的句柄),并返回树中与 StudentState 类型匹配的最近的祖先。InheritedWidgets 还提供了一个 updateShouldNotify()方法,Flutter 调用该方法来决定状态变化是否应该触发使用它的子部件的重建。


Flutter 本身广泛使用 InheritedWidget 作为共享状态框架的一部分,例如应用程序的视觉主题,其中包括颜色和类型样式等属性,这些属性在整个应用程序中是普遍存在的。MaterialApp build()方法在构建时,会在树中插入一个主题,然后在更深的层次结构中,一个 widget 可以使用.of()方法来查找相关的主题数据,例如。


Container(


这种方法也用于提供页面路由的 Navigator 和提供访问屏幕指标(如方向、尺寸和亮度)的 MediaQuery。


随着应用程序的增长,更先进的状态管理方法,减少了创建和使用有状态小部件的仪式,变得更有吸引力。许多 Flutter 应用程序使用了像 provider 这样的实用程序包,它提供了一个围绕 InheritedWidget 的包装器。Flutter 的分层架构也使其他方法能够实现状态到 UI 的转换,例如 flutter_hooks 包。

Rendering and layout

本节介绍了渲染管道,这是 Flutter 将小组件的层次结构转换为实际像素画到屏幕上的一系列步骤。

Flutter’s rendering model

你可能想知道:如果 Flutter 是一个跨平台框架,那么它怎么能提供与单平台框架相当的性能呢?


从传统的 Android 应用的工作方式开始思考是很有用的。绘图时,首先调用 Android 框架的 Java 代码。Android 系统库提供了负责自己绘图的组件,将其转化为 Canvas 对象,然后 Android 可以用 Skia 渲染,Skia 是一个用 C/C++编写的图形引擎,调用 CPU 或 GPU 在设备上完成绘图。


跨平台框架的工作方式通常是在底层的原生 Android 和 iOS UI 库上创建一个抽象层,试图平滑每个平台表示方式的不一致。App 代码通常是用 JavaScript 等解释语言编写的,而 JavaScript 又必须与基于 Java 的 Android 或基于 Objective-C 的 iOS 系统库进行交互以显示 UI。所有这些都会增加大量的开销,特别是在 UI 和应用逻辑之间有大量交互的地方。


相比之下,Flutter 最大限度地减少了这些抽象,绕过系统 UI 小部件库而使用自己的小部件集。绘制 Flutter 视觉效果的 Dart 代码被编译成本地代码,使用 Skia 进行渲染。Flutter 还嵌入了自己的 Skia 副本作为引擎的一部分,允许开发者升级他们的应用程序,以保持最新的性能改进,即使手机还没有更新新的 Android 版本。其他原生平台上的 Flutter 也是如此,比如 iOS、Windows 或 macOS。

From user input to the GPU

Flutter 适用于其渲染管道的首要原则是:简单就是快。Flutter 对于数据如何流向系统有一个简单明了的管道,如下顺序图所示。



让我们来看看这些阶段的一些细节。

Build: from Widget to Element

考虑这个简单的代码片段,它演示了一个简单的小组件层次结构。


Container(


当 Flutter 需要渲染这个片段时,它会调用 build()方法,该方法会返回一个 widgets 的子树,根据当前应用状态渲染 UI。在这个过程中,build()方法可以根据需要,根据其状态引入新的 widgets。举个简单的例子,在前面的代码片段中,Container 有颜色和子属性。通过查看 Container 的源码,可以看到,如果颜色不是 null,它就会插入一个代表颜色的 ColoredBox。


if (color != null)


相应地,图像和文本小组件可能会在构建过程中插入子小组件,如 RawImage 和 RichText。因此,最终的小组件层次结构可能比代码所表示的更深,如本例 2。



这就解释了为什么当你通过调试工具(如 Dart DevTools 的一部分 Flutter 检查器)检查这个树时,你可能会看到一个比你的原始代码更深的结构。


在构建阶段,Flutter 将代码中表达的 widget 翻译成相应的元素树,每个 widget 都有一个元素。每个元素都代表了一个小组件在树层次结构的特定位置的具体实例。元素有两种基本类型。


  • ComponentElement,是其他元素的宿主

  • RenderObjectElement,一个参与布局或绘制阶段的元素。



RenderObjectElements 是它们的 widget 类比和底层 RenderObject 之间的中介,我们稍后会提到。


任何 widget 的元素都可以通过它的 BuildContext 来引用,BuildContext 是 widget 在树中位置的句柄。这是一个函数调用中的上下文,比如 Theme.of(context),并作为参数提供给 build()方法。


因为 widget 是不可改变的,包括节点之间的父/子关系,对 widget 树的任何改变(例如在前面的例子中把 Text('A')改为 Text('B'))都会导致返回一组新的 widget 对象。但这并不意味着必须重建底层表示。元素树从一帧到另一帧都是持久的,因此起着关键的性能作用,允许 Flutter 在缓存其底层表示时,就像小组件层次结构是完全可处置的一样。通过只走过发生变化的 widget,Flutter 可以只重建元素树中需要重新配置的部分。

Layout and rendering

这将是一个很少见的只画一个小部件的应用。因此,任何 UI 框架的一个重要部分都是能够有效地布局 widget 的层次结构,在屏幕上渲染之前确定每个元素的大小和位置。


渲染树中每个节点的基类是 RenderObject,它定义了一个布局和绘画的抽象模型。这是极其通用的:它不承诺固定的尺寸数,甚至不承诺笛卡尔坐标系(通过这个极坐标系的例子来证明)。每个 RenderObject 都知道它的父体,但除了如何访问它们和它们的约束外,对它的子体几乎一无所知。这为 RenderObject 提供了足够的抽象性,能够处理各种用例。


在构建阶段,Flutter 为元素树中的每个 RenderObjectElement 创建或更新一个继承自 RenderObject 的对象。RenderObjects 是基元。RenderParagraph 渲染文本,RenderImage 渲染图像,RenderTransform 在绘制其子元素之前应用一个变换。



大多数 Flutter widget 都是由一个继承自 RenderBox 子类的对象来渲染的,RenderBox 代表了一个在 2D 笛卡尔空间中固定大小的 RenderObject。RenderBox 提供了一个盒子约束模型的基础,为每个要渲染的 widget 建立了一个最小和最大的宽度和高度。


为了执行布局,Flutter 以深度优先的遍历方式走过渲染树,并将尺寸约束从父级传递到子级。在确定其大小时,子代必须尊重其父代给它的约束。子对象在父对象建立的约束条件下,通过向上传递尺寸来做出响应。



在这一单次走过树的结束时,每个对象都在其父约束内有一个定义的大小,并准备好通过调用 paint()方法来绘制。


箱子约束模型作为一种在 O(n)时间内布局对象的方法是非常强大的。


  • 父对象可以通过将最大和最小约束设置为相同的值来决定子对象的大小。例如,手机应用中最上面的渲染对象将其子对象约束为屏幕的大小。(子对象可以选择如何使用该空间。例如,他们可能只是将他们想要渲染的东西放在中心位置,并将其限制在规定的约束范围内)。)

  • 父母可以规定孩子的宽度,但给孩子高度上的灵活性(或规定高度但提供灵活的宽度)。现实世界中的一个例子是流式文本,它可能必须适合一个水平约束,但根据文本的数量而在垂直方向上变化。即使当一个子对象需要知道它有多少可用空间来决定如何渲染它的内容时,这个模型也能工作。通过使用 LayoutBuilder 小组件,子对象可以检查传递下来的约束条件,并使用这些约束条件来决定如何使用这些约束条件,例如。


Widget build(BuildContext context) {


关于约束和布局系统的更多信息,以及工作实例,可以在理解约束主题中找到。


所有 RenderObjects 的根是 RenderView,它代表渲染树的总输出。当平台要求渲染一个新的帧时(例如,因为 vsync 或因为纹理解压/上传完成),会调用 compositeFrame()方法,它是渲染树根部的 RenderView 对象的一部分。这将创建一个 SceneBuilder 来触发场景的更新。当场景完成后,RenderView 对象将合成的场景传递给 dart:ui 中的 Window.render()方法,该方法将控制权传递给 GPU 来渲染它。


管道的合成和光栅化阶段的进一步细节超出了本篇高级文章的范围,但更多的信息可以在这篇关于 Flutter 渲染管道的演讲中找到。

Platform embedding

正如我们所看到的,Flutter 的用户界面不是被翻译成等价的操作系统小部件,而是由 Flutter 自己构建、布局、合成和绘制。根据该平台独特的关注点,获取纹理和参与底层操作系统的应用生命周期的机制不可避免地有所不同。该引擎是平台无关的,呈现了一个稳定的 ABI(应用二进制接口),为平台嵌入者提供了一种设置和使用 Flutter 的方式。


平台嵌入器是承载所有 Flutter 内容的原生操作系统应用程序,并作为主机操作系统和 Flutter 之间的粘合剂。当你启动一个 Flutter 应用时,嵌入器提供入口点,初始化 Flutter 引擎,获取 UI 和光栅化的线程,并创建 Flutter 可以写入的纹理。嵌入器还负责应用程序的生命周期,包括输入手势(如鼠标、键盘、触摸)、窗口大小、线程管理和平台消息。Flutter 包括 Android、iOS、Windows、macOS 和 Linux 的平台嵌入器;你也可以创建一个自定义的平台嵌入器,就像这个工作实例一样,支持通过 VNC 风格的 framebuffer 来遥控 Flutter 会话,或者这个工作实例用于 Raspberry Pi。


每个平台都有自己的一套 API 和约束。一些针对平台的简要说明。


  • 在 iOS 和 macOS 上,Flutter 分别作为 UIViewController 或 NSViewController 加载到嵌入器中。平台嵌入器创建一个 FlutterEngine,作为 Dart 虚拟机和你的 Flutter 运行时的主机,以及一个 FlutterViewController,它连接到 FlutterEngine,将 UIKit 或 Cocoa 输入事件传递到 Flutter,并使用 Metal 或 OpenGL 显示 FlutterEngine 渲染的帧。

  • 在 Android 上,Flutter 默认是作为一个 Activity 加载到嵌入器中。视图由 FlutterView 控制,它根据 Flutter 内容的构成和 z-排序要求,将 Flutter 内容渲染为视图或纹理。

  • 在 Windows 上,Flutter 被托管在一个传统的 Win32 应用程序中,并使用 ANGLE 渲染内容,这是一个将 OpenGL API 调用转换为 DirectX 11 等价物的库。目前正在努力使用 UWP 应用模型提供 Windows 嵌入器,并通过 DirectX 12 以更直接的路径取代 ANGLE。

Integrating with other code

Flutter 提供了多种互操作性机制,无论你是要访问用 Kotlin 或 Swift 等语言编写的代码或 API,还是要调用基于 C 语言的原生 API,在 Flutter 应用中嵌入原生控件,或者在现有应用中嵌入 Flutter。

Platform ch

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


annels


对于移动和桌面应用,Flutter 允许你通过平台通道调用到自定义代码,这是一个简单的机制,用于在你的 Dart 代码和主机应用的平台特定代码之间进行通信。通过创建一个通用通道(封装名称和编解码器),你可以在 Dart 和用 Kotlin 或 Swift 等语言编写的平台组件之间发送和接收消息。数据从像 Map 这样的 Dart 类型序列化为标准格式,然后反序列化为 Kotlin(如 HashMap)或 Swift(如 Dictionary)中的等价表示。



以下是 Kotlin(Android)或 Swift(iOS)中 Dart 调用接收事件处理程序的一个简单平台通道示例。


// Dart side


// Android (Kotlin)


// iOS (Swift)


更多使用平台渠道的例子,包括 macOS 的例子,可以在 flutter/plugins 资源库中找到 3。另外,Flutter 已经有数千个插件,涵盖了很多常见的场景,从 Firebase 到广告,再到摄像头和蓝牙等设备硬件。

Foreign Function Interface

对于基于 C 的 API,包括那些可以为 Rust 或 Go 等现代语言编写的代码生成的 API,Dart 提供了一个直接的机制,使用 dart:ffi 库与本地代码绑定。外来函数接口(FFI)模型可以比平台通道快很多,因为不需要序列化来传递数据。相反,Dart 运行时提供了在由 Dart 对象支持的堆上分配内存的能力,并对静态或动态链接的库进行调用。FFI 适用于除 web 以外的所有平台,在这些平台上,js 包具有同等的作用。


要使用 FFI,你要为每个 Dart 和非托管方法签名创建一个 typedef,并指示 Dart VM 在它们之间进行映射。作为一个简单的例子,这里有一个调用传统的 Win32 MessageBox()API 的代码片段。


typedef MessageBoxNative = Int32 Function(

Rendering native controls in a Flutter app

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
深入解析Flutter架构