写点什么

flutter 系列之:flutter 架构什么的, 看完这篇文章就全懂了

作者:程序那些事
  • 2022 年 5 月 30 日
  • 本文字数:6701 字

    阅读完需:约 22 分钟

flutter系列之:flutter架构什么的,看完这篇文章就全懂了

简介

Flutter 是 google 开发的一个跨平台的 UI 构建工具,flutter 目前最新的版本是 3.0.1。使用 flutter 你可以使用一套代码搭建 android,IOS,web 和 desktop 等不同平台的应用。做到一次编写到处运行的目的。


说到一次编写处处运行,大家可能会想到 java。那么 flutter 跟 java 是不是类似呢?


对于 JAVA 来说,在编写完 JAVA 代码之后,将其编译成为 class 字节码,然后这个 class 字节码就可以不需要进行任何转换的在任何平台上运行。其底层原理是 JAVA 开发了适配不同操作系统和平台的 JVM,class 实际运行在 JVM 中,所以对底层到底运行在哪个平台是无感的。一切的适配都是由 JVM 来执行的。


Flutter 其实更像是 C 或者 C++,虽然代码是一样的,但是需要根据不同的平台编译成不同的二进制文件。而 Flutter 也是一样,虽然我们使用同一套 dart 代码编写了 Flutter 程序,但是需要不同的命令编译成不同平台的命令和安装包。


当然,在开发过程中,flutter 提供了虚拟机,实现了 hot reload 的功能,在代码进行修改之后,可以立刻重载,而不需要重新编译整个代码。


FLutter 这么神奇,那么它到底是怎么工作的呢?

Flutter 的架构图

我们先来看下 Flutter 的架构图,当然这个架构图是官方来的,官方的架构图表示的是权威:



从上图中,我们可以看到 Flutter 的架构可以分为三部分,从下到上分别是 embedder,Engine 和 Framework。

embedder

embedder 可以称为嵌入器,这是和底层的操作系统进行交互的部分。因为 flutter 最终要将程序打包到对应的平台中,所以这个嵌入器需要和底层的平台接口进行交互。


具体而言,对于 Android 平台使用的是 Java 和 C++,对于 iOS 和 macOS 平台,使用的是 Objective-C/Objective-C++,对应 Windows 平台和 Linux 平台的是 C++。


为什么 C++这么强大? 这里就可以看出来了,基本上所有底层的东西都是用 C++写的。


回到 embedder,为什么叫做嵌入器呢?这是因为 Flutter 打包的程序,可以作为整个应用程序,也可以作为现有程序的一部分被嵌入使用。

engine

engine 也叫做 flutter engine,它是 flutter 中最核心的部分。


Flutter engine 基本上使用 C++写的。engine 的存在是为了支持 Dart Framework 的运行。它提供了 Flutter 的核心 API,包括作图、文件操作、网络 IO、dar 运行时环境等核心功能。


engine 主要是通过 dart:ui 暴露给 Flutter framework 层的。

Flutter framework

这一层是用户编程的接口,我们的应用程序需要和 Flutter framework 进行交互,最终构建出一个应用程序。


Flutter framework 主要是使用 dart 语言来编写的。


framework 从下到上,我们有最基础的 foundational 包,和构建在其上的 animation, painting 和 gestures 。


再上面就是 rendering 层,rendering 为我们提供了动态构建可渲染对象树的方法,通过这些方法,我们可以对布局进行处理。


接着是 widgets layer,它是 rendering 层中对象的组合,表示一个小挂件。


最后是 Material 和 Cupertino 库,这些库使用 widegts 层中提供的小部件,组合成了不同风格的控件集。


Flutter framework 就是这样一层层的构建起来的。


当然,上面的 embedder 和 engine 属于比较底层的东西,我们只需要知道 Flutter 有这么一个东西,是这么使用的即可。


真正和我们程序员相关的,就是 Flutter framework 了。因为我们在编写代码的过程中,需要和 Flutter framework 打交道。


接下来,我们重点关注下 Flutter framework 中的几个核心部分。

Widgets

Widgets 翻译成中文就是小插件的意思。Widgets 是 Flutter 中用户界面的基础。你在 flutter 界面中能够观察到的用户界面,都是 Widgets。


