您现在的位置是:首页 >技术杂谈 >Flutter 笔记 | Flutter 核心原理(一)架构和生命周期网站首页技术杂谈

Flutter 笔记 | Flutter 核心原理(一)架构和生命周期

川峰 2024-06-17 10:48:31
简介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 的核心库实现,其中包括平台插件,例如 camerawebview ,以及和平台无关的功能,例如 animations

2. 引擎层

Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。

3. 嵌入层

Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。

通常来说,开发者不需要感知到EngineEmbedder的存在(如果不需要调用平台的系统服务),Framework是开发者需要直接交互的,因而也在整个分层架构模型的最上层。

Widget 接口

在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget 类的声明:

 // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key? key;

  
  @factory
  Element createElement();

  
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  
  
  bool operator ==(Object other) => super == other;

  
  
  int get hashCode => super.hashCode;

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  ...
}
  • @immutable 代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中 如果属性发生变化则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。
  • Widget类继承自DiagnosticableTreeDiagnosticableTree“诊断树”,主要作用是提供调试信息。
  • Key: 这个key属性类似于 React/Vue 中的key,主要的作用是决定是否在下一次build时复用旧的 widget ,决定的条件在canUpdate()方法中。
  • createElement():正如前文所述“一个 widget 可以对应多个Element”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(...) 是一个静态方法,它主要用于 在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用new widget去更新Element对象的配置,否则就会创建新的Element

在Flutter中,可以说万事万物皆Widget。哪怕一个居中的能力,在传统的命令式UI开发中,通常会以一个属性的方式进行设置,而在Flutter中则抽象成一个名为Center的组件。此外,Flutter提倡激进式的组合开发,即尽可能通过一系列基础的Widget构造出你的目标Widget

基于以上两个特点,Flutter的代码中将充斥着各种Widget,每一帧UI的更新都意味着部分Widget的重建。你可能会担心,这种设计是否过于臃肿和低效,其实恰恰相反,这正是Flutter能够进行高性能渲染的基石。Flutter之所以要如此设计也是基于以下两点事实有意而为之:

  • Widget Tree上的Widget节点越多,通过Diff算法得到的需要重建的部分就越精确、范围越小,而UI渲染的主要性能瓶颈就是Widget节点的重建。

  • Dart语言的对象模型GC模型小对象的快读分配和回收做了优化,而Widget正是这种小对象

Flutter中的三棵树

既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?

Flutter 框架的的处理流程是这样的:

  1. 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  2. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  3. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

真正的布局和渲染逻辑在 Render 树中,ElementWidgetRenderObject 的粘合剂,可以理解为一个中间代理。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其中三棵树的各自的作用是:

  • Widget:负责配置,为Element描述UI的配置信息,拥有公开的Api,这部分也是开发者可以直接感知和使用的部分。
  • Element:Flutter Virtual DOM 的管理者,它管理widget的生命周期,代表了内存中真实存在的UI数据,它负责树中特定位置的widget的实例化,持有widget的引用,管理树中的父子关系,实际上Widget TreeRenderObject Tree 都是由Element Tree驱动生成的。
  • RenderObject:负责处理大小、布局和绘制工作,它将绘制自身,摆放子节点等。

这里需要注意:

  • 三棵树中, ElementWidget是 一 一 对应的,但 ElementRenderObject 不是 一 一 对应的。比如 StatelessWidgetStatefulWidget 都没有对应的 RenderObject
  • 渲染树在上屏前会生成一棵 Layer 树,因此在Flutter中实际上是有四棵树,但我们只需了解以上三棵树就行。

StatelessWidget

StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法:


StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget

下面是一个简单的例子:

