Flutter Dart Framework原理简解

栏目: IOS · Android · 发布时间: 5年前

内容简介:从前一篇文章Flutter原理的分析可知,Flutter Engine向在开始本文前,我们还是要了解一下通用的 UI框架绘图的流程,以便我们理解 Flutter的 Dart Framework:

Flutter Dart Framework原理简解

从前一篇文章Flutter原理的分析可知,Flutter Engine向 dart:ui Framework层提供了通用的绘图能力,Flutter渲染 UI的本质就是在 VSync信号间尽快地构建并提供视图数据。所以 Flutter整个框架的精髓在 Dart UI Framework(更准确地叫作 Flutter Dart Framework)里,这里面涉及了非常多的设计思想、优化措施,所以一篇文章是无法概括的。 所以在本篇文章中,我们只能概要性地分析一下 Flutter Dart Framework的大致原理。

UI框架绘图的基本流程

在开始本文前,我们还是要了解一下通用的 UI框架绘图的流程,以便我们理解 Flutter的 Dart Framework:

Flutter Dart Framework原理简解

从上图中我们可以看到,用户的输入才是驱动视图更新的信号。当这个信号发生后,首先需要触发的就是动画的进度更新,框架需要开始视图数据的“build”(也可以看做数据的填充)。

在这之后,视图才会进行布局,计算各个部分的大小,然后进行“paint”,生成每个视图的视觉数据。生成的视觉数据并不能直接用,因为往往视图层级非常多, 这些数据直接向 GPU传递很低效,所以接下来需要进行视图合成,将多个视图数据合成为一个。

最后一步进行“光栅化”,前一步得到合成的视图数据其实还是一份矢量描述数据,光栅化帮助把这份数据真正地生成一个一个的像素填充数据。在 Flutter中,光栅化这个步骤被放在了 Engine中,但是我们前文并没有提到。

在了解 UI框架的通用流程后,我们自然而然能知道 Flutter Dart Framework这一层肯定做了以下这些事:

  1. 视图的数据结构(视图树)
  2. 视图数据的构建
  3. 视图布局的计算
  4. 视图的合成
  5. 与 Engine的数据同步和通信

视图的结构

无论是比较底层的 UI框架(如 Cocoa、WebKit)还是比较上层的 (React),在向绘制引擎提供视图数据都需要一份结构化的视图数据,俗称为“视图树”(View Tree)。Flutter 的视图结构的抽象分为三部分: Widget , Element , RenderObject

三部分?在哪熟悉是不是? Cocoa Touch中,视图树被分为 模型树、呈现树、渲染树, 在 Flutter中你也可以看待这三部分的意义。

Widget:

Widget是 Flutter中控件实现的基本单位,其意义类似于 Cocoa Touch中的 UIView。Widget里面存储了一个视图的配置信息,包括布局、属性等待。所以它只是一份轻量的,可直接使用的数据结构。在构建为结构树,甚至重新创建和销毁结构树时都不存在明显的性能问题。

Element:

Element是 Widget的抽象,它其实承载了视图构建的上下文数据。构建系统通过遍历 Element树来构建 RenderObject数据,比如视图更新时,只会标记 dirty Element,而不会标记 dirty Widget。所以 Widget“无状态”,而 Element“有状态” (这个状态指框架层的构建状态)。

RenderObject:

在 RenderObject树中会发生 Layout、Paint的绘制事件,所以 Flutter中大部分的绘图性能优化发生在这里。RenderObject树构建的数据会被加入到 Engine所需的 LayerTree中,Engine通过 LayerTree进行视图合成并光栅化,提交给 GPU。

Flutter通过这三种概念,把原本比较复杂的视图树状态、数据的维护和构建拆分得更单一、易于集中治理。

视图数据的构建

这个话题其实比较大,涉及到的逻辑也很多。我们可以先从应用入口开始看起。

应用的 root view

Flutter App 入口的部分发生于如下代码:

import 'package:flutter/material.dart';

// 这里的 MyApp是一个 Widget
void main() => runApp(new MyApp());

