likes
comments
collection
share

Flutter渲染原理系列(二)——UI加工厂之产品需求

作者站长头像
站长
· 阅读数 35

前言

本篇是Flutter渲染原理系列的第二篇,上一篇中Flutter渲染原理系列(一)——生产消费者模型我们主要介绍了Flutter渲染流程的基本概念及流程。这一篇我们由整体到局部,深入分析Flutter UI框架这个UI加工厂的实现逻辑。

通过对Flutter UI框架进行分析,相信读者无论是从事Flutter、Android/iOS或Web开发,都将对自身技术栈所使用的UI框架有更深入的理解。

导读

本文将着重介绍Flutter渲染流程中刷新的需求节点及UI框架的初始化过程,而下一篇文章将围绕数据加工过程展开,深入探讨Flutter中的布局、绘制和合成等流程,以帮助读者更全面地理解Flutter渲染的实现原理。

  • 产品(刷新)需求

在分析加工流程之前,对触发刷新的需求节点进行分析,同时也分析App UI框架的初始化过程。

  • 材料组装

下一篇内容,在本章中,我们将介绍什么是Widget树,以及为什么需要重新组装。我们将深入探讨Flutter中当组件刷新时,Widget树会如何重新组装,并对组装的过程进行详细分析。

  • 材料加工

下一篇内容,在本章中,我们将对布局、绘制、合成这三个流程进行更详细的解释。我们将介绍在UI框架中每个流程的具体实现方式,并探讨Flutter是如何实现这些流程的。

目录

1. 产品(刷新)需求

2. 总结和展望

1.产品(刷新)需求

在讨论如何进行Widget树的重新构建、布局、绘制和合成之前,需要先思考如何启动后续的加工流程。就像普通的加工厂在没有需求的时候不会开启机器开始工作一样,Flutter中的渲染流程也需要被需求(刷新)触发。因此,在分析材料组装和加工的流程之前,需要先了解Flutter中刷新的触发方式和渲染流程的启动机制。

在Flutter中,刷新的触发节点主要有以下三个:

  • App启动及首帧渲染
  • setState() 方法调用节点
  • 路由切换新页面

可以将整个加工过程描绘成下面这张图:

Flutter渲染原理系列(二)——UI加工厂之产品需求

因此,在分析材料组装之前,我们需要先了解刷新需求的三个触发节点。

1.1 App启动及首帧渲染

工厂启动的第一步是进行准备工作,这对应于在main.dart文件中调用runApp方法,这个方法会初始化Flutter渲染工厂的运行环境,类似于打开机器、雇佣工人等操作。在接下来的流程中,只需要处理刷新节点对应的工厂订单即可。

我们可以将Flutter工厂中的关键组成部分绘制成一张图,这些关键组成部分都会在启动时生成以便于后续工厂运行:

Flutter渲染原理系列(二)——UI加工厂之产品需求

下面来看下runApp(widget),这个方法在binding.dart文件中,代码示例如下:

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

参数是widget,也就是开发者自定义的最外层的Widget,相当于我们能接触到的所有Widget的父Widget。 其中WidgetsFlutterBinding,顾名思义加上注释可以得知,这是一个将Flutter Framework层 和 Flutter 引擎绑定的关键类,其中Flutter Framework包含UI框架,Flutter 引擎包含了Skia渲染引擎。

这里App初始化运行环境分为三步:

  1. ensureInitialized: 初始化各种binding类,相当于聘请各种车间管理员来操作机器。
  2. scheduleAttachRootWidget: 开启WidgetBinding管理的UI框架机器,随时准备工作。
  3. scheduleWarmUpFrame: SchedulerBinding管理员触发首次刷新需求。

1.1.1 ensureInitialized

ensureInitialized会调用WidgetsFlutterBinding的构造函数,其中WidgetsFlutterBinding混入了很多的binding类,这些类就相当于整个Flutter App运行时的管理员,负责沟通及机器操作,给整个App运行提供必要的功能。

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

通过查看这些管理员既Binding的职责(源码),我们可以发现这些Binding中基本都是监听并处理Window对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。

主要管理员如下:

管理员功能
GestureBinding单例对象,它负责处理所有手势事件的分发和管理,是Framework事件模型与底层事件的绑定入口。
ServicesBinding主要负责管理和绑定系统服务,如网络服务、文件系统服务、蓝牙服务等等。
SchedulerBinding负责管理和调度Flutter的任务队列。它的主要作用包括:1,管理和调度任务队列如UI任务、IO任务和计时器任务 2,处理帧回调 3,处理系统事件如触摸事件、指针事件、键盘事件等
PaintingBinding绑定绘制库,主要用于处理图片缓存
SemanticsBinding语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持
RendererBindingFlutter 渲染引擎的入口点,它负责构建渲染树和管理它的布局、绘制、合成和提交。它是一个单例对象,可以通过 RendererBinding.instance 获取
WidgetsBindingWidgetsBinding 是 Flutter 中的一个绑定类,它是将 GestureBindingServicesBindingSchedulerBindingPaintingBindingRendererBinding 等各个绑定类协调工作的关键类。它主要的作用是管理视图层次、处理用户输入和通知应用程序生命周期事件等。

这其中最关键的管理员是SchedulerBindingWidgetsBindingRendererBinding,他们负责管理后续材料组装和材料加工的关键类,那么我们先来看看,在WidgetsFlutterBinding调用initInstances()方法时WidgetsBindingRendererBinding中的实现:

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
    @override
    void initInstances() {
      super.initInstances();
      _pipelineOwner = PipelineOwner(
        onNeedVisualUpdate: ensureVisualUpdate,
        onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
        onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
      );
    }
}


mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _buildOwner = BuildOwner();
    buildOwner!.onBuildScheduled = _handleBuildScheduled;
  }
}

这两个管理员中分别包含了BuildOwnerPipelineOwner这两条流水线的初始化,而他们就是材料组装和材料加工这两条关键生产环节,负责整个流程的执行,具体会在后续章节介绍。

到这里,工厂聘请了管理员,开启了BuildOwnerPipelineOwner这两条流水线,现在缺少的是UI框架和Skia渲染引擎这两台机器的启动,而开启过程在方法scheduleAttachRootWidget中。

1.1.2 scheduleAttachRootWidget

Skia渲染引擎是用C++编写的,因此在原生平台上启动时已经就绪。Android平台自带Skia C++库,而iOS需要手动引入。在Flutter中,实际上只是启动了UI框架,将根Widget添加到RenderView中。RenderView是一个RenderObject,其实现代码如下:

void attachRootWidget(Widget rootWidget) {
  final bool isBootstrapFrame = renderViewElement == null;
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  if (isBootstrapFrame) {
    SchedulerBinding.instance.ensureVisualUpdate();
  }
}

scheduleAttachRootWidget方法在WidgetBinding类中,这其中涉及到Widget、Element、RenderObject互相绑定的过程,要弄清UI框架的结构,就要理清Widget、Element、RenderObject之间的关系。

Widget->Element->RenderObject

Widget是UI元素的描述,Element是UI元素的一个实例,而RenderObject则是UI元素在屏幕上的渲染结果。它们三者之间的关系可以概括为:Widget->Element->RenderObject

Widget树
Element树
RenderObject树

Widget是一个描述UI元素的配置数据结构,定义了UI元素的属性和子元素等信息。Widget是不可变的,也就是说,如果需要修改UI元素的属性或结构,需要创建一个新的Widget。Flutter框架将Widget看作UI元素的一个“模板”,通过它创建Element

Element是Flutter框架中UI元素的一个实例,它是Widget的一个具体实现,负责管理与该UI元素相关的状态和生命周期,并负责将Widget转化为RenderObject,最终渲染到屏幕上。

RenderObject是Flutter框架中的渲染树节点,它是Flutter渲染引擎的一个抽象,用于描述UI元素在屏幕上的布局和绘制。每个Element都会对应一个RenderObject,负责将Element所描述的UI元素渲染到屏幕上,更多详细的绑定过程将在下一篇介绍。

在调用scheduleAttachRootWidget方法时,UI框架会被启动,UI框架是由以下几个关键零件构成的:

  • _renderViewElement
  • RenderObjectToWidgetAdapter
  • RenderBox
  • RenderView
  • rootWidget
  • buildOwner

下面我们分别来介绍下这些零件在UI机器中的作用。

_renderViewElement

_renderViewElement 是一个Element对象,它是Element树的根部,这样我们除了初始化时传入的关键对象rootWidget,第二个关键根Element类也出现了。

RenderObjectToWidgetAdapter

顾名思义就是RenderObject的适配器,是RenderObjectElement树之间的桥梁,其构造函数传入了三个参数:

  • container
  • child
  • debugShortDescription

container是作为容器RenderObject传入,必须集成自RenderObjectWithChildMixin<T>,其中泛型传入的是RenderBox

child传入的rootWidgetdebugShortDescription用来做debug相关描述的类。

RenderBox

Flutter的渲染引擎使用一个基于渲染树的模型,即通过不同的渲染对象组成树形结构,然后将这个树形结构渲染到屏幕上。