class Echo extends StatelessWidget  {
  const Echo({
    Key? key,  
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

然后我们可以通过如下方式使用它:

 Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

按照惯例,widget构造函数参数应使用命名参数,命名参数中的必需要传的参数要添加required关键字,这样有利于静态代码分析器进行检查;在继承 widget 时,第一个参数通常应该是Key。另外,如果 widget 需要接收子 widget ,那么childchildren参数通常应被放在参数列表的最后。同样是按照惯例, widget属性应尽可能的被声明为final,防止被意外改变。

Context

build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widgetwidget 树中的上下文,每一个 widget 都会对应一个 context 对象。

实际上,context是当前 widgetwidget 树中位置中执行”相关操作“的一个句柄(handle), 比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。

下面是在子树中获取父级 widget 的一个示例:

class ContextRoute extends StatelessWidget  {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

在这里插入图片描述

StatefulWidget

有了StatelessWidget,为什么还要有一个StatefulWidget呢?在Flutter中Widget都是不可变的, 但实际上需要根据对应的状态刷新Widget,于是就产生了StatefulWdiget, StatefulWidget是由2个对象WidgetState组成的。

StatefulWidget同样是继承自widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同,返回的是一个StatefulElement;另外StatefulWidget类中添加了一个新的接口createState()

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  
  StatefulElement createElement() => StatefulElement(this);
    
  
  State createState();
}
  • StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。开发者无需关心该方法。

  • createState() 用于创建和 StatefulWidget 相关的状态,开发者必须在子类中实现该方法。它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

而在StatefulWidget 中,State 对象和StatefulElement具有一 一对应的关系,所以在Flutter的SDK文档中,可以经常看到“从树中移除 State 对象”或“插入 State 对象到树中”这样的描述,此时的树指通过 widget 树生成的 Element 树。Flutter 的 SDK 文档中经常会提到“树” ,我们可以根据语境来判断到底指的是哪棵树。其实,对于用户来说我们无需关心到底是哪棵树,无论哪种树其最终目标都是为了描述 UI 的结构和绘制信息,我们只需理解为 “一棵构成用户界面的节点树”即可,不必过于纠结于这些概念。

State

一个 StatefulWidget 类会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:

  1. Statewidget 构建时可以被同步读取
  2. Statewidget 生命周期中可以被改变,可以手动调用setState()方法改变State,这会通知 Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build方法重新构建 widget 树,从而达到更新UI的目的。

State 中有两个常用属性:

  1. State.widget,通过它我们可以拿到与该 State 实例关联的 widget 实例,由 Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置 State.widget 指向新的 widget 实例

  2. State.context,它是StatefulWidget对应的 BuildContext,作用同 StatelessWidgetBuildContext。当框架需要创建一个StatefulWidget时,它会调用StatefulWidget.createState()方法,然后会将State对象和 BuildContext关联在一起,这种关联关系是永久的,State对象永远不会改变它的BuildContext,但是BuildContext本身可以在树的周围移动。

State生命周期

State对象被createState()方法创建之后,紧接着会调用initState()方法并开始自己的生命周期:

在这里插入图片描述

其中各个回调函数的含义:

  • initState():当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。

    注意:不能在initState中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget),原因是在初始化完成后, widget 树中的InheritFrom widget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。

  • didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前 build() 中包含了一个InheritedWidget ,然后在之后的 build()Inherited widget发生了变化,那么此时InheritedWidget的子 widgetdidChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。需要注意,组件第一次被创建后挂载的时候(包括重新创建)对应的didChangeDependencies也会被调用。

  • build():主要用于构建 widget 子树的,会在如下场景被调用:

    1. 在调用initState()之后。
    2. 在调用didUpdateWidget()之后。
    3. 在调用setState()之后。
    4. 在调用didChangeDependencies()之后。
    5. State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  • reassemble(): 专门为了开发调试而提供的回调,在 热重载(hot reload) 时会被调用,此回调在Release模式下永远不会被调用。

  • didUpdateWidget():在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调。

    正如之前所述,widget.canUpdate会在新旧 widgetkeyruntimeType 同时相等时会返回true,也就是说在这种情况时didUpdateWidget()就会被调用。(实际上这里flutter框架会创建一个新的Widget,绑定本State,并在这个函数中传递老的Widget)上级节点rebuild widget时, 即上级组件状态发生变化时也会触发子widget执行didUpdateWidget。需要注意的是,如果涉及到controller的变更,需要在这个函数中移除老的controller的监听,并创建新controller的监听。

  • deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过 GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。

  • dispose():当 State 对象从树中被永久移除时调用;通常在此回调中释放资源、取消订阅、取消动画等。

通过以下计数器的例子来说明:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;

  
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  
  void initState() {
    super.initState();
    //初始化状态
    _counter = widget.initValue;
    print("initState");
  }

  
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed: () => setState(
            () => ++_counter,
          ),
        ),
      ),
    );
  }

  
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget ");
  }

  
  void deactivate() {
    super.deactivate();
    print("deactivate");
  }

  
  void dispose() {
    super.dispose();
    print("dispose");
  }

  
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

注意:在继承StatefulWidget重写其方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中调用父类方法。

接下来创建一个新路由页面,在其中,我们只显示一个CounterWidget

class StateLifecycleTest extends StatelessWidget {
  const StateLifecycleTest({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return CounterWidget();
  }
}

我们运行应用并打开该路由页面,在新路由页打开后,屏幕中央就会出现一个数字0,然后控制台日志输出:

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

可以看到,在StatefulWidget插入到 widget 树时首先initState方法会被调用。

然后我们点击⚡️按钮热重载,控制台输出日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget 
I/flutter ( 5436): build

可以看到在build方法之前,只有 reassembledidUpdateWidget 被调用。

接下来,我们在 widget 树中移除 CounterWidget,将 StateLifecycleTestbuild 方法改为:

 Widget build(BuildContext context) {
  //移除计数器 
  //return CounterWidget ();
  //随便返回一个Text()
  return Text("xxx");
}

然后热重载,日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

可以看到,在 CounterWidgetwidget 树中移除时,deactivedispose会依次被调用。

setState()

setState() 方法是Flutter中修改状态的唯一合法途径,该方法会通知框架此对象的内部状态已更改,调用方式为:setState(() {_myState = newValue;}); 它会提供一个回调方法,开发者必须在该回调中修改状态值,该回调将立即被同步调用。注意决不能在该回调中执行耗时计算,更不能在其中返回一个Future(回调不能是“async”标记的),该回调通常只用于包装对状态的实际更改。

调用 setState() 后,会将当前widget对应的element对象标记为dirty,随后会调用build()方法刷新UI。下面是加入 setState() 后State的生命周期:
在这里插入图片描述

这样看来其实可以将State生命周期分为三个阶段:

  • 初始化: 构造函数、initState()didChangeDependencies()
  • 状态变化:由配置变化触发didUpdateWidget()导致的build()执行、由setState()导致的build()执行
  • 组件移除:deactivate()dispose()

StatefulWidget 为什么要将 State 和 Widget 分开呢?

StatefulWidget 为什么要将 build 方法放在 State 类中,而不是放在 StatefulWidget 中?

  • 这主要是为了开发的灵活性性能考虑。

如果将build()方法放在StatefulWidget中,从灵活性上考虑会有两个问题:

1. 状态访问不便。

试想一下,如果我们的StatefulWidget有很多状态,而每次状态改变都要调用build方法,由于状态是保存在 State 中的,如果build方法在StatefulWidget中,那么build方法和 状态 分别在两个类中,那么构建时读取状态将会很不方便!试想一下,如果真的将build方法放在 StatefulWidget 中的话,由于构建用户界面过程需要依赖 State,所以build方法将必须加一个State参数,大概是下面这样:

  Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

这样的话就只能将State的所有状态声明为公开的状态,这样才能在State类外部访问状态!但是,将状态设置为公开后,状态将不再具有私密性,这就会导致对状态的修改将会变的不可控。但如果将build()方法放在State中的话,构建过程不仅可以直接访问状态,而且也无需公开私有状态,这会非常方便。

2. 继承 StatefulWidget 不便。

例如,Flutter 中有一个动画 widget 的基类AnimatedWidget,它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法build(BuildContext context),继承自AnimatedWidget的动画 widget 都要实现这个build方法。现在设想一下,如果StatefulWidget 类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个 State 对象,这就意味着AnimatedWidget必须将自己的 State 对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build方法中调用父类的build方法,代码可能如下:

class MyAnimationWidget extends AnimatedWidget {
    
    Widget build(BuildContext context, State state){
      // 由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
      // 所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState 暴露给其子类   
      super.build(context, _animatedWidgetState)
    }
}

这样很显然是不合理的,因为AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部。

如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。

综上所述,可以发现,对于StatefulWidget,将build方法放在 State 中,可以给开发带来很大的灵活性。

另一个方面,主要是性能考虑,State管理状态(可以理解为Controller),Widget是UI(即View),根据状态变化每次生成Widget(即View)可以节省内存,而不必每次创建状态对象State。

在 widget 树中获取State对象

由于 StatefulWidget 的的具体逻辑都在其 State 中,所以很多时候,我们需要获取 StatefulWidget 对应的State 对象来调用一些方法,比如Scaffold组件对应的状态类ScaffoldState中就定义了打开 SnackBar(路由页底部提示条)的方法。我们有两种方法在子 widget 树中获取父级 StatefulWidget 的State 对象。

1. 通过Context获取State

context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打开 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过 context.findAncestorStateOfType 获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供of方法。这个约定在 Flutter SDK 里随处可见。所以,上面示例中的Scaffold也提供了一个of方法,我们其实是可以直接调用它的:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
    },
    child: Text('打开抽屉菜单2'),
  );
}),

又比如我们想显示 snackbar 的话可以通过下面代码调用:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("我是SnackBar")),
      );
    },
    child: Text('显示SnackBar'),
  );
}),

2. 通过GlobalKey获取State

Flutter还有一种通用的获取State对象的方法——通过GlobalKey来获取! 步骤分两步:

  1. 给目标StatefulWidget添加GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)
  1. 通过GlobalKey来获取State对象
_globalKey.currentState.openDrawer()

GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey,那么我们便可以通过globalKey.currentWidget获得该 widget 对象、globalKey.currentElement来获得 widget 对应的element对象,如果当前 widgetStatefulWidget,则可以通过globalKey.currentState来获得该 widget 对应的state对象。

注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复。

导航路由视角下的State生命周期

在这里插入图片描述

从StatefulElement视角分析StatefulWidget生命周期

在这里插入图片描述

可以看到,BuildOwner通过StatefulElement间接驱动了State各个生命周期回调的触发。下面将从源码的角度分析每个回调的触发路径及其含义。

// 代码清单8-1 flutter/packages/flutter/lib/src/widgets/framework.dart
StatefulElement(StatefulWidget widget)
    : state = widget.createState(), // 触发State创建
      super(widget) {
  state._element = this; // State持有Element和Widget的引用
  state._widget = widget;
  assert(state._debugLifecycleState == _StateLifecycle.created);
}