runApp 函数接受一个 Widget类型的对象作为参数,也就是说在 Flutter的概念中,只存在 View,而其他的任何逻辑都只为 View的数据、状态改变服务,不存在 ViewController(或者叫 Activity)。

接下来看 runApp 做了什么:

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      new WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

runApp 中,传入的 widget被挂载到根 widget上。这个 WidgetsFlutterBinding 其实是一个单例,通过 mixin来使用框架中实现的其他 binding的 Service,比如 手势、基础服务、队列、绘图等等。然后会调用 scheduleWarmUpFrame 这个方法,从这个方法注释可知,调用这个方法会主动构建视图数据。这样做的好处是因为 Flutter依赖 Dart的 MicroTask来进行帧数据构建任务的 schedule,这里通过主动调用进行整个周期的 “热身”,这样最近的下次 VSync信号同步时就有视图数据可提供,而不用等到 MicroTask的 next Tick。

然后我们再来看 attachRootWidget 这个函数干了什么:

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
}

attachRootWidget 把 widget交给了 RenderObjectToWidgetAdapter 这座桥梁,通过这座桥梁,Element被创建,并且同时能持有 Widget和 RenderObject的引用。然后我们从上文就知道后面发生的就是第一次的视图数据构建了。

从这一部分能印证前面所说:Flutter应用通过 WidgetElementRenderObject 三种树结构来维护整个应用的视图数据。

视图数据的构建时机

VSync信号其实在 Flutter Dart Framework层是透明的,视图的数据构建和更新并不是由 VSync信号驱动的。我在前文说道: Flutter并不关心显示器、视频控制器以及 GPU具体工作,它只关心 GPU发出的 VSync信号,尽可能快地在两个 VSync信号之间计算并合成视图数据,并且把数据提供给 GPU。 这个说法其实是不准确的,准确的说法其实应该是:

Flutter 并不关心显示器、视频控制器以及 GPU具体工作,它只关心能在 VSync信号间隔时间内尽快地合成视图数据,等待 VSync信号同步来获取这个视图数据提供给 GPU。

那么 Flutter是在什么时机构建视图数据的呢?前面就已经揭晓过答案:MicroTask 循环。

是不是和 Cocoa Touch很像?NSRunloop 的 main loop 也是通过事件循环来执行固定的视图更新回调

dart:ui 有一个抽象的 window ,它并不负责向 LayerTree提供数据,但在 Flutter中,它承担了管理和回调视图更新方法的重担。在 rootView(rootElement)被创建时,ScheduleBinding便会调用 scheduleFrame ,这会调用 window.scheduleFrame ,而在 window 的 postFrameCallback 中, scheduleFrame 会再次被调用,这样就形成了循环。

通过这种有序的周期循环,Flutter能源源不断地提供帧视图数据,VSync信号同步时便能获得这个数据进行 Engine部分后续的操作。

视图数据的构建方式

前面提到,视图树被分为三类: WidgetElementRenderObject ,Element同时持有 Widget和 RenderObject的引用。这是本段的大前提。

Widget是用户能够直接操作的数据结构,Flutter的设计理念中,Widget都是 immutable的,所以 Widget节点的改变其实就是节点的销毁和重新创建。但如果只是最基本的 Widget手动组合的话,写起来会很蛋疼,所以 Flutter使用 State来控制 Widget的创建与销毁,以此来达到响应式 UI编程的目的。

细细品味,Widget的设计理念有没有一点和 React像?

Widget在更新后,Element持有该 Widget的节点也会被标记为 dirtyElement,那么在下一个周期的 drawFrame时,Element树的这一部分子树便会被触发 performRebuild。在 Element树更新完成后,便能获得 RenderObject树,接下来便会进入 Layout和 Paint的流程。

视图的布局与绘制

在 Flutter中,视图的布局与合成是整个 UI框架中最重要的部分,这部分的好坏决定了绘图的性能以及框架的应用表现,所以 Google有一个专门的 TechTalk来讲解这个原理: Flutter’s renderding pipeline

Flutter Dart Framework原理简解

布局的计算