当然这些大的 Widgets 又是由一个个的小的 Widgets 组成的,而这些小的 Widgets 又是由更小的 Widgets 组成的。


这样就构成了 Widgets 的层次依赖结构,这些层次结构的关联关系是通过 Widget 中的 child Widget 进行关联的。


在这种层次结构中,子 Widgets 可以共享父 Widgets 的上下文环境。


Flutter 中的 Widgets 跟其他语言中的类似的 Widgets 组合有什么不同呢?


他们最大的不同是,Flutter 中的 Widgets 更多,每个 Widgets 专注的功能更小。即便是一个很小很小功能,在 Flutter 中都可以找到与之对应的 Widgets。


这样做的好处就是,你可以使用不同的,非常基础的 Widgets 任意组合,从而构建出非常复杂的,个性化的大的 Widgets。


当然,它的缺点也非常明显,就是代码里面的 Widgets 太多了,导致代码中的层级结构特别的多,可能会看的眼花缭乱。


举个简单的例子,Container 是 flutter 提供的一个基本的容器 Widget,我们通常这样来使用它:


 Container(   constraints: BoxConstraints.expand(     height: Theme.of(context).textTheme.headline4!.fontSize! * 1.1 + 200.0,   ),   padding: const EdgeInsets.all(8.0),   color: Colors.blue[600],   alignment: Alignment.center,   child: Text('Hello World',     style: Theme.of(context)         .textTheme         .headline4!         .copyWith(color: Colors.white)),   transform: Matrix4.rotationZ(0.1), )
复制代码


我们向 Container 中传入了 constraints,padding,color,alignment,child,transform 等信息。


我们先来猜一下,这些信息中,哪些是用来构建 Widget 的?


大家第一时间想到的应该是 child,它本身就是一个 Widget,用来表示 Container 中包含的子对象,这个很好理解。


但是,除了 child 这个 Widget 之外,其他的 constraints,padding,color,alignment,transform 等都是构成 Widget 的元素!


我们来看下 Container 的 build 方法:


 Widget build(BuildContext context) {    Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) { current = LimitedBox( maxWidth: 0.0, maxHeight: 0.0, child: ConstrainedBox(constraints: const BoxConstraints.expand()), ); }
if (alignment != null) current = Align(alignment: alignment!, child: current);
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration; if (effectivePadding != null) current = Padding(padding: effectivePadding, child: current);
if (color != null) current = ColoredBox(color: color!, child: current);
if (clipBehavior != Clip.none) { assert(decoration != null); current = ClipPath( clipper: _DecorationClipper( textDirection: Directionality.maybeOf(context), decoration: decoration!, ), clipBehavior: clipBehavior, child: current, ); }
if (decoration != null) current = DecoratedBox(decoration: decoration!, child: current);
if (foregroundDecoration != null) { current = DecoratedBox( decoration: foregroundDecoration!, position: DecorationPosition.foreground, child: current, ); }
if (constraints != null) current = ConstrainedBox(constraints: constraints!, child: current);
if (margin != null) current = Padding(padding: margin!, child: current);
if (transform != null) current = Transform(transform: transform!, alignment: transformAlignment, child: current);
return current!; }
复制代码


从代码中可以看到,Container 先是创建了 LimitedBox,然后将其嵌入到 Align 中,再依次嵌入到 Padding,ColoredBox,ClipPath,DecoratedBox,ConstrainedBox,Padding 和 Transform 中。这些所有的对象都是 Widget。


这里应该可以理解 Flutter 中 Widget 的设计思想了。在 Flutter 中一切皆可为 Widget。

Widgets 的可扩展性

和其他的编译成原生语言特性的跨平台实现如 React native 相比,Flutter 对于每个 UI 都有自己的实现,而不是依赖于操作系统提供的接口。


这样做的好处就是一切都是由 Flutter 自己控制的,使用者可以在 Flutter 的基础上进行无限扩展,而不用受限于系统底层的实现限制。


另一方面,这样可以减少 Flutter 在呈现过程中在 Flutter 代码和平台代码之间来回转换,减少了性能瓶颈,提升效率。


