Flutter 原理和绘图

1架构理解

谈到跨平台一般绕不过webhybirdreact native等框架,其中RN是目前比较主流的跨平台解决方案。我们拿RNflutter做一下对比。

1.1跨平台架构简介

1.1.1 React Native:
Rf85f8ebd19aba1d9607033b500ef7f9f.jpeg

RN可以让JS开发者使用JS代码就可以写出一个跨平台的App,同时RN需要一个JS运行环境,在iOS中使用的是内置的javascriptcore,在android使用的是webkit.org官方开源的jsc.so,搭建UI只需要在虚拟DOM中,之后通过框架转换对应到native的视图上,需要一个和native通信的则使用Bridge进行异步通信。

1.1.2 flutter :
v2-6733941a13ba1b2cfe65a9c57fa112fb_b.png

flutter的架构由两部分组成 分别是frameworkEngine:

1.1.2.1framework

framework是一个Dart库 包括组件、手势、动画、有MaterialCupertino风格,是我们最常打交道的库。开发中的接大部分需求framework给我们提供的库都可以满足。

1.1.2.2Engine

EngineC/C++编写的库,是flutter的核心库,包含了Dart虚拟机、动画和图形、文字渲染、通信通道等,其中渲染引擎采用的是2D的图形渲染库Skia,虚拟机是Dart VM,另外还包含了Embedder中间层代码。

1.2RN和flutter对比

简单了解了RNFlutter的架构,我们就能很直观的看到两者之间差异,至于为什么同是跨平台语言,Flutter性能优于RN、近似Native,我们看一下下面这张图:

WX20210315-173940.png
1.2.1flutter

构建一个flutter项目,我们通过dart语言开发,使用flutter framework提供的库便可以,之后交于Engine中的Skia引擎做图像的绘制,最后生成的CPU或者GPU的指令,在设备上完成绘图。整个过程和Native的流程一模一样。

1.2.2 Android

从上图可以看出flutter框架代码完全取代了AndroidJava代码,所以只要flutter框架dart代码的效率可以媲美原生框架的Java代码的时候,那么flutter的性能就可以媲美原生app。

1.2.3 iOS

iOS中和Skia起到相似作用的是Core Graphic / Core Animation,因为Skia是一个google开源的2D图像库,所以直接用Skia来代替Core Graphic / Core Animation 来做图形的绘制即可,因为要把Skia集成到项目中去,所以同一个项目iOS打出来的包是要比Android要大一些。

1.2.4 RN

它首先要调用框架本身的JavaScript代码,然后再调用Android(iOS)框架的Java(OC)代码,然后调用skia(Core Graphic / Core Animation),这比原生的APP调用方式多出来一步,必然会产型性能上的损耗。

1.2.5 android 低端机

flutter开发的app在一些低端机型上的性能优于android原生app。
因为在原生app中,skia图形引擎是作为android操作系统的一部分,只要等操作系统升级,skia才会升级得到最新的优化。
flutterskia是包含在flutter SDK中的,只要flutter SDK升级,skia也会随着升级,目前flutter处于快速迭代期,skia上最新的优化和改进会很快的应用到flutter项目中。android系统的升级相对慢,且对低端机的升级也不是太友好,所以skia的优化在原生app上是不得到及时的体现。

1.3flutter和native

经过上面的分析,我们再看下面这幅图基本上就可以清晰的看出flutter和native的对比和优势:

1408135781-5ff422679b370.png

2、图像绘制

2.1基本概念

帧率:
GPU一秒内绘制图像的次数(skia可以达到120帧)
刷新率:
屏幕显示图像的数量60帧/秒 (现在机型能达到90帧或者120帧),人眼可接受的最低是12帧。
两者关系:
GPU提供数据 存放在Buffer中,显示器从Buffer中获取图像显示在屏幕,理想情况下,帧率和刷新率一直,实现画面流畅显示。
tearing:
通常情况下帧率是大于刷新率的,在这种情况下,第n张图片在屏幕上加载完成,n+1就已经出来了,会出现撕裂tearing

0ef9bf5a-b974-4ce3-8e0b-827abd9d99d2.png

Vsync:
为了解决tearing的情况,就需要引入Vsync垂直同步信号,在屏幕刷新两帧之间会有一个VBlank,这个间隔就会产生一个Vsync信号,GPU/CPU在收到信号之后再去计算下一帧画面,这样就能够避免tearing,就算没有产生tearing也建议使用Vsync,普遍情况下帧率是要高于刷新率的,为了避免GPU/CPU过快的渲染结果,产生冗余的计算。

