Flutter3.0(framework框架)——UI渲染

Flutter是谷歌开源的移动UI框架,可以快速在Android和iOS上构建出高质量的原生用户界面,目前全世界越来越多的开发者加入到Flutter的队伍。 Flutter相比RN性能更好,由于Flutter自己实现了一套UI框架,丢弃了原生的UI框架,非常接近原生的体验。

我们知道Flutter在UI绘制方面的效率是几乎接近原生的,这点比React Native要优秀很多,因为React Native是通过桥接转换然后去调用各自平台的UI系统(如iOS中的UIKit框架)提供的API来完成绘图。

一、UI线程渲染

为了揭秘Flutter高性能,本文从源码角度来看看Flutter的渲染绘制机制,跟渲染直接相关的两个线程是UI线程和GPU线程:

UI线程:运行着UI Task Runner,是Flutter Engine用于执行Dart root isolate代码,将其转换为layer tree视图结构;

GPU线程:该线程依然是在CPU上执行,运行着GPU Task Runner,处理layer tree,将其转换成为GPU命令并发送到GPU。

通过VSYNC信号使UI线程和GPU线程有条不紊的周期性的渲染界面,本文介绍VSYNC的产生过程、UI线程在引擎和框架的绘制工作,下一篇文章会介绍GPU线程的绘制工作。

1.1 UI渲染原理

1.1.1 UI渲染概览

通过VSYNC信号使UI线程和GPU线程有条不紊的周期性的渲染界面,如下图所示:

Flutter3.0(framework框架)——UI渲染_第1张图片

当需要渲染则会调用到Engine的ScheduleFrame()来注册VSYNC信号回调,一旦触发回调doFrame()执行完成后,便会移除回调方法,也就是说一次注册一次回调;

当需要再次绘制则需要重新调用到ScheduleFrame()方法,该方法的唯一重要参数regenerate_layer_tree决定在帧绘制过程是否需要重新生成layer tree,还是直接复用上一次的layer tree;

UI线程的绘制过程,最核心的是执行WidgetsBinding的drawFrame()方法,然后会创建layer tree视图树

再交由GPU Task Runner将layer tree提供的信息转化为平台可执行的GPU指令。

1.1.2 UI绘制核心工作

1)Vsync单注册模式:保证在一帧的时间窗口里UI线程只会生成一个layer tree发送给GPU线程,原理如下:

Animator中的信号量pending_frame_semaphore_用于控制不能连续频繁地调用Vsync请求,一次只能存在Vsync注册。

pending_frame_semaphore_初始值为1,在Animator::RequestFrame()消费信号会减1,当而后再次调用则会失败直接返回;

Animator的BeginFrame()或者DrawLastLayerTree()方法会执行信号加1操作。

2)UI绘制最核心的方法是drawFrame(),包含以下几个过程:

Animate: 遍历_transientCallbacks,执行动画回调方法;

Build: 对于dirty的元素会执行build构造,没有dirty元素则不会执行,对应于buildScope()

Layout: 计算渲染对象的大小和位置,对应于flushLayout(),这个过程可能会嵌套再调用build操作;

Compositing bits: 更新具有脏合成位的任何渲染对象, 对应于flushCompositingBits();

Paint: 将绘制命令记录到Layer, 对应于flushPaint();

Compositing: 将Compositing bits发送给GPU, 对应于compositeFrame();

Semantics: 编译渲染对象的语义,并将语义发送给操作系统, 对应于flushSemantics()。

UI线程的耗时从doFrame(frameTimeNanos)中的frameTimeNanos为起点,以小节[4.10.6]Animator::Render()方法结束为终点, 并将结果保存到LayerTree的成员变量construction_time_,这便是UI线程的耗时时长。

1.1.3 Timeline说明

3)以上几个过程在Timeline中ui线程中都有体现,如下图所示:

Flutter3.0(framework框架)——UI渲染_第2张图片

