• Flutter框架和原理剖析


    Flutter是Google推出并开源的跨平台开发框架,主打跨平台、高保真、高性能。开发者可以通过Dart语言开发Flutter应用,一套代码同时运行在ios和Android平台。不仅如此,flutter还支持web、桌面、嵌入应用的开发。flutter提供了丰富的组件、接口,开发者可以很快地为flutter添加native扩展。同时flutter还使用skia引擎(其实Android原生中页面绘制也是由skia引擎来渲染的)渲染视图,这无疑为用户提供良好地体验

    Flutter框架地整体架构

    在这里插入图片描述
    简单来讲,Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层。

    框架层

    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 设计规范。

    引擎层

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

    嵌入层

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

    Flutter绘制原理

    为了熟悉flutter地绘制原理,我们先从屏幕显示图像地基本原理说起:我们在买显示器时,都会关注显示器地刷新频率;那么对于手机屏幕也是一样,通常手机屏幕地刷新频率是60Hz,当然现在也有不少高刷新频率地手机也在推出,如:90Hz,120Hz。
    在这里插入图片描述
    一般来说,计算机系统中,CPU、GPU和显示器以一种特定地方式协作:CPU将计算好地显示内容提交给GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照VSync信号从帧缓冲区取帧数据传递给显示器显示。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以60Hz的屏幕就会一秒内发出60次这样的信号。

    上面是CPU、GPU和显示器协作方式,对于Flutter也不例外,Flutter也遵循了这种模式:
    在这里插入图片描述
    GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构(这一步是在Framework层中实现的),绘制好的抽象视图结构会在GPU线程中进行图像的合成(这一步在引擎层中完成),然后提供给skia渲染成GPU所需要的数据,最终提供给GPU进行渲染。由此可知,flutter高性能的核心其实就在于根据vsync信号进行图像的快速构建,也就是上图中的绿色部分

    Flutter渲染流程

    在这里插入图片描述
    在Flutter框架中存在着一个渲染流程。这个渲染流水线是由垂直同步信号驱动的,而Vsync信号由系统提供的,如果你的Flutter app是运行在Android上,那Vsync信号就是我们熟悉的Android的那个Vsync信号。

    当Vsync信号到来了以后,Flutter框架会按照图里的顺序执行一系列动作:

    • 动画
    • 构建
    • 布局
    • 绘制

    最终生成一个场景之后送往底层,由GPU绘制到屏幕上。

    • 动画阶段:因为动画会随着每个Vsync信号的到来而改变状态(State),所以动画阶段是流水线的第一个阶段;
    • 构建阶段:在这个阶段那些需要被重新构建的Widget会在此时被重新构建。也就是我们熟悉的build()方法被调用的时候。
    • 布局阶段:这时会确定各个显示元素的位置、尺寸;此时是RenderObject.performLayout()被调用的时候;
    • 绘制阶段:此时是RenderObject.paint()被调用的时候

    Flutter组件的生命周期

    在这里插入图片描述

    • createState():当框架要创建一个StatefulWidget时,它会立即调用State的createState()
    • initState():当State的构造方法被执行后,会调用一次initState(),需要指出的是initState在State生命周期中只会被调用一次
    • build():这个方法会被经常调用,比如:setState以及配置改变都会触发build方法的调用
    • didUpdateConfig():当收到一个新的config时调用
    • setState():当需要修改页面状态,比如刷新数据等的时候我们可以通过调用setState来实现
    • dispose():当移除State对象时,将调用dispose方法;通常在该方法中进行取消订阅,取消所有动画、流等操作。

    Flutter渲染机制——为了高性能而生的三棵树

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

    既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的处理流程是这样的:

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

    真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。

    从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

    接下来,我们重点看下Element这个东西,Element的生命周期如下:

    1. Framework 调用Widget.createElement 创建一个Element实例,记为element。
    2. Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新添加)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
    3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。
    4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。
    5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
    6. 如果element要重新插入到Element树的其他位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

    现在,读者应该能够知道为什么笔者说:“三棵树是为了性能而生的”。因为,视图中的控件都是Render树中的对象,我们知道new出一个对象都是需要消耗操作系统很多资源的,通过三棵树就可以做到对已有render树中的对象进行复用,而不是每更改一个widget就new出一个新的render对象。

    这里额外多提一嘴:我们在日常开发的过程中经常会接触到BuildContext这个东西。那这个东西究竟是什么呢?笔者就直接说答案了,有兴趣的同学可以看下源码。BuildContext就是widget对应的Element,所以我们可以通过context在StatelessWidget和StatefulWidget的build方法中直接访问Element对象。我们获取主题数据的代码Theme.of(context)内部正是调用了Element的dependOnInheritedWidgetOfExactType()方法。

    三棵树的运用

    我们可以看到Element是Flutter UI框架内部连接widget和RenderObject的纽带,大多数时候开发者只需要关注widget层即可,但是widget层有时候并不能完全屏蔽Element细节,所以Framework在StatelessWidget和StatefulWidget中通过build方法参数又将Element对象也传递给了开发者,这样一来,开发者便可以在需要时直接操作Element对象。
    那么,现在笔者有个问题:如果没有widget层,单靠Element层是否可以搭建起一个可用的UI框架?如果可以应该是什么样子?

    • 案例如下:
    class HomeView extends ComponentElement{
      HomeView(Widget widget) : super(widget);
      String text = "123456789";
    
      
      Widget build() {
        Color primary=Theme.of(this).primaryColor; //1
        return GestureDetector(
          child: Center(
            child: TextButton(
              child: Text(text, style: TextStyle(color: primary),),
              onPressed: () {
                var t = text.split("")..shuffle();
                text = t.join();
                markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
              },
            ),
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 上面build方法不接收参数,这一点和在StatelessWidget和StatefulWidget中build(BuildContext)方法不同。代码中需要用到BuildContext的地方直接用this代替即可,如代码注释1处Theme.of(this)参数直接传this即可,因为当前对象本身就是Element实例。

    2. 当text发生改变时,我们调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。

    3. 上面代码中build方法返回的仍然是一个widget,这是由于Flutter框架中已经有了widget这一层,并且组件库都已经是以widget的形式提供了,如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。

    如果我们需要将上面代码在现有Flutter框架中跑起来,那么还是得提供一个“适配器”widget将HomeView结合到现有框架中,下面CustomHome就相当于“适配器”:

    class CustomHome extends Widget {
      
      Element createElement() {
        return HomeView(this);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    后记

    给大家推荐一本flutter相关的电子书籍:《Flutter实战·第二版》

  • 相关阅读:
    TCP/IP协议数据链路层
    11.16 知识总结(模型层更多内容)
    超越NumPy和Pandas的Python库
    中间件、Kafka、Zookeeper
    杰理之BLE优化原理【篇】
    代码块小知识
    MySQL的Undo Log、Redo Log和Binlog
    springboot网络微小说毕业设计源码031758
    【软件逆向-分析工具】反汇编和反编译工具
    59 分割等和子集
  • 原文地址:https://blog.csdn.net/qq_36828822/article/details/132848366