美文网首页
Flutter 绘制原理浅析

Flutter 绘制原理浅析

作者: 黑色茄子 | 来源:发表于2022-12-31 21:59 被阅读0次

    框架

    • Framework:一个纯 Dart代码的 SDK。它实现了一套基础库, 包含动画、绘制和手势处理。并基于绘制封装了一套Widget控件库,这套控件库还根据 Material 和Cupertino两种设计风格进行了风格化区分。
    • Engine:一个 C++实现的 SDK。其包含了 Skia引擎、Dart运行时、文字排版引擎等。在安卓上,系统自带了Skia,在iOS上,则需要APP打包Skia库,这会导致Flutter开发的iOS应用安装包体积更大。 Dart运行时则可以以 JIT、JIT Snapshot 或者 AOT的模式运行 Dart代码。


      aHR0cHM6Ly9naXRlZS5jb20vYXJjdGljZm94MTkxOS9JbWFnZUhvc3RpbmcvcmF3L21hc3Rlci9pbWcvMV81TVk1eVJFclpjdjQ2bU4zZXI4SklBLnBuZw.png

      其中 dart:ui库是对Engine中Skia库的C++接口的绑定。向上层提供了 window、text、canvas等通用的绘制能力,通过 dart:ui库就能使用Dart代码操作Skia绘制引擎。所以我们实际上可以通过实例化dart:ui包中的类(例如Canvas、Paint等)来绘制界面。然而,除了绘制,还要考虑到协调布局和响应触摸等情况,这一切实现起来都异常麻烦,这也正是Framework帮我们做的事。

    渲染层Rendering是在dart:ui库之上的第一个抽象层,它为你做了所有繁重的数学工作(如跟踪计算坐标等)。为了做到这一点,它使用RenderObject对象,该对象是真正绘制到屏幕上的渲染对象。由这些RenderObject组成的树处理真正的布局和绘制。

    在Engine之下,还包含一层Shell。这个单词是 “壳”的意思,这个壳组合了Dart运行时、第三方工具库、平台特性等,实现在不同平台调用和运行 Flutter应用。

    总的来说, dart:ui给 Dart提供了绘制能力,Dart运行时为 Flutter提供了执行Dart代码的能力,而Shell将他们组合起来,并且将生成的数据渲染到不同的平台。
    我们可以简单的理解成,Skia 是打印机, Dart 运行时则是提供绘制的电源, dart: ui 提供纸张, Randering 是坐标系关系, 告诉打印机上下左右的, Text 是文字排版规范


    u=1549346277,408395929&fm=253&fmt=auto&app=138&f=JPEG.png
    注:

    目前,程序主要有两种运行方式:静态编译与动态解释。
    AOT: 静态编译的程序在执行前所有被翻译为机器码,一般将这种类型称为AOT (Ahead of time compiler)即 “提前编译”;如C、C++。
    JIT:解释执行的则是一句一句边翻译边运行,一般将这种类型称为JIT(Just-in-time)即“即时编译”。如JavaScript、Python。
    Dart中的JIT和AOT:
    Dart在开发过程当中使用JIT,所以每次改都不须要再编译成字节码。节省了大量时间。
    在部署中使用AOT生成高效的ARM代码以保证高效的性能。
    Dart 是少数同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编译)的语言之一。

    那么讲完Flutter框架的内容,我们来看看控件,也就是Flutter的树, Flutter有Widget树、Element树、RenderObject树,这是一般常说的三棵树, 如果算上Layer, 就是四棵树,它们各司其职,分成了几个相关联但清晰的结构

    Widget

    Widget 是 Flutter中UI开发的基本单元。 一个Widget里面通常存储了视图的配置信息,包括布局、属性等。我们可以把它理解为一个UI元素的配置文件,类似于原生安卓开发中的xml描述文件。所谓Widget树,就是我们手动编写的结构化的Widget代码,当被加载到内存时,就形成了Widget树。

    Element

    Element 持有Widget和RenderObject两者的引用,该对象实际上是一个上下文,将Widget与RenderObject映射关联起来。通过遍历Widget控件树来构建一个Element树结构。在原生开发中没有对应的概念,它的概念更接近于Web前端中的虚拟DOM,主要做的事情也是比较前后两次Widget的差异来决定如何更新真实的渲染对象树(RenderObject树)。

    RenderObject

    RenderObject 实际上需要渲染的树,渲染引擎会根据RenderObject 来进行界面渲染。最接近原生开发中的UI控件元素。它主要处理UI构建过程中的布局与绘制。它依赖于Element树来生成一棵RenderObject树。

    Layer

    图层对象。通常一棵RenderObject树经过绘制之后,就会生成一个Layer对象,但并不是所有RenderObject都会绘制到一个Layer中,某些情况下,例如不同路由页面,就会绘制到不同的Layer图层中。这些Layer对象组成的结构就是Layer树。

    在绘制时,会根据 isRepaintBoundary是否为 true来决定是否绘制到新的图层。了解这一点,我们就可以使用RepaintBoundary 控件在外层包裹,然后通过设置该控件的isRepaintBoundary属性来提升绘制性能。因为 isRepaintBoundary 为 true 时,会形成了独立的 Layer,这样其他控件发生频繁的改变时,就不会影响到独立的图层,这个独立的图层也不会发生重绘,节省性能开销。

    aHR0cHM6Ly9naXRlZS5jb20vYXJjdGljZm94MTkxOS9JbWFnZUhvc3RpbmcvcmF3L21hc3Rlci9pbWcvU25pcGFzdGVfMjAyMC0wNy0xMF8xNS00OC0yOS5qcGc.png

    四者关系我们可以简单的理解成:widget 是剧本, Element 是人, RenderObject 是皮偶, layer 是皮偶投射出来的影子,人通过剧本剧情来控制皮偶表演, 投射成一个皮影戏了


    u=4009286841,3784638912&fm=253&fmt=auto&app=138&f=JPEG.png
    树的创建
    1. 创建widget树
    2. 调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了


      866f32c865e3313cd029ae1f988a1a32.png.jpeg
    树的更新

    找到widget对应的element节点,设置element为dirty,最终会将需要更新的element加入到_dirtyElements的列表中,_dirtyElements是一个集合,存储了所有标记为“脏”的节点。在对其中的“脏”节点进行处理时,需要首先对集合中的“脏”节点进行排序,其排序规则如下:

    • 如果“脏”节点的深度不同,则按照深度进行升序排序
    • 如果“脏”节点的深度相同,则会将“脏”节点放在集合的右侧,“干净”节点则在在集合的左侧。
    • 在排序完成后,就要遍历该集合,对其中的“脏”节点进行处理。在这里调用的是rebuild函数,通过该函数,会重新创建“脏”节点下的所有Widget对象,并根据新的Widget对象来判断是否需要重用Element对象。一般只要不是增删Widget,Element对象都会被重用,从而也就会重用RenderObject对象。
    • 这里要注意一点的是,如果_dirtyElements中的“脏”节点还未处理完毕,就又新增了“脏”节点,那么这时候就会重新排序,保证_dirtyElements集合的左侧永远是“干净”节点,右侧永远是“脏”节点。

    在rebuild函数中会调用performRebuild函数,该函数是一个抽象函数,在其子类实现,而标记为“脏”的Element都是StatefulElement。所以就来StatefulElement或者其父类中查找performRebuild函数。
    performRebuild函数做的事很简单,就是创建新的Widget对象来替换旧的对象。再调用Element的updateChild函数,更新Element对应的Widget对象。而在updateChild函数中又会调用子Element的update函数,从而调用子Element的performRebuild,然后在调用子Element的updateChild、update函数。以此类推,从而更新其所有子Element的Widget对象。
    最后就是调用叶子节点的updateRenderObject函数来更新RenderObject。在更新RenderObject对象时,会根据情况来对需要重新布局及重新绘制的RenderObject对象进行标记。然后等待下一次的Vsync信号时来重新布局及绘制UI。

    updateChild函数中分了几种情况

    • 不存在原始child,则新创建新的widget,并重置element,关联renderobject
    • 如果新旧控件的类型相同,并且控件也相同,直接更新wiget
    • 如果新旧控件的类型相同,并且这个wiget 能复用(即widget.canupdate为true),则更新widget,并执行updateRenderObject
    • 如果新旧控件类型不同,则移出原有widget,并重新创建新的widget,并重置element,关联renderobject
    树的作用

    应该说多棵树结构的作用,简而言之是为了性能,为了复用Element从而减少频繁创建和销毁RenderObject。因为实例化一个RenderObject的成本是很高的,频繁的实例化和销毁RenderObject对性能的影响比较大,所以当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的Widget树,element对widget树的变化做了抽象,可以只将真正变化的部分同步给RenderObject进行刷新,这样就能提高渲染效率,而不是重新构建整个widget,对变化前后的数据进行比较,告诉render哪些是需要重新渲染的

    注:
    • 关于更新时,Element的变化:

    • 当有父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来避免复用。

    • 当有祖先级Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先级Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。

    • “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。

    • 如果element要重新插入到Element树的其它位置,如element或element的祖先级拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

    布局过程

    Flutter 中的控件在屏幕上绘制渲染之前需要先进行布局(Layout)操作。其具体可分为两个线性过程:

    从顶部向下传递约束。

    这一过程用于传递布局约束。父节点给每个子节点传递约束,这些约束是每个子节点在布局阶段必须要遵守的规则。常见的约束包括规定子节点最大最小宽度或者子节点最大最小的高度。这种约束会向下延伸,子组件也会产生约束传递给自己的子节点,一直到叶子结点。

    从底部向上传递布局信息。

    这一过程用来传递具体的布局信息。子节点接受到来自父节点的约束后,会依据它产生自己具体的布局信息,如父节点规定我的最小宽度是 500 的单位像素,子节点按照这个规则可能定义自己的宽度为 500 个像素,或者大于 500 像素的任何一个值。这样,确定好自己的布局信息之后,将这些信息告诉父节点。父节点也会继续此操作向上传递一直到最顶部。 其过程可用下图表示:


    2427642-c2ef2401a88c6496.png

    渲染流程

    上面内容是从控件和布局层面来说页面更新的,而宏观流程上,当需要更新页面的时候,由应用上层通知到Engine,Engine会等到下个Vsync信号到达的时候,去通知Framework上层,然后Framework会进行Animation, Build,Layout,Compositing,Paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过OpenGl接口提交数据给GPU, GPU经过处理后在显示器上面显示

    16fc0c8749b139cf~tplv-t2oaga2asx-zoom-in-crop-mark-4536-0-0-0.image.png

    合成与光栅化

    所有图层都交由 GPU 来负责合成并上屏显示。在渲染流程的最后两个步骤中,正是合成与光栅化。

    合成

    就是把所有layer组合成Scene,然后通过 ui.window.render 方法,把 scene提交给Engine,到这一步,Framework向Engine提交数据基本完成了

    光栅化

    合成已经理解了,那么什么是光栅化呢?


    aHR0cHM6Ly9naXRlZS5jb20vYXJjdGljZm94MTkxOS9JbWFnZUhvc3RpbmcvcmF3L21hc3Rlci9pbWcvMjAxMzA4MTkxMjU3Mjc3MDMucG5n.png

    光栅化也称栅格化,是指将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程。光栅化的本质是坐标变换、几何离散化。

    iOS与Flutter渲染对比

    iOS
    2650319-2921185cf230b199.jpg.png
    • 更新视图树,同步更新图层树。
    • CPU 计算要显示的内容、图像解码转换。当runloop 在BeforeWaiting和Exit时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程Render Server。
    • Render Server 将数据反序列化,得到图层树。按照图层树中图层顺序、RGBA 值、图层 frame 过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal。
    • Render Server 会调用 GPU,GPU 开始进行顶点着色器、形状装配、几何着色器、光栅化、片源着色器、测试与混合六个阶段。
    • 将GPU渲染结果放到帧缓冲区,当下个Vsync信号时,从帧缓冲区取出放到屏幕。
    flutter
    2650319-bc6c4d5c3a635836.png

    Flutter由Skia绘制引擎提供提供图形绘制能力,向GPU提供数据。(替代了iOS中的Core Graphics、Core Animation、Core Text)

    相关文章

      网友评论

          本文标题:Flutter 绘制原理浅析

          本文链接:https://www.haomeiwen.com/subject/gbopfdtx.html