要获得每个 Widget的真实视图数据,布局是第一步。布局可以计算出每个节点所占空间的真实大小。在构建视图树时,节点的 size约束是从树顶部向底部的,而在计算布局的时候,遍历树是深度优先,所以获得布局数据的顺序是自下而上的。和 Web一样,Flutter的布局也是基于盒子模型,并且参考了众多布局方案设计而成 (毕竟负责这一部分的 Adam Barth也是 Blink项目的核心开发者),这部分的核心设计比较复杂,我们完全可以另开一篇文章,这里不再展开。

Flutter Dart Framework原理简解

视图的绘制

绘制决定了一个视图节点的真实外观。和布局不太一样的是绘制的顺序,布局会优先计算子节点,而绘制会优先绘制父节点。所以在数据流上看起来两个流程的方向是相反的。

Flutter Dart Framework原理简解

绘制要做的事就是计算出一个 Layer的外观,这通常都不简单,因为开发者经常会在视图上放各种各样的控件,并且他们的层级也不一样,所以绘制的步骤要做的事情就是决定一个 Layer的某一部分长什么样,这也可以看做视图的局部合成。举个例子:

Flutter Dart Framework原理简解

在进行绘制时,每个节点都会先绘制自身,其次才是子节点。比如节点 2是一个背景色绿色的视图,在绘制完自身后,绘制子节点 3和 4,它们可能具有 alpha属性,所以绘制后当布局重叠时会合成红色的节点 5。所以从数据流方向看的时候,获得最终的 Layer的顺序反而是自下而上的。

Flutter Dart Framework原理简解

一个 Layer是一份矢量数据,挂载到 LayerTree后还会经过 Engine的合成和光栅化才能提交给 GPU。RenderObject并不是和 Layer一一对应的,所以需要 paint过程将 RenderObject转化为 Layer

性能优化

视图树会不断更新,这就意味着布局和绘制是不间断的。我们上面提到的布局和绘制只是九牛一毛,Flutter其实在这方面做了很多事情,不间断的布局和绘制肯定会非常耗费性能,所以 Flutter也有对应的优化方案(在 Flutter的布局和绘制算法足够优秀的前提下)。

边界:Flutter使用边界标记需要重新布局和重新绘制的节点部分,这样就可以避免其他节点被污染或者触发重建,这个边界被分别叫做 Relayout boundary 和 Repaint boundary。

Flutter Dart Framework原理简解 Flutter Dart Framework原理简解

缓存:要提升性能表现,缓存也是少不了的。在 Flutter中,几乎所有的 Element都会具有一个 key,这个 key是唯一的。当子树重建后,只会刷新 key不同的部分。而节点数据的复用就是依靠 key来从缓存中取得。

Flutter 的渲染机制可以当做一本教科书,本文只能管中窥豹了

挂载 LayerTree

Layer 的抽象在 Flutter中是真实存在的, RenderObject 在经过 paint生成 Layer后,会通过 dart:uiSceneBuilder 挂载到 LayerTree。

/// Override this method to upload this layer to the engine.
///
/// The `layerOffset` is the accumulated offset of this layer's parent from the
/// origin of the builder's coordinate system.
void addToScene(ui.SceneBuilder builder, Offset layerOffset);

这是一个抽象接口,不同的 Layer(如 ContainerLayer、TextureLayer等)在继承抽象类后自行处理逻辑,但无外乎都会最终调用 builder.addXXX 这类方法,把传递的 Layer真正地挂载上去。

接下来的事情上一篇文章也提过了,Skia在 VSync信号同步时直接从 LayerTree合成 Bitmap,(经过光栅化后)提交给 GPU

Flutter 的通信机制

读完上面大概能了解到 Flutter怎么通过固定的数据结构构建并渲染丰富多彩的视图。但文章开头说过,用户输入才是驱动视图更新的源泉,所以需要专门讨论一下 Flutter是如何获得用户输入并与平台通信的。

用户输入

在当今的移动设备上传感器有很多,屏幕输入也可以算作一种。所以这里以讨论手势输入为例。