StatefulElement在自身的构造函数中完成State的创建,并完成了两个关键字段的赋值。此后会进行该Element节点的Build流程,具体逻辑如代码清单8-2所示。

// 代码清单8-2 flutter/packages/flutter/lib/src/widgets/framework.dart

void _firstBuild() {
  try { // 触发initState回调
    final dynamic debugCheckForReturnedFuture = state.initState() as dynamic;
  } finally { ...... }
  state.didChangeDependencies(); // 触发didChangeDependencies回调
  super._firstBuild();
}

因为以上逻辑触发了生命周期中的initStatedidChangeDependencies方法,所以 initState一般会作为State内部成员变量开始初始化的时间点。

didChangeDependencies虽然在首次构建时也会无条件触发,但是它在后续Build流程中依然会被触发,如代码清单8-3所示。

// 代码清单8-3 flutter/packages/flutter/lib/src/widgets/framework.dart
 // StatefulElement
void performRebuild() {
  if (_didChangeDependencies) { // 通常在代码清单8-13中设置为true,详见8.2节
    state.didChangeDependencies(); // 当该字段为true时再次触发didChangeDependencies
    _didChangeDependencies = false;
  }
  super.performRebuild();
}

didChangeDependencies标志该Element的依赖节点发生了改变,此时 didChangeDependencies 方法会被再次调用,所以该回调比较适合响应一些依赖的更新。performnRebuild最终还会触发StatefulElementbuild方法,如代码清单8-4所示。

// 代码清单8-4 flutter/packages/flutter/lib/src/widgets/framework.dart

Widget build() => state.build(this);

以上逻辑符合使用StatefulWidget的直觉,即build方法是放在State中的,该回调主要用于UI的更新。需要注意的是,如果当前Element节点标记为dirty,则build方法一定会被调用,所以不宜进行耗时操作,以免影响UI的流畅度。

对于非首次Build的情况,通常会触发Elementupdate方法,对StatefulElement来说,其逻辑如代码清单8-5所示。

// 代码清单8-5 flutter/packages/flutter/lib/src/widgets/framework.dart
void update(StatefulWidget newWidget) {  // StatefulElement,见代码清单5-47
  super.update(newWidget);
  assert(widget == newWidget);
  final StatefulWidget oldWidget = state._widget!;
  _dirty = true;
  state._widget = widget as StatefulWidget;
  try {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
    final dynamic debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) 
       as dynamic;
  } finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  }
  rebuild(); // 触发代码清单8-4中的逻辑
}

以上逻辑在通过rebuild触发Statebuild方法之前,会触发其didUpdateWidget方 法。移除对oldWidget的一些引用和依赖,以及更新一些依赖Widget属性的资源,通过该方法进行操作是一个合适的时机。

Element节点在detach阶段会调用_deactivateRecursively方 法,其具体逻辑如代码清单8-6所示。

// 代码清单8-6 flutter/packages/flutter/lib/src/widgets/framework.dart
static void _deactivateRecursively(Element element) { // 见代码清单5-50
  element.deactivate();
  assert(element._lifecycleState == _ElementLifecycle.inactive);
  element.visitChildren(_deactivateRecursively);
}

void deactivate() { // StatefulElement
  state.deactivate(); // 触发deactivate回调
  super.deactivate(); // 见代码清单8-7
}

以上逻辑会触发Statedeactivate方法,并导致当前节点进入inactive阶段。该回调触发说明当前Element节点被移出Element Tree,但仍有可用被再次加入,该时机适合释放一些和当前状态强相关的资源,而对于那些和状态无关的资源,考虑到该Element节点仍有可能进入Element Tree,并不适合在此时释放(可以类比Android中 ActivityonPause回调)。

而以上逻辑中superdeactivate方法是必须调用的,其逻辑如代码清单8-7所示。

// 代码清单8-7 flutter/packages/flutter/lib/src/widgets/framework.dart

void deactivate() { // Element
  if (_dependencies != null && _dependencies!.isNotEmpty) { // 依赖清理
    for (final InheritedElement dependency in _dependencies!)
      dependency._dependents.remove(this);
  }
  _inheritedWidgets = null;
  _lifecycleState = _ElementLifecycle.inactive; // 更新状态
}

以上逻辑主要用于移除对应的依赖。

在Build流程结束后,Elementunmount方法将被调用,其逻辑如代码清单8-8所示。

// 代码清单8-8 flutter/packages/flutter/lib/src/widgets/framework.dart
 // StatefulElement,见代码清单5-52
void unmount() {
  super.unmount();
  state.dispose(); // 触发dispose回调
  state._element = null;
}
 
void unmount() { // Element
  final Key? key = _widget.key;
  if (key is GlobalKey) {
    key._unregister(this); // 取消注册
  }
  _lifecycleState = _ElementLifecycle.defunct;
}

以上逻辑中,StatefulElement中主要会调用Statedispose方法。Element中的通用逻辑负责销毁GlobalKey相关的注册。

Flutter视角下的App生命周期

