Flutter训练营(四)-渲染机制

这是我参与更文挑战的第14天,活动详情查看: 更文挑战

一、前言

前面提过,目前大前端的趋势是移动端(阿里圆心),在移动互联网的浪潮下,我们开发的应用要想取胜,开发效率和使用体验可以说是同等重要。但是,使用原生的方式来开发 App,就要求我们必须针对 iOS 和 Android 这两个平台分别开发和适配,ios还好,但是华为、小米那么难以适配(难搞),这对于中小型团队来说就是隐患和额外的负担。
所以不仅需要在不同的项目间尝试用不同的语言去实现同样的功能,还要承担由此带来的维护任务。如果还要继续向其他平台(比如 Web、Mac 或 Windows)拓展的话,我们需要付出的成本和时间将成倍增长,开发一星期适配两个月,十分打击开发同学的积极性。跨平台开发是为了增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本。在提高业务专注度的同时,能够为用户提供一致的用户体验。用一个词来概括这些好处的话,就是“多快好省”。

二、跨端开发方案

这里指的就是跨 iOS 和 Android 开发。除了 React Native 和 Flutter 之外,这几年还出现过许多其他的解决方案,比如weex、cordova都是不温不火的状态。
针对跨端方案,根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。
Web 容器时代:基于 Web 相关技术通过浏览器组件来实现界面及功能,典型的框架包括Cordova(PhoneGap)、Ionic 和微信小程序。

泛 Web 容器时代:采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有React Native、Weex 和快应用,广义的还包括天猫的Virtual View 等。

自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter,是为数不多的代表。

2.1 Web容器时代

Web 时代的方案,主要采用的是原生应用内嵌浏览器控WebView(iOS 为 UIWebView 或 WKWebView,Android 为 WebView)的方式进行 HTML5 页面渲染,并定义 HTML5 与原生代码交互协议,将部分原生系统能力暴露给 HTML5,从而扩展 HTML5 的边界。这类交互协议,就是我们通常说的 JS Bridge(桥)。这种开发模式既有原生应用代码又有 Web 应用代码,因此又被称为 Hybrid 开发模式。由于 HTML5 代码只需要开发一次,就能同时在多个系统运行,因此大大降低了开发成本。

image.png

由于采用了 Web 开发技术,社区和资源非常丰富,开发效率也很高。但,一个完整 HTML5 页面的展示要经历浏览器控件的加载、解析和渲染三大过程,性能消耗要比原生开发增加 N 个数量级。
接下来,我以加载过程为例,和你说明这个过程的复杂性。
浏览器控件加载 HTML5 页面的 HTML 主文档;

加载过程中遇到外部 CSS 文件,浏览器另外发出一个请求,来获取 CSS 文件;

遇到图片资源,浏览器也会另外发出一个请求,来获取图片资源。这是异步请求,并不会影响 HTML 文档的加载。

加载过程中遇到 JavaScript 文件,由于 JavaScript 代码可能会修改 DOM 树,因此HTML 文档会挂起渲染(加载解析渲染同步)的线程,直到 JavaScript 文件加载解析并执行完毕,才可以恢复 HTML 文档的渲染线程。

JavaScript 代码中有用到 CSS 文件中的属性样式,于是阻塞,等待 CSS 加载完毕才能恢复执行。而这,只是完成 HTML5 页面渲染的最基础的加载过程。加载、解析和渲染这三个过程在实际运行时又不是完全独立的,还会有交叉。也就是说,会存在一边加载,一边解析,一边渲染的现象。这,就使得页面的展示并不像想象中那么容易。

通过上面的分析你可以看出,一个 HTML5 页面的展示是多么得复杂!这和原生开发通过简单直接的创建控件,设置属性后即可完成页面渲染有非常大的差异。Web 与原生在 UI渲染与系统功能调用上各司其职,因此这个时代的框架在Web 与原生系统间还有比较明显的、甚至肉眼可见的边界。

2.2 泛Web容器时代

虽然 Web 容器方案具有生态繁荣、开发体验友好、生产效率高、跨平台兼容性强等优势,但它最大的问题在于承载着大量 Web 标准的 Web 容器过于笨重,以至于性能和体验都达不到与原生同样的水准,在复杂交互和动画上较难实现出优良的用户体验。

image.png

而在实际的产品功能研发中,我们通常只会用到 Web 标准中很小的一部分。面对这样的现实,我们很快就想到:能否对笨重的Web 容器进行功能裁剪,在仅保留必要的 Web标准渲染能力的基上,使得友好的开发体验与稳定的渲染性能保持一个平衡?

当然是可以。

