分享

Flutter渲染原理解析

 hewii 2022-10-18 发布于上海

什么是Flutter

众所周知 Flutter是由Google推出的开源的高性能跨平台框架,一个2D渲染引擎。在Flutter中,Widget是Flutter用户界面的基本构成单元,可以说一切皆Widget。下面来看下Flutter框架的整体结构组成。

  • Flutter Framework:

纯 Dart实现的 SDK,类似于 React在 JavaScript中的作用。它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。

  • Flutter Engine:

纯 C++实现的 SDK,其中包括 Skia引擎、Dart运行时、文字排版引擎等。它是 Dart的一个运行时,它可以以 JIT 或者 AOT的模式运行 Dart代码。这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。

绘图原理

屏幕显示器一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,这时显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,GPU渲染好传递给显示器显示。

Flutter也遵循了这种模式,渲染流程如下图:

GPU的VSync信号同步给到UI线程,UI线程使用Dart来构建抽象的视图结构(这里是Framework层的工作,后面会进行详细讲解),绘制好的抽象视图数据结构在GPU线程中进行图层合成(在Flutter Engine层的工作),然后提供给Skia引擎渲染为GPU数据,最后通过OpenGL或者 Vulkan提供给 GPU。

从上图可知Flutter Engine中的一些绘图原理,Flutter渲染UI的本质就是在VSync信号中快速构建并提供抽象的视图结构数据,所以Flutter的精髓在Flutter Dart Framework里,这里涉及了很多优秀的设计思想、优化策略,下面来分析一下其中的原理。

Flutter Dart Framework 原理介绍

Framework的最底层叫做Foundation,其中定义的大都是非常基础的、提供给其他所有层使用的工具类和方法。

绘制库(Painting)封装了Flutter Engine提供的绘制接口,主要是为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,比如绘制缩放后的位图、绘制文本、插值生成阴影以及在盒子周围绘制边框等等。

Animation是动画相关的类。

Gesture提供了手势识别相关的功能,包括触摸事件类定义和多种内置的手势识别器。

如果使用Flutter提供的控件进行开发,则需要使用WidgetsFlutterBinding,如果不使用Flutter提供的任何控件,而直接调用Render层,则需要使用RenderingFlutterBinding,而我们平时看到的大多数开发案例都是使用WidgetsFlutterBinding的。

Flutter在Android和iOS两个平台,还提供了两套设计语言的控件实现Material & Cupertino,以提供更好的用户体验。

UI界面绘图流程

从上图我们可以看到,更进一步描述了开始的渲染流程图,用户输入是驱动视图更新的信号,如滑动屏幕等。然后会触发动画进度更新,框架开始build抽象视图数据,在之后,视图会进行布局、绘制、合成(渲染过程的三个步骤),最后进行光栅化处理把数据生成一个个真正的像素填充数据。在Flutter中,构建视图数据结构、布局、绘制、合成、与Engine的数据同步和通信放到了Framework层,而光栅化则放在了Engine层中。

视图数据结构

上面说到UI线程使用Dart来构建抽象的视图结构,无论是比较底层的框架,还是上层的应用代码,在向绘制引擎提供视图数据时,都需要一份结构化的视图数据,类似抽象语法树,也就是上面所讲到的Layer Tree,Flutter的视图数据抽象分为3部分,分别是Widget、Element、RenderObject。

Widget

Widget里面存储了一个视图的配置信息,包括布局、属性等。它是一份轻量的数据结构,在构建时是结构树,它不参与直接的绘制,所以说Widget仅仅是配置文件,Flutter团队对它做了优化,频繁的创建/销毁它们,都不会存在明显的性能问题。

Widget包含StatelessWidget和StatefullWidget两个常用类,StatelessWidget是无状态变化的类,需要重新展示时得重新new,StatefullWidget是有状态变化的类,很类似react的设计理念,state存放于中间,通过调用state.setState()才会触发该节点及以下整个子树更新。

Element

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

