我们要使用Flutter框架,必然会接触一门新的语言——Dart。而对大多数人来说切换一门新的语言并不是那么容易的事情,需要付出一定的学习成本。
那么在学习Flutter之前我们必然想要了解这门语言的背景。
Flutter为什么会选择Dart作为开发语言?
Dart有什么特性?
从而来判断是不是值得我们付出时间成本来学习和使用。
1 Flutter为什么选择dart做为开发语言?
从非官方的角度
Google跟Oracle的在Java API版权问题的纠纷,如果Google不用java应该用什么语言替代,而这么语言需要满足高性能并且能尽快上手的要求。
而且为了避免类似Java API的版权纠纷,当然自家的语言最好。
而Dart刚好满足这些要求。
从官方的角度来说
Flutter在四个主要维度进行了评估,考虑了框架作者、开发人员和最终用户的需求等因素,然后发现大部分的语言只符合一部分要求,但是Dart在所有的评估维度上得分都很高,并且符合所有的要求和标准。
对于客户端的开发一大痛点,就是稍微改动一点就需要编译很久才能在设备上看到效果,而Dart的JIT(Just-in-time 即时编译)使得热重载成为可能,从而大大提高开发效率;
同时Dart也支持AOT(Ahead Of Time 提前编译),可生成高效的ARM代码,可以快速启动并拥有可预测的生产部署性能。
“Dart在以下评估标准中得到高分:
- 开发人员的效率。Flutter的主要价值主张之一是通过让开发人员使用相同的代码库为iOS和Android创建应用程序,从而节省了工程资源。使用高效的语言可以进一步加速开发周期,并使Flutter更具吸引力。这对我们的framework团队和开发人员都非常重要。大部分Flutter功能都是用Dart实现,因此我们需要在10万行代码时能保持高效的而不会牺牲framework和widget的可读性
- 面向对象。虽然我们可以使用非面向对象的语言,但这意味着要重新解决几个难题。另外,绝大多数开发人员都具有面向对象开发的经验,因此更容易学习如何使用Flutter进行开发
- 可预测,高性能。借助Flutter,我们希望使开发人员能够快速创建流畅的用户体验。为了实现这一点,我们需要能够在每个动画帧中运行大量的代码。这意味着我们需要一种既能提供高性能又能提供可预测性能的语言,而不会出现会导致丢帧的周期性暂停。
- 快速内存分配。Flutter框架使用函数式流,它很大程度上依赖于底层的内存分配器,从而有效地处理小的、短期的内存分配会非常重要,所以在缺乏此功能的语言中Flutter无法有效地工作。”
针对以上几点这里主要就程序员比较关的两点来展开来讨论——效率提升和高性能
2 开发效率的提升
为什么dart能带来开发效率的提升?
我相信Flutter诞生的主要目的也是这点,一套代码多端运行,降低了人力和开发周期,提高开发效率。在这条道路上前人做了许多努力,比如React Native、Weex。
大家的目的都是为了能高效开发并且尽可能的保证性能。
2.1 Dart的编译和执行
Dart 可以进行高效的 AOT 编译或 JIT 编译,还可以解释或转译成其他语言。
1. AOT——运行前编译
比如普通的静态编译。 通常需要编译成目标机器的本地机器代码(或 汇编代码 )程序,再运行。也就是所谓的提前编译。 C语言就是需要提前编译的。优势:
- 程序运行的就是已编译好的机器码,因而可以避免在运行时的编译的性能和内存消耗
- 降低程序启动时间
- 执行速度快
劣势:
- 每一次修改都需要编译,无法实现热重载
- 安装慢
- 占内存占外存大
2. JIT——运行时编译
通俗来讲,就是实时的把代码编译为目标机器上的机器码。一般解释型语言就是JIT的编译方式比如JavaScript/PHP/Ruby/Python等。
优势:
- 可以根据当前硬件情况和程序的运行情况实时编译生成最优机器指令
- 当程序需要支持动态链接时,只能使用JIT
- 可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用
劣势:
- 由于边解释边执行,执行速度比较慢
- 编译需要占用运行时内存资源,会导致性能损耗。
而Dart在开发时使用JIT模式,使得可以动态根据代码变更实时渲染页面,从而提高开发效率,缩短开发周期。
同时在打包发布时是AOT模式,将代码编译之后在终端来执行编译之后的机器吗,又能保证执行速度和性能。
Dart 在编译和执行方面的灵活性并不止于此。Dart 还可以编译成 JavaScript在浏览器执行。也就是说可以大大提高代码的利用率。
2.2 Dart的独立虚拟机
flutter开发大家最喜欢的一个能力就是——Hot reload (热重载)。
Flutter之所以能够热重载,一方面归功于Dart支持JIT编译方式;另一方面是因为Dart提供了Dart VM。
Hot Reload大致流程如下:
在我们更改代码并保存之后,flutter会扫描出修改的内容,然后编译成kernel files,然后通过http协议及相应端口从本地发送到Dart VM。然后通过RPC协议来进行资源加载并重新渲染界面。
相对于现阶段客户端开发——修改代码必须经过很久的编译才能看到效果。Flutter 的Hot reload大大提高开发效率,缩短开发周期。
3 Dart的高性能
Dart高性能主要从以下两点上来说:
- Dart针对高频率循环刷新在内存层面进行了优化,使得Dart能以60fps 或者120fps更新界面,其性能远远优于使用其他跨平台开发器框架创建的用户界面
- 没有了桥接。
3.1 60fps 或者120fps
Flutter遵循了ios/android 以显示器的频率刷新的模式。
我们知道不同频率的显示屏刷新不一样,比如 iPhone是 60Hz、iPad Pro是120Hz。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),而 60Hz的屏幕就会一秒内发出 60次这样的信号, 120Hz的屏幕一秒发出120次这样的信号。
flutter的渲染流程,我们看Flutter官方提供的两张图:
GPU Vsync同步流程图
具体渲染过程
Flutter架构图
GPU同步 VSync信号到 UI线程,UI线程驱动Framework(Dart)经过nimations, build,layout,compositing,paint构建出渲染树(Layer tree),然后由GPU进程进行图层合成(Compositor)生成纹理,然后由 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU,GPU处理后更新显示器。
3.2 没有了桥接
可以看React Native之类的框架和Flutter对比图:
React Native这样的跨平台框架由于只是扩展调用了 OEM组件,其使用JavaScript动态语言来跨平台开发,动态语言在与Native本地代码交互时必然要通过桥接进行通讯。这种通讯必然会导致上下文切换,同时会产生一些中间状态的存储,会减慢速度甚至造成严重的卡顿。
而flutter从开发语言到图形渲染引擎一套流程全部自成一体,经过AOT编译成机器码之后,运行速度更快。4 其他特点
4.1 isolate机制
什么是isolate机制?
由于Dart是单线程的语言。但是在很多应用场景中需要用到并发,所以Dart提供了isolate机制。Dart的isolate机制就是其并发机制,类似于web开发中的Web Worker 对于单线程的JavaScript的作用。
可以看看isolate创建过程:
- 初始化isolate数据结构
- 初始化堆内存(Heap)
- 进入新创建的isolate,使用跟isolate一对一的线程运行isolate
- 配置Port
- 配置消息处理机制(Message Handler)
- 将isolate注册到全局监控器(Monitor)
因而Dart的isolate之间是不可见的,但是他们可以通过传递message来进行交流。通过isolate创建过程可以知道消息处理机制是每一个isolate都有的。
一般讲具有大量计算的任务放在isolate里处理。
网上有个很好的isolate例子,从本地应用中读取一个长度数万单词列表,返回一个List。
首先需要创建两个方法,一个处理单词,一个暴露给外部。
class MyWordReader
Future<String> _getFilePath() async {
// async function here
}
Future<List<CYWord>> getRawWordList() async {
//取得文件路径
String filePath = await _getFilePath();
//to be done
}
static Future<void> _getRawWordList(SendPort sendPort) async {
//to be done
}复制代码
完整实现:
class MyWordReader{ Future<String> _getFilePath() async { // async function here } Future<List<CYWord>> getRawWordList() async { //取得文件路径 String filePath = await _getFilePath(); //创建一个 ReceivePort ReceivePort receivePort = ReceivePort(); //创建 isolate , 并且将和 ReceivePort 对应的 SendPort 传给 isolate await Isolate.spawn(_getRawWordList, receivePort.sendPort); List<String> words; await for (var value in receivePort) { if (value is SendPort) { //接受到另一个 isolate 的SendPort,使用该 port 发送文件路径 SendPort sendPort = value; sendPort.send(filePath); } else if (value is List) { //接收到结果,结束 words = value; receivePort.close(); break; } } return words; }} static Future<void> _getRawWordList(SendPort sendPort) async { ReceivePort port = ReceivePort(); sendPort.send(port.sendPort); //等待文件路径 String filePath = await port.first; port.close(); List<String> result = new List<String>(); var fields = await input .transform(utf8.decoder) .transform(csvCodec.decoder) .toList(); for (List<dynamic> list in fields) { if (list.length > 0) { result.add(list[0]); } } //返回结果 sendPort.send(result); }}复制代码
代码里的ReceivePort 和 SendPort 是一一对应的,每个 isolate 都有自己的 ReceivePort 和接受到的另一个 isolate 的 SendPort。用作isolate之间的通信的。
4.2 Even Loop——事件循环
如果你了解JavaScript的事件循环机制,应该对此不会感到陌生。
事件循环就是按照FIFO的原则处理Event队列里处理事件,直到队列为空。
同样作为单线程的Dart也有一套事件循环机制。
Dart通JavaScript一样在执行过程中通过事件循环机制使其执行不被打断。Event 队列处理过程如下:
dart在是单线程和JavaScript一样在执行过程中每次都有一个Event Loop,但是有两个队列,前面提到了Event队列还有一个microtask队列。
event队列包含所有外来的事件:I/O,mouse events,drawing events,timers,isolate之间的message等。
microtask 队列在Dart中是必要的,因为有时候事件处理想要在稍后完成一些任务但又希望是在执行下一个事件消息之前。
event队列包含Dart和来自系统其它位置的事件。但microtask队列只包含来自当前isolate的内部代码。
可以看一下Dart执行的流程图:
由图可知当main方法退出之后,event loop就开始了。在事件循环的过程中Dart或首先清空microtask队列,然后再去执行Event 队列。
所以应该注意: 当事件循环正在处理micro task的时候event队列会被堵塞,这时候app就无法进行UI绘制、响应鼠标事件和I/O等事件等。