泛 Web 容器时代的解决方案优化了 Web 容器时代的加载、解析和渲染这三大过程,把影响它们独立运行的 Web标准进行了裁剪,以相对简单的方式支持了构建移动端页面必要的 Web 标准(如 Flexbox 等),也保证了便捷的前端开发体验;同时,这个时代的解决方案基本上完全放弃了浏览器控件渲染,而是采用原生自带的 UI 组件实现代替了核心的渲染引擎,仅保持必要的基本控件渲染能力,从而使得渲染过程更加简化,也保证了良好的渲染性能。
也就是说,在泛 Web 容器时代,我们仍然采用前端友好的JavaScript 进行开发,整体加载、渲染机制大大简化,并且由原生接管绘制,即将原生系统作为渲染的后端,为依托JavaScript 虚拟机的 JavaScript 代码提供所需要的 UI 控件的实体。这,也是现在绝大部分跨平台框架的思路,而React Native 和 Weex 就是其中的佼佼者。
为了追求性能体验的极致,并进一步维持方案的简单可扩展性,有些轻量级的跨平台方案甚至会完全抛弃 Web 标准、放弃 JavaScript 的动态执行能力而自创一套原生 DSL,如天猫的VirtualView框架。从广义上来说,这些方案也是泛Web 容器类方案。

2.3 自绘引擎时代

image.png

泛 Web 容器时代使用原生控件承载界面渲染,固然解决了不少性能问题,但同时也带来了新的问题。抛开框架本身需要处理大量平台相关的逻辑外,随着系统版本变化和 API 的变化,我们还需要处理不同平台的原生控件渲染能力差异,修复各类奇奇怪怪的 Bug。始终需要 Follow Native 的思维方式,就使得泛 Web 容器框架的跨平台特性被大打折扣。而这一时期的代表 Flutter 则开辟了一种全新的思路,即从头到尾重写一套跨平台的 UI 框架,包括渲染逻辑,甚至是开发语言。

渲染引擎依靠跨平台的 Skia 图形库来实现,Skia 引擎会将使用 Dart 构建的抽象的视图结构数据加工成 GPU 数据,交由 OpenGL 最终提供给 GPU 渲染,至此完成渲染闭环,因此可以在最大程度上保证一款应用在不同平台、不同设备上的体验一致性。

而开发语言选用的是同时支持 JIT(Just-in-Time,即时编译)和 AOT(Ahead-of-Time,预编译)的 Dart,不仅保证了开发效率,更提升了执行效率(比使用JavaScript 开发的泛 Web 容器方案要高得多)。通过这样的思路,Flutter 可以尽可能地减少不同平台之间的差异, 同时保持和原生开发一样的高性能。所以说,Flutter 成了三类跨平台移动开发方案中最灵活的那个,也成了目前最受业界关注的框架。

image.png

从上面这个表格分析对比,从各个维度综合考量,React Native 和 Flutter 无疑是最均衡的两种跨平台开发方案,而其他的方案或多或少都“偏科严重”。
React Native 依托于 Facebook,经过 4 年多的发展已经成长为跨平台开发方案的实际领导者,并拥有较为丰富的第三方库和开发社区;Flutter 以挑战者姿态出现在我们的面前,可以提供更彻底的跨平台技术解决方案。虽然 Flutter 推出时间不长,但也有了诸多商用案例,加上清晰的产品路线图和Google 的强大号召力,Flutter 未来的发展非常值得期待。

Flutter 作为后来者,也从 React Native 社区学习和借鉴了不少的优秀设计,很多概念两边都有对应,比如ReactNative 的 Component 和 Flutter 的 Widget、Flex 布局思想、状态管理和函数式编程等等,这类的知识都是两个框架通用的技术。前端就是这边变幻莫测,未来也许还会出现新的解决方案,老框架也会不断更新,只有掌握核心原理才能真正立于不败之地。

三、Flutter的天赋异禀

针对跨端开发,为不同的操作系统开发拥有相同功能的应用程序,开发人员只有两个选择:
使用原生开发语言(即 Java、kotlin 和 Objective-C、swift),针对不同平台分别进行开发。

使用跨平台解决方案(RN、Flutter、Weex),对不同平台进行统一开发。

针对RN的坑不再赘述,对于用户体验更接近于原生的 React Native,对业务的支持能力却还不到浏览器的 5%,仅适用于中低复杂度的低交互类页面。面对稍微复杂一点儿的交互和动画需求,开发者都需要 case by case 地去 review,甚至还可能要通过原生代码去扩展才能实现。

React Native 之类的框架,只是通过 JavaScript 虚拟机扩展调用系统组件,由 Android 和 iOS 系统进行组件的渲染;Flutter 则是自己完成了组件渲染的闭环。

Flutter 是构建 Google 物联网操作系统 Fuchsia 的SDK,主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App,一套代码可以同时运行在 iOS 和 Android 平台。Flutter 使用 Native 引擎渲染视图,并提供了丰富的组件和接口,这无疑为开发者和用户都提供了良好的体验。

与用于构建移动应用程序的其他大多数框架不同,Flutter是重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案。这样不仅可以保证视图渲染在 Android 和 iOS 上的高度一致性(即高保真),在代码执行效率和渲染性能上也可以媲美原生 App 的体验(即高性能)。

四、Flutter渲染机制

image.png