Jank:
tearing相对应的就是jank,这是由于在CPU/GPU在收到Vsync之后没有在16.6ms(以60帧/秒为例)提供渲染结果供屏幕显示,就会出现jank卡顿,这种情况下,就需要优化代码(isolate),或者做三级缓存(cache Buffer)在这里就不详细介绍。

WX20210315-185343.png
WX20210315-185322.png

2.2flutter渲染

f7246b600c33874440c97d574abec4fed62aa0c1.jpeg
2.2.1渲染过程
  1. framework 注册监听enginevsync信号
    2.engine注册监听GPU的vsync信号
  2. GPU收到屏幕的vsync信号通知engine
    4.engine通知framework
  3. 通过framework完成页面的重建、布局、绘制、合成,最终交由engine
  4. GPU存放到buffer中,等待屏幕来获取,获取之后,屏幕发出vsync信号,重复3-6步骤。
2.2.2 framework中的流程

framew中的操作是开发具体操作的流程,大致步骤如下图:

WX20210318-110911.png
Layout

确定每个widget的宽高和在屏幕中的位置

Constraints go down. Sizes go up. Parent sets position.
约束条件向下传,大小信息向上传,父视图决定位置

WX20210318-110923.png

节点在组建视图树的时候,每个节点的Constraint是自上而下,大小位置是从内到外的,应为节点自己的大小是由子节点size决定的,所以获取size必须从下到上,从里到外。

通过下面的例子就很好理解了(来源flutter官网):

# 整个屏幕作为 Container 的父级,并且强制 Container 变成和屏幕一样的大小。
# 所以这个 Container 充满了整个屏幕,并绘制成红色。
Container(color: Colors.red)


#红色的 Container 想要变成 100 x 100 的大小,但是它无法变成,因为屏幕强制它变成和屏幕一样的大小。
#所以 Container 充满了整个屏幕。
Container(width: 100, height: 100, color: Colors.red)


#屏幕强制 Center 变得和屏幕一样大,所以 Center 充满了屏幕。
#然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 可以真正变成 100 × 100 大小了。
Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)

Relayout boundary
通过上面的layout结构来看,一个节点的size变化了,整个树结构都需要从新计算,为了避免这个问题使用Relayout boundary来解决这个问题。

WX20210318-144239.png

Relayout boundary的作用是设置测量边界,边界内的Widget做任何改变都不会导致边界外重新计算并绘制。这个机制是在一些特定的情况下系统帮你设置的,一般情况下不需要开发手动来设置。

  • widget设置了固定的size(constraints.isTight (min = max))
  • widget是不需要依赖子widgetsize(parentUsesSize == false),这种情况下子widget重新布局的时候就不会通知父widget,布局的边界就是自身。
  • widget自动占满父widget所提供的空间大小(sizedByParent == true),比如(Expanded/Center)。
    以上三种情况,就会自动触发了Relayout boundary机制,自动隔绝了父子组件的联动刷新。

默认情况下,开发不要直接去设置Relayout boundary,只需要触发上诉三个条件之一即可。

Paint

为widget提供画布,让widget进行绘制

在iOS中每一个UIView都有一个layerUIViewUIView之间是相互隔离的。但是flutter中的renderObject不一定有layer,通常情况下,一个renderObject的子节点都是渲染在同一个layer上,这就会出现以下一种情况:

23b34bb3984f1c8e0ac5cade96112d92cab.jpeg
  • 节点2、3、4、5、6 是节点1的子节点,应该绘制在节点1所在的绿色layer上。
  • 如果节点4(video)需要单独占据一个layer,这个时候就4就会单独占据一个黄色layer。
  • 节点2上面还有部分内容需要绘制,顶部黄色layer是4单独占据的,需要新的红色layer来绘制节点2上面剩余的部分内容5。
  • 6也会绘制在顶层红色layer上,这样红色layer上就会存在5和6的共同数据。
  • 如果节点2发生改变红色layer会重绘,这样会影响毫不相干的节点6

Repaint Boundary
为了应对上诉情况,flutter设计了和Relayout Boundary相同思想的Repaint Boundary,在遇上上诉问题的时候 就强制添加新的layer
给节点6添加父节点RepaintBoundary widget,在创建的时候遇到RepaintBoundary widget就会添加新的layer,实现节点layer的隔离。即使发生重绘也不会其他子节点产生影响。

Composite

屏幕上看到的界面一般都是有很多的layer一层一层叠加在一起的,在这一步
把绘制好的多个layer合成为一个layer进行光栅化处理,然后交付给GPU处理,提高效率。

总结

flutter是一款非常优秀的框架,在跨平台上直截了当,和Native的切割也十分干净,flutter已经发布了2.0版本,支持空安全优化了web端的支持,期待flutter发展的越来越好。

你可能感兴趣的:(Flutter 原理和绘图)