原文转自:腾讯技术工程
当 Flutter 遇见 Web,会有怎样的秘密?
在线教育团队(简称:OED)已经将 Flutter 这样技术在业务中落地了,做为 IMWeb 前端团队的我们也要进行一些尝试。本文从前端角度进行 Flutter 开发的概况描述。主要是为了让您了解和感受一下:Flutter to Web 的实例、Flutter 为什么会出现、Flutter 设计实现原理、Flutter 技术特点和优势。
OED的客户端团队在 2019 年上半年 ,就已经把 Flutter 落地到企鹅辅导的业务中了。今年我们又一起去上海参加了 2019 年谷歌开发者大会,遇见了更多的 Flutter 开发者,这次体验比第一次去的时候感觉熟悉了很多。希望未来有机会把他们邀请来深圳,进行一些 Flutter 的技术分享。此次开发者大会又恰逢 Flutter to Web 也已经正式合入 Master,那么,前端同学是否可以趁着这股东风一起参与到 Flutter 的协同开发中呢,我想这问题会困扰着很多人?如果您有好的想法,可以在留言区参与评论。
本文不是一篇 Flutter 详细的学习教程,更像是一个概览,用尽可能平实的语言和对比的思路去描述它。本着依旧从前端同学的角度出发,去理解一项新的技术,但又不限于前端技术本身。希望您能通过这篇文章相对全面的理解 Flutter 这项技术本身。特别感谢领导的鼓励和支持,让我有机会去学习和理解 Flutter 框架,因为相对我而言,OED 的客户端团队的同学经验会远超于我,他们已经完整经历了业务从 0 到 1 的过程,这是一种非常有意思的体验。
下面转换的工程案例 是我们的 企鹅辅导APP 里面的业务代码(详细的操作流程)。Flutter 官方文档具有很好的说明,如果您单纯转一个完全不依赖 APP 的项目,您安装完环境并且切换到 Master 版本就可以直接进行了,甚至不需要任何代码修改。转换业务产品中的代码,还需要处理一些奇奇怪怪的问题,相信这对您来说,应该都不是问题。可以在这里安装和环境配置 进行环境安装。Flutter 官网提供了一个 案例可以尝试一下。官方给了一个开箱即用的 开发文档 。
下面是一个处理无限列表的场景,无论是在 Mac,还是在移动端时,依旧会有卡顿的现象,FPS 表现并不理想。
首先官方目前还不建议,在产品化中使用 。但既然已经合入 Master,相信这一天也不会远了。
转换这里需要解决一些问题,整理了一下官方建议和实践的体验:
业务转换使用的时候,需要把系统依赖解决掉,部分样式问题跟 Flutter 排版组件有关,而系统相关的如本地存储、网络请求需要我们自定义转化方案。例如:客户端使用的是 WNS 协议,而前端需要使用的是 HTTPS。目前看 Flutter to Web 作为业务容灾的策略还是可以的,总是优于 APP Crash 或者 影响范围非常大的 BUG 导致用户无法使用的情况出现。这里 Skia 有个明显的短板,就是 3D 动画,如果您业务对 3D 动画依赖比较强,一段时间内,就不要选择 Flutter 作为业务的技术选型了。
上面简单的罗列了一些前端在 Flutter 的工作范围,前端定位更多是打辅助的!当然如果您是全能型开发,也可以全部都做。技术本质上没有边界,局限的只有自己。从前端角度看 Flutter 的开发成本相比于 H5 确实还是多了一些成本,但学起来也不会太难,只是时间的问题。对于团队有人力,并且希望尝试新技术的,完全放在业务中尝试使用。
至于未来是否能确定是 Flutter 这个框架成为行业标杆,还不得而知。但如果想彻底统一全端技术栈,Flutter 今天的设计思路是一个非常有突破性的存在!应该说,类 Flutter 的自绘引擎方案在未来会有机会大放异彩。
站在前端的角度上,我们尝试着在组件化和工程化的方向找到自己在 Flutter 生态中的定位。并且相比于 IOS 和 Android,Flutter 的 Dart 代码是完全开源的,参与感是会让开发者提升不少的,就类似 linux 和 windows 的感觉。
至于团队是否要参与进去,很多时候是看综合的成本和收益,做与不做,做到什么程度,适合什么时候进行业务跟进,其实,都是要以团队的价值最大化为目标,没有绝对的对与错,结合团队的实际情况量身定制就好。错误的时机进入,也会付出不小的成本,您自己考量。
如下图,横向对比行业开源方案:
简单对比来看,结合团队的技术实践和能力。站在第三方的角度上。RN 和 Flutter 相对是 2 个比较好的跨平台方案。而且其它方案或多或少都有一些局限。
那真的放到业务上,又当怎么选择呢?
回答这个问题确实有些艰难,仁者见仁智者见智。无论是相对成熟的 React Native,还是新贵 Flutter,放眼到整个大前端技术史里面看,都不如 Hybird 影响的深远。但如今它们也都达到了商用的标准。我同时使用过 Hybird 和 RN 作为业务接入方,算是会有一点点经验之谈。
从目前行业的产品,以及社区生态来说,React Native 整体还是胜出 Flutter 一筹。毕竟早出来几年,市场占有率和行业积累还是在的。但是长远来看,技术发展也有它的必然规律,Flutter 的技术理念已经领先了 React Native,作为大公司、或者大前端团队的技术储备和技术选型,科技公司要想在未来在行业有一席之地,使用 Flutter 这样的技术,必然也会是一个趋势。至于开发者对于技术本身,应该也会对 Flutter 保有浓厚的兴趣吧,毕竟技术永远都会向前发展,无论是谁,不进步一样会被淘汰。因此,作为技术开发人员首先保持的就是一颗好奇的心,进而持续成长。
当然,未来可能成功的框架不一定就是 Flutter,但是它设计理念和设计思路是一脉相承的,类 Flutter 框架一样也会出现。 就像 React 出来了,Vue 也会跟着出来了。有的团队使用 Vue,有的团队使用 React。但是,您会发现它们越来越像了。使用 Vue 的也没必要说使用 React 的同学水平不好,不存在的!理念很重要,有一句话说的很有意思,叫 ~ 思路决定出路!
在谈及 Flutter 之前,我们还是要先简单回顾一下,客户端的上一次技术革新 —— ReactNative(此后简称 RN)。相信非常多的团队都有去落地实践 RN 的机会,很多 APP 的首屏渲染方案都是用 RN 技术栈进行的。我们自己的产品 企鹅辅导 和 腾讯课堂 内的应用也是一样。
这里简单回顾一下,在有客户端开发的场景下,为什么又出现了 RN ?
RN 的价值简单来讲就是—— 可接受的页面性能 + 高效开发 + 热更新。
更新:传统的 APP 上架之后,出现了业务 BUG,用户只能去更新 APP,进行 BUG 修复。客户端实现热更新修复 BUG,有多难,可以问问 IOS 的开发同学。大概率猜测,手 Q 和微信,应该还是有方案可以热更新的。但是对很多小厂商这确实是非常艰难的事情。因此,得益于强大的动态化能力 RN 的价值也就完美的体现出来了。
高效:一个 APP 发布上线,Android 和 IOS 同时需要开发两个应用,而 RN 只需要一套代码,就可以运行在双平台上,节省很大的人力成本。并且很多业务线有很强的业务运营诉求,可能会存在很短时间内的多次改版和发布的情况出现,客户端开发的人力瓶颈和发布周期的限制,已经很难满足这样的业务场景了。尤其在一些有损发布的情况下,赶着时间点,带着 BUG 上线的场景,在后续进行增量的修复,再这样的情况下,传统客户端的表现,简直就是灾难性的。
性能:RN 具有优于 H5 的性能体验。毕竟是通过客户端进行的页面渲染,速度上比 WebView 渲染还是要快不少的。这个在 Weex、Hippy、Plato 上都有所体现,虽然低于 Native 的性能,但是在可接受范围。PS:这里的表达,不是描述客户端开发不好。只是单纯从业务角度上看待问题,而把合适的技术放在合适的位置是非常重要的,这也是架构师核心价值之一
回顾了以上三点,我们发现 RN 的出现,有它的必然性。那么回到主题,RN 已经这么优秀了,为什么还要有 Flutter 的存在,有一次向 Ab 哥请教技术成长的时候,Ab 哥提到了很有意思的一个观点,就是您对一项技术了解的深入程度,取决于是否能认清这项技术的局限。 就像人一样,他(她)有多少优点,就会存在多少缺点。没发现,不等于不存在,因为一定存在。因此,顺着这个思路,我们简单的看一下 RN 的问题。
首先,看维护成本,虽然 RN 是一套代码多端运行。但还是需要 IOS 和 Android 开发帮助我们去一个一个的绘制组件,尤其遇到特殊诉求的时候,还要 case by case 的处理,并且随着 IOS 和 Android 系统本身的迭代和升级,以及框架自身发展的历史包袱,我们可能还需要处理很多与原生系统之间的平台差异,修复各种奇奇怪怪的 BUG,这对业务来说是很大的负担。
其次,对性能诉求,无论是产品还是开发同学,对于用户体验的追求,永远都不会停止。RN 存在诸多性能的短板,因此,才会有 Weex 这样的产品出现,去定制化的解决业务场景下的问题。JS 和 Native 的通信,页面的事件监听,复杂动画的渲染和交换成本,都是很大的性能挑战。
最后,在存在更强的业务诉求的时候,人们就不得不去寻找更好的方式去实现。非常存感激的看待谷歌这家公司,都是定位于商业公司,但实际上对世界的影响力上面,公司与公司之间差距还是非常大的。这个课题范围太大,以后有机会可以深度讨论一下。
您看到了上面的描述,为了解决上面这些问题 —— 自绘引擎时代出现了,以 Flutter 为代表的技术方案会应运而生,相信一定不会只有 Flutter 一项跨平台技术出现的,历史总是惊人的相似。其实想到自绘引擎,我最先想到的是那些游戏引擎。那现在又为什么给出 自绘引擎 这样的一个概念呢?H5 是依赖于浏览器渲染,RN 依赖于客户端渲染,而 Flutter 基于 Skia 自己绘制的图形界面。因此,Flutter 才能真正实现跨端!相信在不久的未来,在传统客户端上也能看到 Flutter 的身影,这样才能真正达到多端统一。
最后,我们再简单总结一下有哪些问题:
1、Web 性能差,跟原生 App 存在肉眼可见的差距;
2、React Native 跟 Web 相比,支持的能力非常有限,特定长场景问题,需要三端团队一个一个处理;
3、Web 浏览器的安卓碎片化严重(感谢 X5,腾讯的同学过得相对轻松一些)。
为了解决上面的问题,Flutter 出现了:
一套代码可以运行在两端;
自绘 UI,脱离平台,也可以简单的把它理解为一个浏览器的子集。
铺垫了这么多,就是为了帮助您回忆起技术发展的脉络和技术趋势,可以更好的理解下面即将要表达的文稿,下面我们正式开始介绍 Flutter。
能介绍的技术点其实非常多,这里找了一些具有代表性的技术项,结合自己的理解跟大家分享一下。包括设计思路、渲染方式、UI 的生命周期。因为这几个点,跟 React 技术栈风格非常相似,以这种思考结构去对比介绍,可以帮助大家更好的理解这项技术本身。
Flutter 整体架构设计
Google 了一下关键词,搜素得到了这张图片,从下向上进行一些描述:
Embedder:是操作系统适配层,实现了渲染 Surface 设置,线程设置,以及平台插件等平台相关特性的适配。从这里我们可以看到,Flutter 平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对较低。
Flutter Engine:这是一个纯 C++实现的 SDK,其中囊括了 Skia 引擎、Dart 运行时、文字排版引擎等。不过说白了,它就是 Dart 的一个运行时,它可以以 JIT、JITSnapshot 或者 AOT 的模式运行 Dart 代码。在代码调用 dart:ui 库时,提供 dart:ui 库中 Native Binding 实现。 不过别忘了,这个运行时还控制着 VSync 信号的传递、GPU 数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。具体的绘制方式,我们放在后面描述。
Flutter Framework:这是一个纯 Dart 实现的 SDK,类似于 React 在 JavaScript 中的作用。它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI 组件库,然后根据 Material 和 Cupertino 两种视觉风格区分开来。这个纯 Dart 实现的 SDK 被封装为了一个叫作 dart:ui 的 Dart 库。我们在使用 Flutter 写 App 的时候,直接导入这个库即可使用组件等功能。
PS:虽然很早知道 Flutter,但实际写 Flutter 时间也比较短暂。引擎源码层面,目前也没有深入的涉猎。了解的方式可以通过自己阅读源码,或者找谷歌、阿里、美团、以及我司的开发者帮忙。从技术角度来了解这些,在需要的阶段,不会成为大家的瓶颈。毕竟商业世界充满了壁垒,而应用层面的技术本身是开放的。
简单描述一下 JIT 与 AOT:
Dart 是什么
它的目标在于成为下一代结构化 Web 开发语言。Dart 发布于 2011 年 10 月 Google 的"GOTO 国际软件开发大会"。是一种基于类编程语言(class-based programminglanguage),在所有浏览器都能够有高性能的运行效率。Chrome 浏览器内置了 DartVM,可以直接高效的运行 dart 代码(2015 年被移出)。支持 Dart 代码转成 Javascript,直接在 Javascript引擎上运行。dart2js
Dart 的特点
备注:
1)TS 可以一定程度上帮助 JS 添加一些静态检测,但本质上依旧是无法达成这样的效果;
Flutter 选择 Dart 的原因
看到了上面的介绍,这里总结一下 Flutter 的实现思路。它开辟了新的设计理念,实现了真正的跨平台的方案,自研 UI 框架,它的渲染引擎是 Skia 图形库来实现的,而开发语言选择了同时支持 JIT 和 AOT 的 Dart。不仅保证了开发效率,同时也提升了执行效率。由于 Flutter 自绘 UI 的实现方式,因此也尽可能的减少了不同平台之间的差异。也保持和原生应用一样的高性能。因此,Flutter 也是跨平台开发方案中最灵活和彻底的那个,它重写了底层渲染逻辑和上层开发语言的一整套完整解决方案。
彻底跨端:
Flutter 构建了一整套包括底层渲染、顶层设计的全套开发套件。
这样不仅可以保证视图渲染在 Android 和 IOS 上面的高度一致,也可以保证渲染和交互性能(媲美原生应用)。
与现有方案核心区别:
类 RN 方案,JS 开发,Native 渲染。数据通信 bridge;
Hybird 浏览器渲染 + 原生组件绘制;
Flutter 设计自闭环,完成渲染和数据通信。
渲染方案是 Flutter 目前独特的设计形态,就是由于渲染自闭环,才能真正跨平台。谈到 UI 渲染方案,作为前端开发,我们是绕不过现在如火如荼的三大框架的。为什么要谈类 React 方案呢?因为 Flutter 的设计方案,与 React 设计具有一样的思路。在渲染这里我们会谈及控件、渲染原理、以及生命周期。
Flutter 是如何进行页面渲染的呢?传统 Web 是通过浏览器,而 Flutter 是自绘。所谓自绘就是用户界面上 Flutter 自己绘制到界面,无需依赖 IOS 和 Android 原生能力,是通过一个叫做 Skia 引擎进行页面绘图。
是一个 2D 的绘图引擎库,其前身是一个向量绘图软件,Chrome 和 Android 均采用 Skia 作为绘图引擎。Skia 提供了非常友好的 API,并且在图形转换、文字渲染、位图渲染方面都提供了友好、高效的表现。Skia 是跨平台的,所以可以被嵌入到 Flutter 的 iOS SDK 中,而不用去研究 iOS 闭源的 CoreGraphics / Core Animation。
Skia 是用 C++ 开发的、性能彪悍的 2D 图像绘制引擎,其前身是一个向量绘图软件。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。Android 自带了 Skia,所以 Flutter Android SDK 要比 iOS SDK 小很多。正是得益于 Skia 的存在:
用户可以看到一张图像展示,至少需要三类介质:CPU、GPU 和 显示器。CPU 负责图像的数据计算,GPU 负责图像数据的渲染,而显示器是最终图片展示的载体。CPU 拿到需要上屏的数据做处理和加工,处理完成之后交给 GPU,GPU 在渲染之后将数据放入帧缓冲区,随后随着控制同步信号 (VSync)以周期性的频率,从缓冲区内读出数据,在显示器上进行图像呈现。而且操作系统就是一个无限循环的机制,不停的重复上面的操作,进行显示器的更新.
Flutter 的渲染整体流程也是这样的, Dart 进行视图数据的合成,然后交给 Skia 引擎进行处理,处理之后再交给 GPU 进行数据合成,然后准备上屏。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以 60Hz 的屏幕就会一秒内发出 60 次这样的信号。
如上图所示,Flutter 渲染流程分为 7 个步骤:
首先是获取到用户的操作,然后你的应用会因此显示一些动画 ;
接着 Flutter 开始构建 Widget 对象。Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:
最后的光栅化由 Engine 层来完成。
布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将自己的约束传递给子节点,子节点根据接收到的约束来计算自己的大小,然后将自己的尺寸返回给父节点。整个过程中,位置信息由父节点来控制,子节点并不关心自己所在的位置,而父节点也不关心子节点具体长什么样子。
为了防止因子节点发生变化而导致的整个控件树重绘,Flutter 加入了一个机制——RelayoutBoundary,在一些特定的情形下 Relayout Boundary 会被自动创建,不需要开发者手动添加。
边界:Flutter 使用边界标记需要重新布局和重新绘制的节点部分,这样就可以避免其他节点被污染或者触发重建。就是控件大小不会影响其他控件时,就没必要重新布局整个控件树。有了这个机制后,无论子树发生什么样的变化,处理范围都只在子树上。
缓存:要提升性能表现,缓存也是少不了的。在 Flutter 中,几乎所有的 Element 都会具有一个 key,这个 key 是唯一的。当子树重建后,只会刷新 key 不同的部分,而节点数据的复用就是依靠 key 来从缓存中取得。
在确定每个空间的位置和大小之后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,然后把不同的 RenderObject 绘制到不同的图层上。
在布局完成之后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter 会把所有的 Element 绘制到不同的图层上。与布局过程类似,绘制的过程也是深度优先遍历,先绘制父节点,然后绘制子节点。以下图为例:节点 1、节点 2、节点 3、4、5,最好绘制节点 6。
如上图可以看到一种场景,就是比如视图可能会合并,导致 节点 2 的子节点 5 与它的兄弟节点 6 处于同一个图层,这样会导致当 节点 2 需要重绘的时候,与其无关的节点 6 也会被重绘,带来性能问题。
为了解决上面的问题,Flutter 提出了布局边界的机制 ——重绘边界(Repaint-Boundary)。在重绘边界内,Flutter 会强制切换新的图层,这样可以避免边界内外的互相影响,避免无关内容虽然处于同一个层级导致的不必要的重绘。
重绘边界的一个典型场景就是 ScrollView。ScorllView 滚动的时候会刷新视图,从而触发内容重绘,而当滚动内容重绘时,一般情况下其它内容是不需要被重绘的。这个时候重绘边界就非常有价值了。
这里简单理解,就是更精细化的对控件的更新,进行了小范围的控制。在时间复杂度和空间复杂度中进行权衡。未来我们优化业务,大概率也会优化这里,找到自身业务的平衡点。
最上面已经展示了 Flutter 的 7 层渲染流水线(Renderingpipline)的图里。这里主要描述一下对合成和渲染的理解。渲染流水线是由垂直同步信号(Vsync)驱动的。这个概念很类似我们平时说的 FPS 的概念,每秒 60 帧,过低的频率会显得页面很卡。当每一次 Vsync 信号到来以后,Flutter 框架会按照图里的顺序执行一系列动作:动画(Animate)、构建(Build)、布局(Layout)和绘制(Paint)
最终生成一个场景(Scene)之后送往底层,由 GPU 绘制到屏幕上。
Flutter App 只有在状态发生变化的时候需要触发渲染流水线。当你的 App 无任何状态改变的时候,Flutter 是不需要重新渲染页面的。所以,Vsync 信号需要 Flutter App 去调度。比如,我们在 Widget 内使用了 setState 方法改变了控件的状态。
整个渲染流水线是运行在 UI 线程里的,以 Vsync 信号为驱动,在框架渲染完成之后会输出 Layer Tree。Layer Tree 被送入 Engine,Engine 会把 Layer Tree 调度到 GPU 线程,在 GPU 线程内合成(compsite)Layer Tree,然后由 Skia 2D 渲染引擎渲染后送入 GPU 显示。这里提到 Layer Tree 是因为我们即将要分析的渲染流水线绘制阶段最终输出就是这样的 LayerTree。所以绘制阶段并不是简单的调用 Paint 函数这么简单了,而是很多地方都涉及到 Layer Tree 的管理。
Flutter 只关心向 GPU 提供视图数据,GPU 的 VSync 信号同步到 UI 线程,UI 线程使用 Dart 来构建抽象的视图结构,这份数据结构在 GPU 线程进行图层合成,视图数据提供给 Skia 引擎渲染为 GPU 数据,这些数据通过 OpenGL 或者 Vulkan 提供给 GPU。
这里描述一下合成的概念,所谓合成就是因为我们绘制的页面结构复杂,如果直接交付给绘图引擎去进行图层渲染,可能会出现大量的渲染内容重绘,因此,需要先进性一次图层合成,就是说先把所有的图层根据大小、层级等规则计算出最终的显示效果,将相同的图层合并,简化渲染树,提升渲染效率。
Flutter 会将合成之后的数据,交给 Skia 进行页面二维图层的渲染
粗略了整理了一下我最近这 2 周体验开发过程中认知的学习范围,这上面除了跟 APP 相关的部分,大部分场景已经通过代码体验和实践过了。这里由于篇幅限制,就不在一一的介绍了。最重要的是关注 Flutter 的官方文档。