Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。同时 Flutter还使用 Native引擎渲染视图,这无疑能为用户提供良好的体验
一、Flutter简介
跨平台自绘引擎
Flutter与用于构建移动应用程序的其它大多数框架不同,因为Flutter既不使用WebView,也不使用操作系统的原生控件。 相反,Flutter使用自己的高性能渲染引擎来绘制widget。这样不仅可以保证在Android和iOS上UI的一致性,而且也可以避免对原生控件依赖而带来的限制及高昂的维护成本。
Flutter使用Skia作为其2D渲染引擎,Skia 是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎。
目前Flutter默认支持iOS、Android、Fuchsia(Google新的自研操作系统)三个移动平台。但Flutter亦可支持Web开发(Flutter for web)和PC开发,本书的示例和介绍主要是基于iOS和Android平台的,其它平台读者可以自行了解。
高性能
Flutter高性能主要靠两点来保证,首先,Flutter APP采用Dart语言开发。
- Dart 在 JIT(即时编译) 模式下,速度与 JavaScript 基本持平
- 但是 Dart支持 AOT,当以 AOT模式运行时,JavaScript便远远追不上了
速度的提升对高帧率下的视图数据计算很有帮助。其次,Flutter使用自己的渲染引擎来绘制UI,布局数据等由Dart语言直接控制,所以在布局过程中不需要像RN那样要在JavaScript和Native之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以JavaScript需要和Native之间不停的同步布局信息,这和在浏览器中要JavaScript频繁操作DOM所带来的问题是相同的,都会带来比较可观的性能开销。
采用Dart语言开发
这是一个很有意思,但也很有争议的问题,在了解Flutter为什么选择了 Dart而不是 JavaScript之前我们先来介绍两个概念:JIT 和 AOT。目前,程序主要有两种运行方式:静态编译与动态解释。
- 静态编译的程序在执行前全部被翻译为机器码,通常将这种类型称为AOT(Ahead of time)。即 “提前编译”,AOT程序的典型代表是用C/C++开发的应用,它们必须在执行前编译成机器码
- 而解释执行的则是边翻译边运行,通常将这种类型称为JIT(Just-in-time)即“即时编译”,JIT的代表则非常多,如JavaScript、python等
- 事实上,所有脚本语言都支持JIT模式。但需要注意的是JIT和AOT指的是程序运行方式,和编程语言并非强关联的,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,它们可以 在第一次执行时编译成中间字节码、然后 在之后执行时可以直接执行字节码,也许有人会说,中间字节码并非机器码,在程序执行时仍然需要动态将字节码转为机器码,是的,这没有错,不过通常我们区分是否为AOT的标准就是看代码在执行之前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT。在此,读者不必纠结于概念,概念就是为了传达精神而发明的,只要读者能够理解其原理即可,得其神忘其形。
现在我们看看Flutter为什么选择Dart语言?笔者根据官方解释以及自己对Flutter的理解总结了以下几条(由于其它跨平台框架都将JavaScript作为其开发语言,所以主要将Dart和JavaScript做一个对比)
- 开发效率高
Dart运行时和编译器支持Flutter的两个关键特性的组合:
基于JIT的快速开发周期 Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;
基于AOT的发布包 Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力。 - 高性能
Flutter旨在提供流畅、高保真的的UI体验。为了实现这一点,Flutter中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现会丢帧的周期性暂停,而Dart支持AOT,在这一点上可以做的比JavaScript更好。 - 快速内存分配
Flutter框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。因此,拥有一个能够有效地处理琐碎任务的内存分配器将显得十分重要,在缺乏此功能的语言中,Flutter将无法有效地工作。当然Chrome V8的JavaScript引擎在内存分配上也已经做的很好,事实上Dart开发团队的很多成员都是来自Chrome团队的,所以在内存分配上Dart并不能作为超越JavaScript的优势,而对于Flutter来说,它需要这样的特性,而Dart也正好满足而已。 - 类型安全
由于Dart是类型安全的语言,支持静态类型检测,所以可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript是一个弱类型语言,也因此前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的 TypeScript以 及 Facebook 的Flow。相比之下,Dart本身就支持静态类型,这是它的一个重要优势。 - Dart团队就在你身边
看似不起眼,实则举足轻重。由于有Dart团队的积极投入,Flutter团队可以获得更多、更方便的支持,正如Flutter官网所述“我们正与Dart社区进行密切合作,以改进Dart在Flutter中的使用。例如,当我们最初采用Dart时,该语言并没有提供生成原生二进制文件的工具链(这对于实现可预测的高性能具有很大的帮助),但是现在它实现了,因为Dart团队专门为Flutter构建了它。同样,Dart VM之前已经针对吞吐量进行了优化,但团队现在正在优化VM的延迟时间,这对于Flutter的工作负载更为重要
二、Flutter框架结构
我们先对Flutter的框架做一个整体介绍,旨在让读者心中有一个整体的印象,这对初学者来说非常重要。如果一下子便深入到Flutter中,就会像是一个在沙漠中没有地图的人,即使可以找到一个绿洲,但是他也不会知道下一个绿洲在哪。因此,无论学什么技术,都要现有一张清晰的“地图”,而我们的学习过程就是“按图索骥”,这样我们才不会陷于细节而“目无全牛”。言归正传,我们看一下Flutter官方提供的Flutter框架图,如图所示:
Flutter Framework
这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:
- 底下两层(Foundation和Animation、Painting、Gestures) 在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的 dart:ui 包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力
- Rendering层 这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)
- Widgets层 Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道
Flutter Engine
这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。
对比Android自身的架架
这种设计符合Unix哲学的 正交性原则 (计算机网路中的OSI模型也是),可以很好地遮蔽每一个层的细节,接下来我们基于这个架构图讨论几个问题
- Flutter如何绘制UI
- 为什么选择Dart
- 如何和Native互动
- HotReload与动态化
GUI Framework
现代GUI框架的模型,无论是Web、PC、移动端、游戏开发,都是基于这种框架工作
- Widgets(控制元件,也叫 View Tree),它是用于描述使用者介面原始资料的树状结构。通常这一层根本不关心绘制,它只关心使用者对资料的操作
- Render Tree 它是一种更为抽象的树状资料结构,一般来说它是和上一步的 View Tree结构相同,并且它不关心原始资料,只关心控制元件的布局和大小。通过这一步计算出控制元件布局后才能真正地确定控制元件的外观。
- Layer Tree 跟 Render Tree是相对应的,这一步会主动触发 Render Tree中每个元素的外观渲染,在已知控制元件大小和位置的情况下决定每个控制元件的真正外观。但 Layer Tree的树状结构不是和 Render Tree一一对应的,Layer Tree有可能因为 Layer合併优化导致一层的 Render Tree叶子节点最终只对应一个 Layer
- Bitmap(位图),在已经决定好控制元件的大小位置以及长相后,剩下的工作就需要把这些东西组合起来显示到萤幕上。这一步原理比较简单,就是将前一步的 Layer合并成一张 Bitmap,这是一种最简单的影象储存形式。将 Bitmap光栅化后便可以提交给 GPU渲染
比如Android开发都很熟悉的 measure layout draw 就和前三层吻合,有了原始资料还不够,萤幕只关心每个画素点的值,所以最后要进行一次 光栅化,历史已经证明这种模型是目前来说相对高效的GUI方案
Flutter如何绘制UI
有了以上知识,我们来看Flutter的UI绘制过程
Flutter绘制流程1
Flutter绘制流程2
- GPU发出 Vsync 讯号,Dart捕获后就开始一帧的绘製
- Throttle 用来做节流,防止短时间内重复呼叫,提高效能
- Compositor 这一步进行 Layer合成,决定某一块具体显示哪一个 Layer的资料,可以额外的计算开支
- GL or Vulkan 这一阶段过后得到的将是一份向量图资料,在进行光栅化后提交给 GPU执行渲染即可
- Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU
Flutter的线程管理模型
默认情况下,Flutter Engine 层会创建一个Isolate,并且 Dart 代码默认就运行在这个 主Isolate 上。必要时可以使用 spawnUri 和 spawn 两种方式来创建新的Isolate,在 Flutter中,新创建的Isolate由 Flutter 进行统一的管理。
事实上,Flutter Engine 自己不创建和管理线程,Flutter Engine 线程的创建和管理是 Embeder 负责的,Embeder指的是将引擎移植到平台的中间层代码,Flutter Engine层的架构示意图如下图所示
在Flutter的架构中,Embeder提供四个Task Runner,分别是
- Platform Task Runner
- UI Task Runner Thread
- GPU Task Runner
- IO Task Runner
每个Task Runner负责不同的任务,Flutter Engine不在乎Task Runner运行在哪个线程,但是它需要线程在整个生命周期里面保持稳定
Flutter 是如何与原生Android、iOS进行通信的?
flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:
- BasicMessageChannel 用于传递字符串和半结构化的信息
- MethodChannel 用于传递方法调用(method invocation)
- EventChannel 用于数据流(event streams)的通信
每种Channel具有三个重要的成员变量:
- name: String类型,代表Channel的名字,也是其唯一标识符。
- messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。
- codec: MessageCodec类型或MethodCodec类型,代表消息的编解码器。
简述Flutter 的热重载
Flutter的HotReload确实让人耳目一新,但这东西可能只是对于客户端开发比较稀奇,从Instant Run到freeline,开发者一直希望能提高项目构建速度,更快地看到代码改动的结果,从原理上来说,只要这门语言及其所在平台支持解释执行(or JIT)和增量编译就可以做到很好的HotReload效果,flutter的这一个特性算是填上了之前Android开发的一个坑,创举肯定是算不上的。
- Flutter 的热重载是基于 JIT 编译模式的代码增量同步。由于 JIT 属于动态编译,能够将 Dart 代码编译成生成中间代码,让 Dart VM 在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。
- 热重载的流程可以分为 5 步,包括:
- 扫描工程改动(首先会扫描代码,找到上次编译之后有变化的Dart代码)
- 增量编译 (将这些变化的Dart代码转化为增量的Dart Kernel文件)
- 推送更新 (将增量的Dart Kernel文件发送到正在移动设备上运行的Dart VM)
- 代码合并
- Widget 重建 (触发widgets树的重新建立、重新布局、重新绘制)
Flutter 在接收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,大大缩短了从代码修改到看到修改产生的变化之间所需要的时间
- 另一方面,由于涉及到状态的保存与恢复,涉及状态兼容与状态初始化的场景,热重载是无法支持的,如改动前后 Widget 状态无法兼容、全局变量与静态属性的更改、main 方法里的更改、initState 方法里的更改、枚举和泛型的更改等
- 可以发现,热重载提高了调试 UI 的效率,非常适合写界面样式这样需要反复查看修改效果的场景。但由于其状态保存的机制所限,热重载本身也有一些无法支持的边界
不能使用的场景(所以对这个功能不能过于乐观,业务复杂之后能用的场景就不会太多):
- 代码出现编译错误的不能使用 Hot Reload
- Widget的状态更改不能使用 Hot Reload
- 在 Flutter 中,全局变量和静态字段被视为状态,因此在 Hot Reload 期间不会重新初始化。
- 修改通用类型声明时
三、为什么选择Dart
官方解释:
- Developer productivityJIT + AOTDart开发团队对于Flutter支持粒度很大
- Object-orientation
- Predictable, high performance
- Fast allocation.
其他声音:
- Dart 的性能更好,对高帧率下的视图数据计算很有帮助。
- 多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化
- Native Binding,在 Android上,v8的 Native Binding可以很好地实现,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基础框架的代码模式就很难统一了。而 Dart的 Native Binding可以很好地通过 Dart Lib实现。
- Fuchsia OS(谷歌的野心:5G + IOT)
- Dart是类型安全的语言,拥有完善的包管理和诸多特性
四、如何学习Flutter
资源
- 官网 阅读Flutter官网的资源是快速入门的最佳方式,同时官网也是了解最新Flutter发展动态的地方,由于目前Flutter仍然处于快速发展阶段,所以建议读者还是时不时的去官网看看有没有新的动态
- 源码及注释 源码注释应作为学习Flutter的第一文档,Flutter SDK的源码是开源的,并且注释非常详细,也有很多示例,实际上,Flutter官方的SDK文档就是通过注释生成的。源码结合注释可以帮你解决大多数问题
- Github 如果遇到的问题在StackOverflow上也没有找到答案,可以去github flutter 项目下提issue
Gallery源码 Gallery是Flutter官方示例APP,里面有丰富的示例,读者可以在网上下载安装。Gallery的源码在Flutter源码“examples”目录下
社区
StackOverflow 如果你还没听过StackOverflow,这是目前全球最大的程序员问答社区,现在也是活跃度最高的Flutter问答社区。StackOverflow上面除了世界各地的Flutter使用者会在上面交流之外,Flutter开发团队的成员也经常会在上面回答问题
- Flutter中文网社区 Flutter中文网 目前是最大的中文资源社区,上面提供了Flutter官网的文档翻译、开源项目、及案例,还有申请加入组织的入口哦
- 博客 随着Flutter技术的推广,相信很快网上将会有很多Flutter相关的文章、博客,读者可以多去浏览、阅读
五、总结
有了资料和社区后,对于我们学习者自身来说,最重要的还是要多动手、多实践,在本书后面的章节中,希望读者能够亲自动手写一下示例。准备好了吗,下一章中,我们将正式进入Flutter的世界!