需要指出的是如果想要知道App的生命周期,那么需要通过WidgetsBindingObserverdidChangeAppLifecycleState 来获取。通过该接口可以获取的生命周期状态在AppLifecycleState类中。常用状态包含如下几个:

  • resumed:界面可见,可对用户输入做出响应,同Android的onResume
  • inactive:界面退到后台或弹出对话框情况下,处于非活动状态,即失去了焦点,没有接收用户输入,但仍可以执行drawFrame回调;同Android的onPause
  • paused:应用挂起,当前对用户不可见,比如退到后台,失去了焦点,不能响应用户输入,且不会收到drawFrame回调(engine不会回调PlatformDispatcheronBeginFrameonDrawFrame);同Android的onStop
  • detached: 应用仍然托管在flutter引擎上,但已与任何主机视图分离,不会再执行drawFrame回调;当应用处于此状态时,engine在没有视图的情况下运行。这可能是处在engine首次初始化时附加视图的过程,也可能是在由于Navigator弹出而销毁视图之后。

在这里插入图片描述

使用示例:

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }
  
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print(state);
  } 
} 

Flutter 与其他跨平台解决方案的对比

时至今日,业界已经有很多跨平台框架(特指 Android 和 iOS 两个平台),根据其原理,主要分为三类:

  • H5 + 原生(Cordova、Ionic、微信小程序)
  • JavaScript 开发 + 原生渲染 (React Native、Weex)
  • 自绘UI + 原生 (Qt for mobile、Flutter)

在这里插入图片描述

上表中开发语言主要指应用层的开发语言,而开发效率,是指整个开发周期的效率,包括编码时间、调试时间、以及排错、处理兼容性问题时间。动态化主要指是否支持动态下发代码和是否支持热更新。值得注意的是 Flutter 的Release 包默认是使用 Dart AOT 模式编译的,所以不支持动态化,但 Dart 还有 JIT 或 snapshot 运行方式,这些模式都是支持动态化的。

方案1:H5 + 原生

这类框架主要原理就是将 App 中需要动态变动的内容通过HTML5(简称 H5)来实现,通过原生的网页加载控件WebView (Android)或 WKWebView(iOS)来加载。这种方案中,H5 部分是可以随时改变而不用发版,动态化需求能满足;同时,由于 H5 代码只需要一次开发,就能同时在 Android 和 iOS 两个平台运行,这也可以减小开发成本,也就是说,H5 部分功能越多,开发成本就越小。我们称这种 H5 + 原生 的开发模式为混合开发 ,采用混合模式开发的App我们称之为混合应用HTMLybrid App ,如果一个应用的大多数功能都是 H5 实现的话,我们称其为 Web App

目前混合开发框架的典型代表有:Cordova、Ionic 。大多数 App 中都会有一些功能是 H5 开发的,至少目前为止,HTMLybrid App 仍然是最通用且最成熟的跨端解决方案。

在此,我们需要提一下小程序,目前国内各家公司小程序应用层的开发技术栈是 Web 技术栈,而底层渲染方式基本都是 WebView 和原生相结合的方式。

混合开发技术点

1) 示例:JavaScript调用原生API获取手机型号

下面我们以 Android 为例,实现一个获取手机型号的原生 API 供 JavaScript 调用。在这个示例中将展示 JavaScript 调用原生 API 的流程。我们选用 Github上开源库的 dsBridge 作为 JsBridge 来进行通信。

  • 首先在原生中实现获取手机型号的API
class JSAPI {
 
 public Object getPhoneModel(Object msg) {
   return Build.MODEL;
 }
}    
  • 将原生API通过WebView注册到JsBridge
import wendu.dsbridge.DWebView
...
//DWebView继承自WebView,由dsBridge提供  
DWebView dwebView = (DWebView) findViewById(R.id.dwebview);
//注册原生API到JsBridge
dwebView.addJavascriptObject(new JsAPI(), null);
  • JavaScript中调用原生API
var dsBridge = require("dsbridge")
//直接调用原生API `getPhoneModel`
var model = dsBridge.call("getPhoneModel");
//打印机型
console.log(model);

上面示例演示了 JavaScript 调用原生 API 的过程,同样的,一般来说优秀的 JsBridge 也支持原生调用 JavaScriptdsBridge 也是支持的,如果感兴趣,可以去 Github dsBridge 项目主页查看。

现在,我们回头来看一下,混合应用无非就是在第一步中预先实现一系列 API 供 JavaScript 调用,让 JavaScript 有访问系统功能的能力,看到这里相信你也可以自己实现一个混合开发框架了。

小结

混合应用的优点是:动态内容可以用 H5开发,而H5是Web 技术栈,Web技术栈生态开放且社区资源丰富,整体开发效率高。缺点是性能体验不佳,对于复杂用户界面或动画,WebView 有时会不堪重任。

方案2:JavaScript开发 + 原生渲染

React 响应式编程

React 是一个响应式的 Web 框架,我们先了解一下两个重要的概念:DOM 树与响应式编程。

