Flutter UI 渲染浅析(一)总览
Flutter UI 渲染系列文章,基于 Flutter v1.20.4
我们知道屏幕显示的基本单位是像素,每个像素点显示不同的颜色值,按一定规则排列就形成了图像。
显示器按一定频率从 GPU 获取数据,就可以完成图像的更新。现在手机屏幕一般有60HZ、90HZ、120HZ,以60HZ为例,屏幕每秒会发出 60 个 VSync 垂直同步信号。
VSync 信号用于协调显示器、CPU 和 GPU 的工作。
GPU 每秒可以绘制的帧数叫做帧速率,如果帧速率大于屏幕刷新率,屏幕上显示的内容就有可能是两个图像的不完全内容,造成图像撕裂。
VSync 垂直信号可以保证刷新频率的统一。
在 VSync 信号的调度下,UI 系统把逻辑代码变成图像显示到屏幕上,一般需要经过以下几个阶段:
- View,构建 View/DOM 节点
- Layout,计算样式(Style)、布局(Layout)
- ViewTree,将 View 节点按一定规则归属到不同的图层,构建或更新 ViewTree
- Paint,绘制,输出绘制指令到 DisplayList
- Rasterization,光栅化,执行 DisplayList 中的绘图指令,生成图层区域的像素数据
- Composite,合成,把各图层光栅化后的数据进行叠加和特性处理,输出到屏幕上
简单来说就是做了两件事情:
- 构建 UI 描述规则,用于构建或刷新图像,并把绘制指令保存起来用于绘制,即 1-4
- 光栅化和合成,对硬件绘制 API 做了统一封装,屏蔽底层绘制细节,即 5-6
Android 和 iOS 基本都是按照这个流程来构建UI的。
以 Android 为例,Android 通过 XML 来描述 UI 结构,用 DisplayList 保存绘制指令信息,通过 Skia 封装底层细节实现光栅化合成。
Flutter 作为一款跨端 UI 开发框架,它的渲染流程也是类似的。
这个系列的文章主要分析下 Flutter UI 渲染流程,涉及到的中间过程会分部解析,基于 Flutter v1.20.4 版本源码,主要分为以下几个部分:
- 总览
- VSync 注册
- Animate 及动画实现原理
- Build
- Layout
- Paint & RepaintBoundary
- CompositeFrame
- RasterThread 光栅化及合成
1、Flutter 架构
Flutter 架构分为三层:Dart Framework、C++ Engine、 Platform Embedder。
Dart Framework 提供了响应式的开发框架,使用 Dart 开发,它对渲染逻辑做了统一封装,屏蔽了底层实现,对底层 C++ Engine 提供双向通信能力,开发者只需要组合 Widgets 用于构建 App 视图即可。
- 最底层的 Foundation 层提供一些最基础的抽象类或定义,基于此, Animation 动画、Painting 绘制、Gestures 手势等构建出通用抽象能力
- Rendering 层,构建出渲染树 Render Tree,也即 RenderObject Tree,用于具体绘制,RenderObject 会自动随着数据改变而动态改变
- Widgets 层,提供了一套非常丰富的 Widget 组件库,用于构建 Widgets Tree 和 Element Tree,这是响应式编程的基础实现,每一个 RenderObject 都有一个对应的 Widget 及 Element
- Materail 层和 Cupertino 层使用 Widgets 组件库,构建 Android Materail 或者 iOS Cupertino 风格的应用视图,开发者基于这些 Widgets 即可构建出效果一致的跨端应用
C++ Engine 是 Flutter 的核心部分,大部分使用 C++ 开发,它的主要职责是光栅化合成上屏用于显示绘制内容,同时它也提供低层次的核心能力,比如Skia图形化绘制(graphics)、TextLayout、文件系统、网络 I/O、无障碍支持、插件体系、Dart运行时(DartVM)和 GC、编译链。
Engine 层对 App 层暴露 dart:ui
包,dart:ui
包是 Flutter App 的构建基础,其中的 dart 类对 C++ Engine 层中的实现类做了包装,它提供了基础能力,诸如交互系统、图形图像处理、渲染子系统等。
其中最重要的一个类是包下的 Window
,它向上提供了最核心的一些服务,比如任务Scheduler API、绘制 API、输入事件响应等等。
Platform Embedder 是平台嵌入层,把 Flutte 代码打包嵌入到具体的实现平台,提供运行入口,并对上层提供最基础的能力,比如提供渲染画布、插件系统、无障碍、交互管理、消息循环管理等。
Flutter 分层架构使得平台相关性大大降低。
Dart Framework 对上提供统一的基于 dart 的响应式 UI 描述框架。
C++ Engine 向下对 Skia 绘制引擎对下统一封装,屏蔽了平台实现。
ReactNative / Weex 作为跨端框架 ,虽然走的也是是原生渲染,但是需要通过 js 来组织和描述 UI,中间需要做一层转换,才可以变成原生的 UI 描述结构,进而原生渲染。
中间多了一层转换过程。
而 Flutter 自建渲染引擎,不依赖平台实现,并且 Dart 可以直接被编译成机器码,从架构上来说,性能相比 ReactNative / Weex 会更好。
2、Flutter 渲染管线
Flutter 渲染管线的设计是类似的,流程参考下图
首先看下用到的线程:
UIThread 是 Platform 创建的子线程,DartVM Root Isolate 所有的 dart 代码都运行在该线程。
阻塞UIThread 会直接导致 Flutter 应用卡顿掉帧、
RasterThread 原本叫做 GPUThread,也是 Platform 创建的子线程,由于很多人误认为运行在 GPU 上,但其实它是运行在 CPU 用于处理数据提交给 GPU,所以 Flutter 团队将其名字改为 Raster,表明它的作用是光栅化。
C++ Engine 中的光栅化和合成过程运行在该线程。
整个流程会经过以下几个过程:
- C++ Engine 触发 Platform 注册 VSync 垂直信号回调,通过 Platform -> C++ Engine -> Dart Framework 触发整个绘制流程
- Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记录绘制区域及绘制指令信息生成
flutter::LayerTree
,并保存在Scene
对象用以光栅化,这个过程运行在 UIThread - 通过 Flutter 自建引擎 Skia 进行光栅化和合成操作, 将
flutter::LayerTree
转换为 GPU 指令,并发送给 GPU 完成光栅化合成上屏显示操作,这个过程执行在 RasterThread
整个调度过程是生产者消费者模型,它的实现在 Engine 的 LayerTreePipeline
。
UIThread 负责生产 flutter::Layer Tree
,RasterThread 负责消费 flutter::Layer Tree
。
这种调度机制可以确保 RasterThread 不至于过载(2个任务),同时也可以避免 UIThread 不必要的资源消耗。
所以不论在 UIThread 还是在 RasterThread 耗时太久,都可能会导致 Flutter 应用卡顿,因为会导致延迟接受 VSync 信号,导致掉帧。
这个调度过程会在下一篇文章中详细分析。
3、Flutter UI 绘制管线
其中在 UIThread 生成 Layer Tree 的过程,我们将其称为 Rendering Pipeline
绘制管线。
主要过程为:
- Animate,触发动画更新下一帧的值
- Build,触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree
- Layout,触发布局操作,确定布局大小和位置信息
- CompositeBits,更新需要合成的 Layer 层标记
- Paint,触发 RenderObject Tree 的绘制操作,构建 Layer Tree
- Composite,触发 Layer Tree 发送到 Engine,生成 Engine LayerTree
在 UIThread 构建出四棵树,并在 Engine 生成 Scene,最后提交给 RasterThread,对 LayerTree 做光栅化合成上屏。
下一篇文章分析下 VSync 注册。
参考
本文链接: http://w4lle.com/2020/11/09/flutter-ui-overview/
版权声明:本文为 w4lle 原创文章,可以随意转载,但必须在明确位置注明出处!
本文链接: http://w4lle.com/2020/11/09/flutter-ui-overview/