Element是Widget的抽象,当一个Widget首次被创建的时候,那么这个Widget会通过Widget.createElement,创建一个element,挂载到Element Tree遍历视图树。在attachRootWidget函数中,把 widget交给了 RenderObjectToWidgetAdapter这座桥梁,Element创建的同时还持有 Widget和 RenderObject的引用。构建系统通过遍历Element Tree来创建RenderObject,每一个Element都具有一个唯一的key,当触发视图更新时,只会更新标记的需变化的Element。类似react中setState后虚拟dom树的更新。

RenderObject

RenderObject作为UI视图的描述方式,其中含有4个重用的属性和方法,

  • constraints: 从 parent 传递过来的约束。

  • parentData: 这里面携带的是 parent 渲染 child 的时候所用到的数据。

  • performLayout():此方法用于布局所有的 child。

  • paint():这个方法用于绘制自己或者 child。

后面会看到具体出现场景。

在 RenderObject树中会发生 Layout、Paint的绘制事件(下面会具体讲到),大部分绘图性能优化发生在这里,RenderObject Tree构建为Canvas的所需描述数据,加入到Layer Tree中,最终在Flutter Engine中进行视图合成并光栅化交给GPU。

接下来看下Flutter Widget渲染的三个阶段:

  • Layout(布局的计算):确定每个子widget大小和在屏幕中的位置。

  • Paint(视图的绘制):为每个子widget提供canvas,让他们绘制自己。

  • Composite(合成):所有widget合成到一起,交给GPU处理。

Layout 布局

  1. 父控件(parent)将布局约束传递给子控件(child),父控件通过传递Containers参数,告诉子控件自己的大小(布局约束),以此来决定子控件的位置。

  2. 子控件将布局详情上传给父控件,并继续向下约束子控件,子控件的位置不存储在自己的容器(布局详情)中,而是存储在自己的parentData字段里,所以当他的位置发生变化时,并不需要重新布局或绘制。

例如:parent会给child一个约束,最大宽度500px(布局约束)、最大高度500px(布局约束),child会说我只用100px(布局详情),并将其传递给parent,parent会继续向上传递,直到root widget为止。所以布局约束数据的传递顺序是自上而下,和web一样,布局约束条件和布局详情都取决于盒子模型协议和滑动布局协议。

性能优化(布局)

在上面的布局过程中,视图会不断更新,也就不断的触发布局和绘制,这会很损耗性能,所以这里也就到了之前说的大部分绘图性能优化的发生地方。Flutter可以在某些节点设置布局边界 Relayout boundary(Paint过程同样可以设置 Repaint boundary 进行优化),需要开发人员自己设置,边界内的控件发生重新布局或绘制时,不会影响边界外的控件。

Paint 绘制

布局完成后,每个节点就会有各自的位置和大小,然后Flutter会把所有Widget绘制到不同的图层上,如下图:

以上图为例,在进行绘制时会自上而下,先绘制自身,然后向下绘制子节点,原本会统一绘制在绿色图层上,当绘制到节点“4”时,由于节点“4”可能是视频,需要单独占据一个图层(黄色图层),这样就会导致节点“2”的前景部分需要重绘,而影响到了后面的节点“6”一起重绘,占据到蓝色图层。

性能优化(绘制)

为避免这种情况,Flutter提供了重绘边界 Repaint boundary,设置了重绘边界后,Flutter会强制切换到新的图层,避免之间的相互影响,节点“6”会换到红色图层中。

Flutter 和 Native 的通信

Flutter定义了三种Channel,分别为:

  • BasicMessageChannel:用于传递字符串和半结构化的信息。

  • MethodChannel:用于传递方法调用(method invocation)。

  • EventChannel: 用于数据流(event streams)的通信。

具体和Native间的消息传递,是通过MethodChannel完成的。MethodChannel入参为name和MethodCodec参数,name为MethodChannel的标识,而MethodCodec是个消息编/解码器,当我们使用 invokeMethod 发起一次方法调用时,就开始了消息传递,invokeMethod 会将其入参message和arguments封装成一个MethodCall对象,并使用MethodCodec将其编码为二进制格式数据,再通过BinaryMessages将消息发到Platform。结果回传时,Platform端会将二进制结果转化为Dart的二进制数据类型,通过回调传递给Dart层。Dart接收到二进制结果后,再使用MethodCodec将数据解码,传回到上层业务,至此,一次完成的消息传递就结束了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多