最后,因为 UI 的实现和底层的操作系统是分离的,所以 Flutter 的 APP 在不同的平台上面可以有统一的外观和实现,可以保证风格的统一。

Widgets 的状态管理

Widgets 表示的是不可变的用户 UI 界面结构。虽然结构是不能够变化的,但是 Widgets 里面的状态是可以动态变化的。


根据 Widgets 中是否包含状态,Widgets 可以分为 stateful 和 stateless widget,对应的类是 StatefulWidget 和 StatelessWidget。


对于有些 Widgets 来说,比如 icon 或者 Label,它里面本身就不需要状态,这些 Widgets 就是 StatelessWidget。


但是如果有些 Widgets 中的某些内容可能需要根据用户或者其他原因来动态变化,则就需要使用 StatefulWidget。


之前提到了 Widgets 是不可变的,StatefulWidget 中的可变数据是存放在对应的 State 中的,所以 StatefulWidgets 本身并没有 build 方法,所有用户界面都是通过 State 对象来构建的。


当 State 发生变化的时候,需要调用 setState() 方法来通知 flutter 框架来调用 State 的 build 方法,从而将变化反馈到用户界面中。


既然 StatefulWidget 是带有状态的,那么这些状态是怎么进行管理和传递的呢?


State 本身提供了一个 build 方法,用于构建初始的状态:


Widget build(BuildContext context);
复制代码


如果在一个 StatefulWidget 中需要嵌入另外一个 StatefulWidget,那么可以在其对应的 State 中调用另外一个 StatefulWidget 的构造函数,将要传递的数据,以构造函数参数的形式传递给子 Widget。


当然这样做是没问题的。但是如果组件的嵌套层数过多的话,这种构造函数的传递方式,显然不能满足我们的需求。


于是 Flutter 提供了一个 InheritedWidget 类,如果我们自定义的类需要共享数据给子 Widgets,则可以继承 InheritedWidget。


Inherited widgets 有两个作用: 第一,子 Widget 可以通过 Inherited widgets 提供的静态 of 方法拿到离他最近的父 Inherited widgets 实例。


第二,当 Inherited widgets 改变 state 之后,会自动触发 state 消费者的 rebuild 行为。


先来看一下 inherited widgets 类的定义:


abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ Key? key, required Widget child }) : super(key: key, child: child);
@override InheritedElement createElement() => InheritedElement(this);
@protected bool updateShouldNotify(covariant InheritedWidget oldWidget);}
复制代码


可以看到 InheritedWidget 是对实际 Widget 对象的代理,另外还将 InheritedWidget 封装到了 InheritedElement 中。


这里不多讲解 InheritedElement,InheritedElement 是底层通知机制的实现。


我们看到 InheritedWidget 还添加了一个 updateShouldNotify,这个方法可以提供给我们控制当前 InheritedWidget rebuilt 的时候,是否需要 rebuilt 继承它的子 Widget。


下面我们看一个 InheritedWidget 的具体实现:


class FrogColor extends InheritedWidget {  const FrogColor({    Key? key,    required this.color,    required Widget child,  }) : super(key: key, child: child);
final Color color;
static FrogColor of(BuildContext context) { final FrogColor? result = context.dependOnInheritedWidgetOfExactType<FrogColor>(); assert(result != null, 'No FrogColor found in context'); return result!; }
@override bool updateShouldNotify(FrogColor old) => color != old.color;}
复制代码


FrogColor 中定义了一个 Color 属性,当 Color 发生变化的时候,就会调用 updateShouldNotify。


另外,FrogColor 还提供了一个 of 方法,接受的参数是 BuildContext,然后调用 context.dependOnInheritedWidgetOfExactType 去查找离该 context 最近的 FrogColor。


为什么要使用 of 方法对 context.dependOnInheritedWidgetOfExactType 进行封装呢?这是因为,context.dependOnInheritedWidgetOfExactType 方法不一定能够找到要找的对象,所以我们需要进行一些异常值的处理。


另外,有可能 of 方法返回的对象和 context.dependOnInheritedWidgetOfExactType 中查找的对象不一样,这都是可以的。


我们看下 of 方法的具体使用:


class MyPage extends StatelessWidget {  const MyPage({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( body: FrogColor( color: Colors.green, child: Builder( builder: (BuildContext innerContext) { return Text( 'Hello Frog', style: TextStyle(color: FrogColor.of(innerContext).color), ); }, ), ), ); }}
复制代码


还有一个问题,of 方法传入的是 BuildContext 对象,注意,这里的 BuildContext 必须是 InheritedWidget 对象本身的后辈,也就是说在对象树中,必须是 InheritedWidget 的子树。再看下面的例子:


class MyOtherPage extends StatelessWidget {  const MyOtherPage({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( body: FrogColor( color: Colors.green, child: Text( 'Hello Frog', style: TextStyle(color: FrogColor.of(context).color), ), ), ); }}
复制代码


这个例子中,FrogColor.of 方法中的 context 是 FrogColor 的父 context,所以是找不到 FrogColor 对象的,这样的使用是错误的。


当然,除了 InheritedWidget,Flutter 还提供了很多状态管理的工具,比如 provider,bloc,flutter_hooks 等,也是非常好用的。

渲染和布局

渲染就是将上面我们提到的 widgets 转换成用户肉眼可以感知的像素的过程。


Flutter 作为一种跨平台的框架,它和普通的跨平台的框架或者原生的框架有什么区别呢?


首先来考虑一下原生框架。以 android 为例,首先调用的是 andorid 框架的 java 代码,通过调用 android 系统库提供的进行绘制的组件,最后调用底层的 Skia 来进行绘制。Skia 是一种用 C/C++ 编写的图形引擎,它调用 CPU 或 GPU 在设备上完成绘制。


那么常见的跨平台框架是怎么运行的呢?它们实际上在原生的代码框架上面又封装了一层。通常使用 javascript 这样的解释性语言来进行编写,然后编写的代码再和 andorid 的 JAVA 或者 IOS 的 Objective-C 系统库进行交互。这样的结果就是在 UI 交互或者调用之间会造成显著的性能开销。这也就是通用的跨平台语言不如原生的性能好的原因。


但是 flutter 不一样,它并不是用系统自带的 UI 控件,而是拥有自己的实现。Flutter 代码会直接被编译成使用 Skia 进行渲染的原生代码,从而提升渲染效率。


接下来,我们具体看一下 flutter 从代码到渲染的整个流程。首先看一段代码:


Container(  color: Colors.blue,  child: Row(    children: [      Image.network('http://www.flydean.com/1.png'),      const Text('A'),    ],  ),);
复制代码


上面的代码是构建一个 Container widget。当 flutter 想要渲染这个 widget 的时候,会去调用 build() 方法,然后生成一个 widget 集合.


为什么是 Widget 集合呢?在上面我们也分析过,Container 这个 widget 是由很多个其他的 widget 组成的,所以,上面的 Container 会生成下面的 widget 树:



上面的就是代码中生成的 widget,这些 widget 在 build 的过程中,会被转换为 element tree。一个 element 和一个 widget 对应。


element 表示的 widget 的实例。flutter 中有两种类型的 element,分别是:ComponentElement 和 RenderObjectElement.


ComponentElement 是其他 Element 的容器,而 RenderObjectElement 是真正参与 layout 和渲染的 element。


因为 Widget 本身是不可变的,所以任何对于 Widget 的修改都会返回一个新的 Widget。那么是不是所有的变动,都会导致整个 element tree 重新渲染呢?


答案是不会的,flutter 仅会重新渲染需要被重新绘制的 element。


接下来,我们看下渲染树是怎么构建的,渲染树中的每个元素叫做 RenderObject,它定义了布局和绘制的抽象模型。


上面我们提到的 RenderObjectElement 会在渲染的时候转换成为 RenderObject,如下所示:



当然,不同的 Render element 会转换成为不同的 Render 对象。

总结

Widget 和 Layout 是我们实际在做 flutter 开发的时候,经常需要使用到的部分,大家需要深入了解和熟练掌握。


更多内容请参考 http://www.flydean.com/01-flutter-architectural/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 15 小时前阅读数: 25
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
flutter系列之:flutter架构什么的,看完这篇文章就全懂了_flutter_程序那些事_InfoQ写作社区