DOM 树

文档对象模型(Document Object Model,简称DOM),是 W3C 组织推荐的处理可扩展标志语言的标准编程接口,一种独立于平台和语言的方式访问和修改一个文档的内容和结构。换句话说,这是表示和处理一个 HTML 或XML 文档的标准接口。简单来说,DOM 就是文档树,与用户界面控件树对应,在前端开发中通常指 HTML 对应的渲染树,但广义的 DOM 也可以指 Android 中的 XML 布局文件对应的控件树,而术语DOM操作就是指直接来操作渲染树(或控件树), 因此,可以看到其实 DOM 树和控件树是等价的概念,只不过前者常用于 Web 开发中,而后者常用于原生开发中。

响应式编程

React 中提出一个重要思想:状态改变则UI随之自动改变。而 React 框架本身就是响应用户状态改变的事件而执行重新构建用户界面的工作,这就是典型的 响应式 编程范式,下面我们总结一下 React 中响应式原理:

  • 开发者只需关注状态转移(数据),当状态发生变化,React 框架会自动根据新的状态重新构建UI。
  • React 框架在接收到用户状态改变通知后,会根据当前渲染树,结合最新的状态改变,通过 Diff 算法,计算出树中变化的部分,然后只更新变化的部分(DOM操作),从而避免整棵树重构,提高性能。

值得注意的是,在第二步中,状态变化后 React 框架并不会立即去计算并渲染 DOM 树的变化部分,相反,React会在 DOM 树的基础上建立一个抽象层,即虚拟DOM树,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟 DOM ,最后再批量同步到真实 DOM 中,而不是每次改变都去操作一下DOM。

为什么不能每次改变都直接去操作 DOM 树?这是因为在浏览器中每一次 DOM 操作都有可能引起浏览器的重绘或回流(重新排版布局,确定 DOM 节点的大小和位置):

  1. 如果 DOM 只是外观风格发生变化,如颜色变化,会导致浏览器重绘界面。
  2. 如果 DOM 树的结构发生变化,如尺寸、布局、节点隐藏等导致,浏览器就需要回流。

而浏览器的重绘和回流都是比较昂贵的操作,如果每一次改变都直接对 DOM 进行操作,这会带来性能问题,而批量操作只会触发一次 DOM 更新,会有更高的性能。

React Native

React Native (简称 RN )是 Facebook 于 2015 年 4 月开源的跨平台移动应用开发框架,是 Facebook 早先开源的 Web 框架 React 在原生移动应用平台的衍生产物,目前支持 iOS 和 Android 两个平台。RN 使用JSX 语言(扩展后的 JavaScript,主要是可以在 JavaScript 中写 HTML标签)和 CSS 来开发移动应用。因此,熟悉 Web 前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。

由于 RN 和 React 原理相通,并且 Flutter在应用层也是受 React 启发,很多思想也都是相通的。

上文已经提到 React Native 是 React 在原生移动应用平台的衍生产物,那两者主要的区别是什么呢?其实,主要的区别在于虚拟 DOM 映射的对象是什么。React中虚拟 DOM 最终会映射为浏览器 DOM 树,而 RN 中虚拟 DOM会通过 JavaScriptCore 映射为原生控件。

JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:

  1. 为 JavaScript 提供运行环境。

  2. 是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。
    而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步:

  3. 布局消息传递; 将虚拟 DOM 布局信息传递给原生;

  4. 原生根据布局信息通过对应的原生控件渲染;

至此,React Native 便实现了跨平台。 相对于混合应用,由于React Native是 原生控件渲染,所以性能会比混合应用中 H5 好一些,同时 React Native 提供了很多原生组件对应的 Web 组件,大多数情况下开发者只需要使用 Web 技术栈 就能开发出 App。我们可以发现,这样也就做到了维护一份代码,便可以跨平台了。

Weex

Weex 是阿里巴巴于 2016 年发布的跨平台移动端开发框架,思想及原理和 React Native 类似,底层都是通过原生渲染的,不同是应用层开发语法 (即 DSL,Domain Specific Language):Weex 支持 Vue 语法和 Rax 语法,Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造,而 RN 的 DSL 是基于 React 的,不支持 Vue。

小结

JavaScript 开发 + 原生渲染 的方式主要优点如下:

  1. 采用 Web 开发技术栈,社区庞大、上手快、开发成本相对较低。
  2. 原生渲染,性能相比 H5 提高很多。
  3. 动态化较好,支持热更新。

不足:

  1. 渲染时需要 JavaScript 和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。
  2. JavaScript 为脚本语言,执行时需要解释执行 (这种执行方式通常称为 JIT,即 Just In Time,指在执行时实时生成机器码),执行效率和编译类语言(编译类语言的执行方式为 AOT ,即 Ahead Of Time,指在代码执行前已经将源码进行了预处理,这种预处理通常情况下是将源码编译为机器码或某种中间码)仍有差距。
  3. 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;除此之外,其控件系统也会受到原生UI系统限制,例如,在 Android 中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。这就会导致,如果需要自定义原生渲染组件时,开发和维护成本过高。