在这个树形结构中,RenderBox渲染对象的一种,它代表一个矩形区域,并提供了处理它的方法,比如绘制、布局和处理手势等。具体来说,RenderBox定义了以下方法:

方法功能
layout用于计算和设置渲染对象的位置和大小
paint用于绘制渲染对象的内容
hitTest用于处理手势事件
performResize用于处理父容器的约束变化
performLayout用于在布局阶段处理子元素的位置和大小

可以看出,RenderBox是Flutter渲染引擎的基础,它提供了处理布局、绘制和事件的基本方法,使得Flutter能够快速高效地创建复杂的用户界面。

除了RenderBox以外,Flutter还有其他几种常见的渲染对象,包括:

渲染对象功能
RenderObjectWidge它是一个抽象类,用于将widget转换为渲染对象。每个RenderObjectWidget都有一个对应的RenderObject子类,用于处理实际的渲染操作。
RenderFlex继承自RenderBox,用于实现弹性盒子布局模型,即Flex布局。
RenderViewport用于实现可滚动的视图区域。它可以包含任何子元素,但只会绘制那些可见的子元素。
RenderParagraph继承自RenderBox,用于实现图片的加载和渲染。它支持各种图片格式和缩放方式,并提供了加载和缓存图片的方法。
RenderImage继承自RenderBox,用于实现图片的加载和渲染。它支持各种图片格式和缩放方式,并提供了加载和缓存图片的方法
RenderSliver基于Sliver布局模型的渲染对象,用于实现可滚动的列表、网格、悬浮头部等复杂的滚动效果

这些渲染对象都继承自RenderObject类,并根据自己的特性提供了各种处理布局、绘制和事件的方法。开发者可以自由地组合这些渲染对象来构建出符合自己需求的用户界面。

RenderView

RenderView是Flutter应用程序中最顶层的渲染对象,它负责将整个布局树渲染到屏幕上。在Flutter应用程序中,每个应用程序只有一个RenderView对象。

RenderView对象会遍历整个布局树,并调用每个子对象的performLayout()方法进行布局,然后再调用每个子对象的paint()方法进行绘制。在Flutter中,每个渲染对象都有自己的父级对象和子对象,形成了一棵树状结构。

总体来说,RenderView在Flutter应用程序中扮演着重要的角色,它是布局树的管理员,负责将整个布局树渲染到屏幕上。

rootWidget

rootWidget是通过runApp传入的开发者可接触的最顶层的Widget,算是应用程序的入口点,所有其他Widget都是从根Widget开始构建的。

根Widget通常是MaterialApp、CupertinoApp或WidgetsApp这样的应用程序级别的Widget,它们提供了应用程序级别的配置,如主题、语言、导航等。在根Widget之下,可以嵌套其他的Widget,这些Widget构成了应用程序的页面、视图和控件等。

buildOwner

在Flutter中,BuildOwner是一个核心类,它负责管理Flutter框架中的所有Element对象,以及它们对应的Widget树和渲染树,同时负责Widget的重新构建,是一条重要的流水线。

具体来说,BuildOwner扮演了一个中介的角色,它在Widget树和渲染树之间进行数据传递和事件处理,以确保它们之间的一致性和同步。它的主要职责包括:

  • 维护Element对象的注册和注销,为每个Element对象创建一个BuildContext,并将它们连接到树中。
  • 调用Element对象的build方法,将Widget转化为Element对象,并在需要时更新Element对象的状态。
  • 根据Element对象的配置和约束,构建渲染树,并将其附加到视图中。
  • 处理Element对象的事件,如点击、滑动等,将事件传递给对应的Element对象进行处理。
  • 提供一些额外的方法,如scheduleBuildForrebuildAllWidgets等,用于手动触发Widget树的重建和渲染。

简单来讲,BuildOwner负责管理Element对象和Widget树,以及与渲染树之间的数据传递和事件处理,是实现Flutter应用程序的核心组件之一。

上面介绍完了机器零件,UI机器的启动就是当零件运转起来后调用attachToRenderTree方法将rootWidget对象附加到渲染树中,这样整个UI框架便运转起来,接下来就需要等待工厂订单了。

这里的工厂订单就是“刷新需求”,在App初始化时首帧渲染便是调用scheduleWarmUpFrame方法主动触发刷新。

1.1.3 scheduleWarmUpFrame

在了解“刷新”之前,我们需要先了解Flutter中关于“帧”相关的概念,这些概念包含:

概念介绍
帧(Frame)帧(Frame)是指屏幕上的一帧画面,它由一组连续的图像构成,通常每秒钟会刷新60次,也就是60帧每秒(60 FPS)
VSync信号VSync信号是系统每秒钟发送的定时信号,通常是约16.7ms发送一次,用于通知Flutter框架需要开始下一帧的渲染和绘制
FrameCallbackFrameCallback是Flutter框架提供的一个回调函数,它会在每一帧绘制完毕之后被调用,开发者可以在这个回调函数中进行一些额外的操作,例如更新状态、执行动画等
TickerTicker是Flutter框架提供的一个计时器对象,它会每一帧都执行一次回调函数,通常用于执行动画、定时器等功能
SchedulerBinding在前面介绍过,SchedulerBinding是Flutter框架中的调度器对象,它负责处理帧刷新、VSync信号等任务,并将这些任务分发到各个子系统中执行

在Flutter应用程序初始化时真正的“刷新”触发节点在scheduleWarmUpFramescheduleWarmUpFrame方法可以用来触发一次“热身”帧的渲染,以加速应用程序的启动过程。

具体来说,scheduleWarmUpFrame方法会将一个WarmUpFrameCallback回调函数注册到Flutter引擎中,该回调函数会在下一帧渲染之前被执行。这个回调函数通常会在渲染完成后立即注销,以确保它只被执行一次。

通过执行热身帧渲染,Flutter应用程序可以预先准备渲染资源并进行必要的缓存,以提高后续渲染帧的性能和响应速度。在应用程序启动时,特别是在首次启动时,这个优化可以明显降低应用程序的启动时间和帧率抖动,提高用户体验。

以上就是关于Flutter App启动时的准备工作(UI框架初始化)及首帧渲染,实际在开发过程,“刷新需求”大多来自于SetState方法。

1.2 SetState方法调用

每个技术栈中都有一个触发UI刷新的方法,在Flutter中是SetState,当调用setState方法时:

void setState(VoidCallback fn) {
  _element!.markNeedsBuild();
}

void markNeedsBuild() {
  if (dirty)
    return;
  _dirty = true;
  owner!.scheduleBuildFor(this);
}


void scheduleBuildFor(Element element) {
  if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
    _scheduledFlushDirtyElements = true;
    onBuildScheduled!();
  }
  _dirtyElements.add(element);
  element._inDirtyList = true;
}

通过代码发现主要分为三步:

  1. 第一步是调用当前 element 的 markNeedsBuild 方法,将 element标记为 dirty状态。
  2. 第二步在markNeedsBuild方法中调用 scheduleBuildFor,将当前 element 添加到BuildOwner的 dirtyElements 列表中。
  3. 最后通过请求一个新的 frame,随后会绘制新的 frame:onBuildScheduled->ensureVisualUpdate->scheduleFrame() 。当新的 frame 到来时执行渲染管线.

整个流程可以用如下的时序图表示:

StateelementBuildOwnerWidgetbindingSchedulerBindingPlatformDispatchersetStatemarkNeedsBuildscheduleBuildForonBuildScheduled_handleBuildScheduledensureVisualUpdatescheduleFramescheduleFrameStateelementBuildOwnerWidgetbindingSchedulerBindingPlatformDispatcher

在将需要更新的element添加的dirty列表中后,最终通过SchedulerBinding中的ensureVisualUpdate请求新的frame。

void ensureVisualUpdate() {
  switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame();
      return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
      return;
  }
}

void scheduleFrame() {
  if (_hasScheduledFrame || !framesEnabled)
    return;
  ensureFrameCallbacksRegistered();
  platformDispatcher.scheduleFrame();
  _hasScheduledFrame = true;
}


/// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
/// [onDrawFrame] callbacks be invoked.
///
/// See also:
///
///  * [SchedulerBinding], the Flutter framework class which manages the
///    scheduling of frames.
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';

这其中涉及到两个关键类SchedulerPhasePlatformDispatcher

SchedulerPhase

SchedulerPhase是一个枚举类型,它表示Flutter调度程序中的不同阶段。Flutter调度程序是Flutter框架的一个重要组件,负责管理应用程序的主事件循环和动画渲染

SchedulerPhase枚举类型中包含以下几个值:

  • idle:空闲阶段,Flutter调度程序当前没有要处理的任务。
  • transientCallbacks:短时任务阶段,Flutter调度程序当前正在处理短时任务(比如微任务)。
  • persistentCallbacks:长时任务阶段,Flutter调度程序当前正在处理长时任务(比如网络请求或文件读写操作),渲染任务就是通过就是在这个阶段执行的
  • postFrameCallbacks:帧后任务阶段,Flutter调度程序当前正在处理帧后任务(比如动画更新或布局重建)。