在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。

CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。

操作系统在呈现图像时遵循了这种机制,而 Flutter 作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释 Flutter 的绘制原理。

可以看到,Flutter 关注如何尽可能快地在两个硬件时钟的VSync 信号之间计算并合成视图数据,然后通过 Skia 交给GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给GPU 渲染。

4.1 Skia的作用

要想了解 Flutter,你必须先了解它的底层图像渲染引擎Skia。因为,Flutter 只关心如何向 GPU 提供视图数据,而Skia 就是它向 GPU 提供视图数据的好帮手。Skia 是一款用 C++ 开发的、性能彪悍的 2D 图像绘制引擎,其前身是一个向量绘图软件,OpenGL是Skia的绘图引擎。2005 年被 Google 公司收购后,因为其出色的绘制表现被广泛应用在 Chrome 和Android 等核心产品上。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。目前,Skia 已然是 Android 官方的图像渲染引擎了,因此Flutter Android SDK 无需内嵌 Skia 引擎就可以获得天然的 Skia 支持;而对于 iOS 平台来说,由于 Skia 是跨平台的,因此它作为 Flutter iOS 渲染引擎被嵌入到 Flutter 的iOS SDK 中,替代了 iOS 闭源的 Core Graphics/CoreAnimation/Core Text,这也正是 Flutter iOS SDK 打包的App 包体积比 Android 要大一些的原因。底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。

4.2 Dart的优势

Dart 同时支持即时编译 JIT 和事前编译 AOT。在开发期使用 JIT,开发周期异常短,调试方式颠覆常规(支持有状态的热重载);而发布期使用 AOT,本地代码的执行更高效,代码性能和用户体验也更卓越。比如今年 2019 年 5 月发布的 Dart2.3,每次发布都包含了为 Flutter 量身定制的诸多改造包括改进的 AOT 性能、更智能的类型隐式转换等。

image.png

备注:Dart有它自身的优点,Flutter的好并不意味着JS的失败,Dart诞生也不迟,一直不火的原因也是人尽皆知的(早期诞生的目的是干掉JS,结果~),不过随着Flutter的推进,Dart在今后的更新中会更加优化吧。

五、Flutter运行原理

Flutter 架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework。
Embedder 是操作系统适配层,实现了渲染 Surface 设置,线程设置,以及平台插件等平台相关特性的适配。从这里我们可以看到,Flutter 平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对较低。
Engine 层主要包含 Skia、Dart 和 Text,实现了 Flutter的渲染引擎、文字排版、事件处理和 Dart 运行时等功能。Skia 和 Text 为上层接口提供了调用底层渲染和排版的能力,Dart 则为 Flutter 提供了运行时调用 Dart 和渲染引擎的能力。而 Engine 层的作用,则是将它们组合起来,从它们生成的数据中实现视图渲染。

Framework 层则是一个用 Dart 实现的 UI SDK,包含了动画、图形绘制和手势识别等功能。为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,Flutter 还基于这些基础能力,根据 Material 和 Cupertino 两种视觉设计风格封装了一套 UI 组件库。我们在开发 Flutter的时候,可以直接使用这些组件库。

如上篇文章所介绍,Flutter 的核心设计思想便是一切皆 Widget,页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter 通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。
而渲染对象树在 Flutter 的展示过程分为四个阶段:布局、绘制、合成、渲染。

5.1 布局

Flutter 采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。

image.png

为了防止因子节点发生变化而导致整个控件树重新布局,Flutter 加入了一个机制——布局边界(RelayoutBoundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。

image.png

5.2 绘制

布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter 会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。以下图为例:节点 1 在绘制完自身后,会再绘制节点 2,然后绘制它的子节点 3、4 和 5,最后绘制节点 6。可以看到,由于一些其他原因(比如,视图手动合并)导致2 的子节点 5 与它的兄弟节点 6 处于了同一层,这样会导致当节点 2 需要重绘的时候,与其无关的节点 6 也会被重绘,带来性能损耗。为了解决这一问题,Flutter 提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,

image.png

Flutter 会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。

image.png

重绘边界的一个典型场景是 Scrollview。ScrollView 滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。

5.3 合成和渲染

终端设备的页面越来越复杂,因此 Flutter 的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
需要注意的是,无论布局还是绘制,都是父子间的遍历关系:父Widget的布局需要依赖子Widget的布局结果;而绘制则反过来(子Widget需要盖在父Widget上)布局是后续遍历,绘制是前序遍历,他们都是深度优先遍历。

总结

Web最大的优势是标准化和生态,判断一个技术是否能成为未来大前端主流技术发展的趋势,主要还是看这个技术是否能减少对底层环境的依赖,是否能用尽量完整的技术栈和工具链去隔离各终端系统差异,向开发者提供统一而标准化的能力;以及在大前端永恒的三大主题,效率、质量和性能维度,能否从原理和运行机制层面有领先同类产品的表现。按这个标准,目前Flutter和Web都站在了大趋势上。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享