方案3:自绘UI + 原生

这种技术的思路是:通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,所以可以做到不同平台UI的一致性。

注意,自绘引擎解决的是 UI 的跨平台问题,如果涉及其他系统能力调用,依然要涉及原生开发。这种平台技术的优点如下:

  1. 性能高;由于自绘引擎是直接调用系统API来绘制UI,所以性能和原生控件接近。

  2. 灵活、组件库易维护、UI外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到高保真和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。

不足:

  1. 动态性不足;为了保证UI绘制性能,自绘UI系统一般都会采用 AOT 模式编译其发布包,所以应用发布后,不能像 Hybrid 和 RN 那些使用 JavaScript(JIT)作为开发语言的框架那样动态下发代码。
  2. 应用开发效率低:Qt 使用 C++ 作为其开发语言,而编程效率是直接会影响 App 开发效率的,C++ 作为一门静态语言,在 UI 开发方面灵活性不及 JavaScript 这样的动态语言,另外,C++需要开发者手动去管理内存分配,没有 JavaScript 及Java中垃圾回收(GC)的机制。

也许你已经猜到 Flutter 就属于这一类跨平台技术,没错,Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破。不过,自绘制引擎的思路并不是什么新概念,Flutter并不是第一个尝试这么做的,在它之前有一个典型的代表,即大名鼎鼎的Qt。

Qt Mobile

Qt 是一个1991年由 Qt Company 开发的跨平台 C++ 图形用户界面应用程序开发框架。2008年,Qt Company 科技被诺基亚公司收购,Qt 也因此成为诺基亚旗下的编程语言工具。2012年,Qt 被 Digia 收购。2014年4月,跨平台集成开发环境 Qt Creator 3.1.0 正式发布,实现了对于 iOS 的完全支持,新增 WinRT、Beautifier 等插件,废弃了无 Python 接口的 GDB 调试支持,集成了基于 Clang 的 C/C++ 代码模块,并对 Android 支持做出了调整,至此实现了全面支持 iOS、Android、WP,它提供给应用程序开发者构建图形用户界面所需的所有功能。

但是,Qt 虽然在 PC 端获得了巨大成功,备受社区追捧,然而其在移动端却表现不佳,在近几年很少能听到 Qt 的声音,尽管 Qt 是移动端开发跨平台自绘引擎的先驱,但却成为了烈士。究其原因,可能有以下几点:

  • Qt 移动开发社区太小,学习资料不足,生态不好。
  • 官方推广不利,支持不够。
  • 移动端发力较晚,市场已被其他动态化框架占领( Hybrid 和 RN )。
  • 在移动开发中,C++ 开发和Web开发栈相比有着先天的劣势,直接结果就是 Qt 开发效率太低。

Flutter

Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。Flutter 和 Qt mobile 一样,都没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。那么,我们会担心,Qt mobile 面对的问题Flutter是否也一样,Flutter会不会步入Qt mobile后尘,成为另一个烈士?Flutter从2017 年诞生至今,经历了多年时间的历练,Flutter 的生态系统得以快速增长,国内外有非常多基于 Flutter 的成功案例,国内的互联网公司基本都有专门的 Flutter 团队。总之,Flutter 发展飞快,已在业界得到了广泛的关注和认可,在开发者中受到了热烈的欢迎,成为了移动跨端开发中最受欢迎的框架之一。

现在,我们来和 Qt mobile做一个对比:

  1. 生态:Flutter 生态系统发展迅速,社区非常活跃,无论是开发者数量还是第三方组件都已经非常可观。
  2. 技术支持:现在 Google 正在大力推广Flutter,Flutter 的作者中很多人都是来自Chromium团队,并且 Github上活跃度很高。另一个角度,从 Flutter 诞生到现在,频繁的版本发布也可以看出 Google 对 Flutter的投入的资源不小,所以在官方技术支持这方面,大可不必担心。
  3. 开发效率:一套代码,多端运行;并且在开发过程中 Flutter 的热重载可帮助开发者快速地进行测试、构建UI、添加功能并更快地修复错误。在 iOS 和 Android 模拟器或真机上可以实现毫秒级热重载,并且不会丢失状态。这真的很棒,相信我,如果你是一名原生开发者,体验了Flutter开发流后,很可能就不想重新回去做原生了,毕竟很少有人不吐槽原生开发的编译速度。

Flutter 中的 GC

Flutter使用Dart作为开发语言和运行时机制,Dart一直保留着运行时机制,无论是在调试模式(debug)还是发布模式(release),但是两种构建方式之间存在很大的差异。

  • debug模式下,Dart将所有的管道(需要用到的所有配件)全部装载到设备上:Dart runtimeJIT(the just-in-time)编译器/解释器(JIT for Android and interpreter for iOS)、调试和性能分析服务。
  • release模式下,会采用AOT编译,去除JIT,依然保留Dart runtime,因为Dart runtime是Flutter App的主要贡献者。