Flutter通过 Native方法捕捉用户的屏幕输入,这些数据被封装成一个一个的数据包,通过 Dart的 runtime hook发送给 dart:ui 中的 _dispatchPointerDataPacket 方法。这个方法会直接将屏幕输入数据分发给 window ,所以在 Flutter中,我们可以将触摸时间看作发生在全局的 window 上。

// Shell
// 将封装好的手势数据 packet发送给 engine
blink::Threads::UI()->PostTask(fxl::MakeCopyable(
      [ engine = _platformView->engine().GetWeakPtr(), packet = std::move(packet) ] {
        if (engine.get())
          engine->DispatchPointerDataPacket(*packet);
      }));
      

// engine直接 通过 libtonic hook Dart的 _dispatchPointerDataPacket 方法将数据包传递过去
void RuntimeController::DispatchPointerDataPacket(
    const PointerDataPacket& packet) {
  TRACE_EVENT1("flutter", "RuntimeController::DispatchPointerDataPacket",
               "mode", "basic");
  GetWindow()->DispatchPointerDataPacket(packet);
}

void Window::DispatchPointerDataPacket(const PointerDataPacket& packet) {
  tonic::DartState* dart_state = library_.dart_state().get();
  if (!dart_state)
    return;
  tonic::DartState::Scope scope(dart_state);

  Dart_Handle data_handle = ToByteData(packet.data());
  if (Dart_IsError(data_handle))
    return;
  DartInvokeField(library_.value(), "_dispatchPointerDataPacket",
                  {data_handle});
}

Flutter在接收到数据包后,识别触摸点发生的坐标,通过数学计算来识别手势。这和 Native框架的逻辑几乎是一样的。

Native通信

但除了手势这种特定的输入,还有其他传感器和事件的传递被抽象得更通用,叫做 MethodChannel 。这样就可以避免用户每次需要开发一个 Native extension时都要写 Native Binding了。 MethodChannel的原理和手势的传递几乎是一样的,但是 API会更通用一点。

Flutter Dart Framework原理简解

在这篇教程中 PlatformChannels 示例了一个如何通过 Native 获取电池电量,并通过 PlatformChannel传递给 Flutter应用。而这个 PlatformChannel像手势数据分发那样,换了一个方法:

void Window::DispatchPlatformMessage(fxl::RefPtr<PlatformMessage> message) {
  tonic::DartState* dart_state = library_.dart_state().get();
  if (!dart_state)
    return;
  tonic::DartState::Scope scope(dart_state);
  Dart_Handle data_handle =
      (message->hasData()) ? ToByteData(message->data()) : Dart_Null();
  if (Dart_IsError(data_handle))
    return;

  int response_id = 0;
  if (auto response = message->response()) {
    response_id = next_response_id_++;
    pending_responses_[response_id] = response;
  }

  DartInvokeField(
      library_.value(), "_dispatchPlatformMessage",
      {ToDart(message->channel()), data_handle, ToDart(response_id)});
}

通过这种打开一个 Channel来进行 request/response 的抽象概念可以实现任何用户自定义的 extension。

The End

Flutter是 engine和 Dart Framework的统称,目前 Flutter还处于 beta阶段,虽然说大致原理不会变,但一些细节部分还是会有不少改动。本文只能看个大概,因为,信息量实在是太大了。

好了,原理分析告一段落,在后面的使用中,我们再来细细探讨 Flutter的一些设计理念。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

进化式运营:从互联网菜鸟到绝顶高手

进化式运营:从互联网菜鸟到绝顶高手

李少加 / 电子工业出版社 / 2016-11 / 59

互联网运营作为一个新兴的岗位,一方面它是企业的核心岗职,身负重任,另一方面,又由于其短暂的历史,缺乏成熟体系的工作方法论,而目前业界主流的运营方法却是从企业视角出发,存在极大的改进空间。 《进化式运营:从互联网菜鸟到绝顶高手》作者基于自身十年的互联网洞察、实践经验,并融合了信息论、心理学、经济学、管理学、甚至包括生态学、进化论等跨学科跨学业的知识,从无到有地构建了一套全新的互联网运营体系:基......一起来看看 《进化式运营:从互联网菜鸟到绝顶高手》 这本书的介绍吧!

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具