总体来讲分为 idle 和 frame 两种状态,渲染过程状态往往在idleframe这两种状态之间进行切换,而渲染任务是在persistentCallbacks阶段。

PlatformDispatcher

PlatformDispatcher是一个抽象类,全局单例,它定义了Flutter引擎和操作系统之间的通信接口,负责将Flutter应用程序的UI事件和系统事件进行转换和传递,具体的实现由不同的平台(比如Android、iOS、Web等)进行实现。开发者可以通过PlatformDispatcher提供的接口来与操作系统进行交互,并实现Flutter应用程序与操作系统的无缝集成。

具体来说,PlatformDispatcher的主要作用包括:

  1. 将Flutter应用程序的UI事件(比如点击、滑动、键盘输入等)转换为操作系统的事件(比如鼠标事件、触摸事件、键盘事件等),并发送给操作系统。
  2. 接收操作系统的事件,将其转换为Flutter应用程序的UI事件,并传递给Flutter引擎处理。
  3. 同步Flutter应用程序的UI状态和操作系统的状态,保证两者的一致性。

而在渲染流程阶段,PlatformDispatcher是Flutter的渲染引擎和平台之间的桥梁,它定义了一些回调方法来处理与平台交互的各种事件。其中,onBeginFrameonDrawFrame是两个非常重要的回调方法。

当Flutter请求新的frame时,onBeginFrameonDrawFrame方法会被调用,Flutter会通过SchedulerBinding对象调用drawFrame方法来执行一帧的绘制操作。

当一帧绘制完成后,onDrawFrame方法会被调用,这个方法的主要作用是通知平台当前帧的绘制已经完成。此时,平台会将绘制缓冲区中的内容显示到屏幕上,并回收缓冲区的内存。同时,Flutter会根据需要进行下一帧的绘制操作,从而实现动态的界面刷新。

整个流程可以用时序图表示:

PlatformDispatcherSchedulerBindingonBeginFrame_handleBeginFramehandleBeginFramePlatformDispatcherSchedulerBinding

接下来是onDrawFrame的调用:

PlatformDispatcherSchedulerBindingRendererBindingWidgetsBindingonDrawFrame_handleOnDrawFramehandleOnDrawFrame_invokeFrameCallback_persistentCallbacks_handlePersistentFrameCallbackdrawFramedrawFramePlatformDispatcherSchedulerBindingRendererBindingWidgetsBinding

也就是说每次刷新需求到来时,底层会调用PlatformDispatcheronBeginFrameonDrawFrame来实现后续流程的材料组装材料加工过程:

@override
void drawFrame() {
  try {
    if (renderViewElement != null)  
      buildOwner!.buildScope(renderViewElement!);//材料组装
    super.drawFrame();//材料加工
    buildOwner!.finalizeTree();
  } finally {
    assert(() {
      debugBuildingDirtyElements = false;
      return true;
    }());
  }
}

//上面的super.drawFrame()材料加工过程
@protected
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}

其中 buildOwner!.buildScope(renderViewElement!)对应的就是第二章的材料组装过程,RendererBinding中的drawFrame则代表材料加工过程。

1.3 路由切换

路由切换的本质就是 触发State对象的生命周期方法 Flutter会触发新页面State对象的生命周期方法,包括initState()build()方法。在这两个方法中,我们可以初始化页面的状态并渲染页面的UI,其他流程同上。

在第一章中,我们分析了Flutter应用程序在启动时所做的准备工作,以及在页面刷新时的三种需求场景。接下来,第二、三章将介绍整个渲染流水线的运行流程,以及当需求刷新时,各个组件之间是如何协同工作,最终将数据和样式渲染到屏幕上的。

2.总结和展望

本篇是Flutter渲染系列的第二篇,主要介绍Flutter渲染流程中刷新的需求节点及UI框架的初始化过程,我们分析了主要有三个刷新节点

  • App启动及首帧渲染
  • setState() 方法调用节点
  • 路由切换新页面

其中App启动过程也是UI框架初始化的过程,后面的setState()通常是开发者在代码中调用触发,路由切换本质是生命周期触发刷新,原理一样。

下一篇我们将对材料组装材料加工进行深入。具体可参见Flutter渲染原理系列(二)——UI加工厂之材料加工。请注意,如果链接无效,则说明该文章正在撰写中,请读者耐心等待。

转载自:https://juejin.cn/post/7226632657152704572
评论
请登录