一、为什么选择Flutter
随着无线时代的来临,怎么样用最标准化的手段能够让更多的人开发这个页面、怎么样能够提供像H5一样标准的页面,成为大前端时代开发者们最关心的事情。
我们把时间线拉长,来看看移动端跨平台技术经过了一个怎样的发展史:下面主要介绍在这个发展过程中跨平台技术有了哪些进步或者做了哪些优化。
Ionic/Cordova(Hybrid): 在技术原理上的核心是,将原生的一些能力通过JSBridge封装给Web来调用,扩充了Web应用能力。但是这种方法有两个不足,一是依赖客户端,二是在性能和体验上都非常依赖于Web端。因此,整体上的体验不可预知。目前这个技术还经常被应用到,例如,当前App内会提供白名单域名和可调用的JSBridge方法,由此来增强H5与客户端交互能力,从而提升App内H5的灵活性。
React Native/Weex: 在原来的Hybrid的JSBridge基础上进行改进,将JavaScript的界面以及交互转化为Native的控件,从而在体验上和原生界面基本一致。但因为是JIT模式,因此需要频繁地在JavaScript与Native之间进行通信,从而会有一定的性能损耗影响,导致体验上与原生会有一些差异。
Flutter: 取长补短,结合了之前的一些优点,解决了与Native之间通信的问题,同时也有了自渲染模式(框架自身实现了一套UI基础框架,与原来的渲染模式基本一致)。从而在体验和性能上相对之前的两种框架表现都较好。
选择Flutter并不是为了代替iOS或者Android,而是做一个技术互补,比如,Flutter负责业务功能,而iOS和Android则负责部分的底层交互提供服务给到Flutter应用,这里大胆预测一下未来跨端技术团队的组成:
二、Flutter简介
Flutter是一款移动应用程序跨平台框架,使用一种语言(Dart)编写的同一份代码可以生成iOS和Android两个高性能、高保真的应用程序。
Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。兼容滚动行为、排版、图标等方面的差异。那么Flutter是如何编译成原生app的呢?
Flutter不借助原生的渲染能力,而是自己实现了一套与Android和iOS一样的渲染原理,从而在性能上与原生平台保持基本一致。不过这里由于目前 Flutter只是一个UI框架,因此在原生功能方面还是需要依赖原生平台,这也是它存在的一些问题。
三、Flutter运行原理
如前面已提到的那样,Flutter是重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案。这样不仅可以保证视图渲染在Android和iOS上的高度一致性(即高保真),在代码执行效率和渲染性能上也可以媲美原生App的体验(即高性能)。那Flutter是怎么运行的呢?
我们从图像显示的基本原理说起。
在计算机系统中,图像的显示需要CPU、GPU和显示器一起配合完成:CPU负责图像数据计算,GPU负责图像数据渲染,而显示器则负责最终图像显示。
CPU把计算好的、需要显示的内容交给GPU,由GPU完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒60次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。
操作系统在呈现图像时遵循了这种机制,而Flutter作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释Flutter的绘制原理。
可以看到,Flutter关注如何尽可能快地在两个硬件时钟的VSync信号之间计算并合成视图数据,然后通过Skia交给GPU渲染:UI线程使用Dart来构建视图结构数据,这些数据会在GPU线程进行图层合成,随后交给Skia引擎加工成GPU数据,而这些数据会通过OpenGL最终提供给GPU渲染。
在进一步学习Flutter之前,我们有必要了解下构建Flutter的关键技术,即Skia和Dart。
备注:
Skia是一款用C++开发的、性能彪悍的2D图像绘制引擎,Skia保证了同一套代码调用在Android和iOS平台上的渲染效果是完全一致的。
Dart是一个专注于前端(mobile/web)UI(用户交互)开发的强类型语言。
在了解了Flutter的基本运作机制后,我们再来深入了解一下Flutter的实现原理。
首先,我们来看一下Flutter的架构图。我希望通过这张图以及对应的解读,你能在开始学习的时候就建立起对Flutter的整体印象。
注:此图引自Flutter System Overview
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)以树的形式组织,即控件树。Flutter通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。而渲染对象树在Flutter的展示过程分为三个阶段:布局、绘制、合成和渲染。
(一)布局
Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。
为了防止因子节点发生变化而导致整个控件树重新布局,Flutter加入了一个机制——布局边界(Relayout Boundary),可以在某些节点自动或手动地设置布局边界,当边界内的任何对象发生重新布局时,不会影响边界外的对象,反之亦然。
(二)绘制
布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。
以下图为例:节点1在绘制完自身后,会再绘制节点2,然后绘制它的子节点3、4和5,最后绘制节点6。
可以看到,由于一些其他原因(比如,视图手动合并)导致2的子节点5与它的兄弟节点6处于了同一层,这样会导致当节点2需要重绘的时候,与其无关的节点6也会被重绘,带来性能损耗。
为了解决这一问题,Flutter提出了与布局边界对应的机制——重绘边界(Repaint Boundary)。在重绘边界内,Flutter会强制切换新的图层,这样就可以避免边界内外的互相影响,避免无关内容置于同一图层引起不必要的重绘。
重绘边界的一个典型场景是Scrollview。ScrollView滚动的时候需要刷新视图内容,从而触发内容重绘。而当滚动内容重绘时,一般情况下其他内容是不需要重绘的,这时候重绘边界就派上用场了。
(三)合成和渲染
终端设备的页面越来越复杂,因此Flutter的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。
合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示。
四、总结
咱们从各种业界主流跨端方案与Flutter的对比开始,到Flutter的简要介绍以及Flutter的运行机制,并以界面渲染过程为例,从布局、绘制、合成和渲染三个阶段讲述了Flutter的实现原理。相信大家对Flutter已经有一个整体认知,赶快一起上手操作起来吧!