另外Timeline中还有两个比较常见的标签项

“Frame Request Pending”:从Animator::RequestFrame 到Animator::BeginFrame()结束;

”PipelineProduce“: 从Animator::BeginFrame()到Animator::Render()结束。

二、渲染过程

构建 Widgets

flutter3.0学习

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

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      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 结构比代码表示的层级更深,在该场景中如下图2:

Flutter3.0(framework框架)——UI渲染_第3张图片

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

从 Widget 到 Element

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

ComponentElement,其他 Element 的宿主。

RenderObjectElement,参与布局或绘制阶段的 Element。

Flutter3.0(framework框架)——UI渲染_第4张图片

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 是更为上层的实现。

更新 UI

上面介绍了 Flutter 在 Framework 级别渲染的流程(后续交给图像引擎),这只是一帧的流程。Flutter 在更新 UI 上也有一些不同。

更新 Widget Tree

之前提到了 Widget 的不可变性,所以每一帧都会调用 build()方法返回 widget 树,即使是 StatefulWidget ,也只是根据不同的状态返回不同的 widget 树 。但这样的话理论上会有大量的实例的产生和销毁,频繁 GC, 不过实际上,上面提到了 Widget 只是 UI 的配置,不负责实际的渲染,开销并没有那么大。

更新 Element Tree

更重量级的 Element Tree 并不会全部重新渲染,而是根据 Widget Tree 的变化维护 Element Tree (插入、删除、更新、移动…),其中核心的几个方法包括:

1.Element 调用 Widget.canUpdate(),去判断新的 widget 是否能用于更新 Element。

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

2.可以更新的话调用 Element.update() Element.updateChild() …

//继承类自行实现
@mustCallSuper
void update(covariant Widget newWidget) {
  _widget = newWidget;
}

//framework.dart
 @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key key = newWidget?.key;
      if (key is GlobalKey) {
        key._debugReserveFor(this, newChild);
      }
      return true;
    }());
    
    return newChild;
  }
...
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

newWidget == null —— 说明子节点对应的 Widget 已被移除,直接 remove child element (如有);
child == null —— 说明 newWidget 是新插入的,创建子节点 (inflateWidget);
child != null —— 此时,分为 3 种情况:
若 child.widget == newWidget,说明 child.widget 前后没有变化,若 child.slot != newSlot 表明子节点在兄弟结点间移动了位置,通过updateSlotForChild修改 child.slot 即可;
通过Widget.canUpdate判断是否可以用 newWidget 修改 child element,若可以,则调用update方法;
否则先将 child element 移除,并通 newWidget 创建新的 element 子节点。
更新 RenderObject Tree

Dart VM

DartVM 的内存回收机制采用多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化。 基本的流程: DartVM的内存分配策略非常简单,创建对象时只需要在现有堆上移动指针,内存增长始终是线形的,省去了查找可用内存段的过程:

Flutter3.0(framework框架)——UI渲染_第5张图片

Dart中类似线程的概念叫做Isolate,每个Isolate之间是无法共享内存的,所以这种分配策略可以让Dart实现无锁的快速分配。 Dart的垃圾回收也采用了多生代算法,新生代在回收内存时采用了“半空间”算法,触发垃圾回收时Dart会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存:

Flutter3.0(framework框架)——UI渲染_第6张图片

整个过程中Dart只需要操作少量的“活跃”对象,大量的没有引用的“死亡”对象则被忽略,这种算法也非常适合Flutter框架中大量Widget重建的场景。

flutter基础到精通学习;dart语法基础到flutterUI、线程、启动流程、框架、性能监控等。

文末

文章主要解析了flutter的UI线程渲染原理,以及渲染其过程。关于更多flutter的进阶学习。比喻(flutter动画原理、渲染机制、通信机制等。这只是部分)可以上面直达得到。

你可能感兴趣的:(flutter,ui,flutter,react,native,android)