在这里插入图片描述

Dart的运行时包括一个非常重要的组件:垃圾回收器,它主要的作用就是在一个对象被实例化(instantiated)或者变成不可达(unreachable)时,分配和释放内存。

在Flutter运行过程中,会有很多的Object。在StatelessWidget在渲染前(其实上还有StatefulWidget),他们被创建出来。当状态发生变化的时候,他们又会被销毁。事实上,他们有很短的寿命。当我们构建一个复杂的UI界面时,会有成千上万这样的Widget

所以,作为Flutter开发者,我们是否需要担心垃圾回收器能否很好的帮助我们管理这些?(是否会带来很多的性能问题?)随着Flutter频繁地创建和销毁这些Widget(Objects)对象,开发人员是否应该采取措施限制这种行为?

  • 对于新的Flutter开发人员来说,如果他们知道一个Widget不会随着时间的推移而改变时,他们会创建一个Widget引用,并将它们放在State中,这样它们就不会被破坏和重建,这并不罕见。

这样做大可不必

  • 担心Dart的GC是完全没有必要的,这是因为它的分代架构为了可以让我们频繁的创建和销毁对象专门做了优化。在大多数情况下,我们只需要让Flutter的引擎创建并销毁它喜欢的所有Widget即可。

Dart的GC

Dart的GC是分代的(generational)和由两个阶段构成:Young Space Scavenger(scavenger针对年轻一袋进行回收) 和 Parallel Marking and Concurrent Sweeping(sweep collectors针对老一代进行回收)

Scheduling

为了最大限度地减少GC对应用程序和UI性能的影响,垃圾收集器为Flutter引擎提供了hooks,当引擎检测到应用程序空闲且没有用户交互时,该hooks会提醒它。这使垃圾收集器窗口有机会在不影响性能的情况下运行其收集阶段。

垃圾收集器还可以在这些空闲间隔期间运行滑动压缩(sliding compaction),从而通过减少内存碎片来最大程度地减少内存开销。

在这里插入图片描述

阶段一:Young Space Scavenger

这个阶段主要是清理一些寿命很短的对象,比如StatelessWidget。当它处于阻塞时,它的清理速度远快于第二代的mark/sweep方式。并且结合调度,完成可以消除程序运行时的暂停现象。

本质上,对象被分配到内存中的一个连续空间,当对象被创建时,它们被分配到下一个可用空间,直到分配的内存被填满。Dart使用碰撞指针(bump pointer)分配来快速分配新的空间,使过程非常快速。(如果像malloc一样,维护free_list再分配,效率很低)

分配新对象的新空间由两半组成,称为半空间。任何时候都只使用一半空间:一半处于活动状态,另一半处于非活动状态。新对象被分配到活动的一半区域,一旦活动的一半被填充完毕,活动对象就会从活动区域copy到非活动区域,并且清除死亡的Object。然后,不活动的一半就变为了活动状态,并重复该过程。(这跟JVM的分代回收策略中新生代的幸存者空间很像)

为了确定哪些Object是存活的或死亡的,GC从根对象(如堆栈变量)开始检查它们引用的内容。然后将有引用的Object(存活的)移动到非活动状态,直接所有的存活Object被移动。死亡的Object没有参照物,因此被留下来;在将来的垃圾收集事件中,活动对象将被复制到它们上面。

有关此的更多信息,请查看Cheney算法。

在这里插入图片描述

阶段二:Parallel Marking and Concurrent Sweeping

当对象达到一定的寿命(在第一阶段没有被GC回收),它们会被提升到一个由第二代收集器管理的新的内存空间:mark-sweep。

这种垃圾收集技术有两个阶段:首先遍历对象图,然后标记仍在使用的对象。在第二阶段,扫描整个内存,并回收任何未标记的对象。然后清除所有标志。

这种GC技术阻塞了标记阶段;不会发生内存突变,并且UI线程也会被阻塞。但是由于短暂的对象在Young Space Scavenger阶段已经被处理,所以这种情况阶段非常罕见。但有时Dart运行时需要暂停才能运行这种形式的GC。考虑到Flutter计划收集的能力,应将其影响降至最低。

需要注意的是,如果一个应用程序不遵循分代的假设(即大多数对象英年早逝的假设),那么这种形式的GC将更频繁地发生。考虑到Flutter中Widget的工作机制,这不太可能发生,但需要了解。

Isolate

值得注意的是,Dart中的Isolate机制具有私有堆的概念,彼此是独立的。每个Isolate有自己单独的线程来运行,每个Isolate的GC不影响其他Isolate的性能。使用Isolate是避免阻塞UI和卸载流程密集型活动的好方法。(耗时操作可以使用Isolate)

到这里你应该明白:Dart采用了一个强大的分代垃圾收集器,以最大限度的减少Flutter中GC带来的性能影响。 所以,你不需要担心Dart的垃圾回收器,可以放心的把精力放在业务上。


参考:

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。