[译]为什么Flutter选择Dart

这是一篇翻译,原文:Why Flutter Uses Dart

许多语言学家认为,一个人说的自然语言会影响他们的思维方式。同样的概念是否也适用于计算机语言?对于同一个问题,使用不同类型的编程语言的程序员往往会想出截然不同的解决方案。这里有一个更极端的例子,计算机科学家们为了鼓励更多的结构化程序而去掉了GOTO声明(与小说《1984》中的极权主义领袖从自然语言中驱逐异端词以消除思想罪的说法不尽相同,但你会明白的)。

这与Flutter和Dart有什么关系?其实有很大的关系。早期的Flutter团队评估了十几种语言,并选择了Dart,因为它符合他们构建用户界面的方式。

为什么开发者喜欢Flutter?答案是Dart。正如下面这则推特所说:

Jordy @JordyvD @flutterio让我看了一下@dart,我很高兴地玩了一下。#Dart是一门很棒的语言,而#flutterio把它带到了移动设备上,让它更进一步。

以下整理了Dart的特性,这些特性共同构成了Flutter不可缺少的功能。

  • Dart是AOT(预先编译)类型语言,可编译成快速、可预测的原生代码,这使得Flutter几乎所有的代码都可以用Dart编写。这不仅让Flutter变得快速,几乎所有东西(包括所有的widgets)都可以自定义。

  • Dart还可以通过JIT(即时编译),以实现快速的开发周期和改变游戏规则的工作流(包括Flutter流行的亚秒级、有状态的热重载)。

  • Dart让你更容易创建以60fps运行的流畅动画和转场。Dart可以在没有锁的情况下进行对象分配和垃圾回收。和JavaScript一样,Dart避免了抢先调度和共享内存(从而避免了锁)。因为Flutter应用程序被编译成了原生代码,所以它们不需要在层级领域之间建立缓慢的桥梁(例如,从JavaScript到原生)。它们的启动速度也更快。

  • Dart允许Flutter避免了单独的声明式布局语言,如JX或XML,或单独的可视化界面构建器,因为Dart的声明式、程序化布局很容易阅读和可视化。而且,由于所有的布局都在一种语言和一个地方,Flutter很容易提供高级工具,使布局变得简单易行。

  • 开发人员发现,Dart特别容易学习,因为它具有静态和动态语言用户都熟悉的功能。

并非所有这些功能都是Dart独有的,但它们的结合让Dart在实现Flutter方面具有独特的优势。以至于很难想象没有Dart,Flutter会有如此强大的功能。

本文的其余部分将更深入地介绍Dart的许多特点(包括其标准库),使其成为实现Flutter的最佳语言

编译和执行

[如果你已经了解静态语言与动态语言、AOT和JIT编译以及虚拟机等主题,你可以跳过这一部分。]

历史上,计算机语言被分为两类:静态语言(如Fortran或C语言,在编译时,变量在编译时被静态地键入)和动态语言(如Smalltalk或JavaScript,在运行时,变量的类型可以改变)。静态语言通常被编译成目标机器的原生机器码(或汇编代码)程序,在运行时由硬件直接执行。动态语言由解释器执行,不产生机器码。

当然,事情最终变得更加复杂了。虚拟机(VM)的概念开始流行,它实际上只是一个高级解释器,在软件上模仿硬件机器的高级解释器。虚拟机使得将一种语言移植到新的硬件平台上更容易。在这种情况下,虚拟机的输入语言往往是一种中间语言。例如,将一种编程语言(如Java)编译成中间语言(字节码),然后在虚拟机(JVM)上执行。

此外,现在还有即时编译器(JIT)。JIT编译器是在程序执行过程中运行,即时编译。原来在程序创建过程中(运行前)执行的编译器现在被称为预先编译器(AOT)。

一般来说,只有静态语言才适合AOT编译成原生机器码,因为机器语言通常需要知道数据的类型,而在动态语言中,类型是不固定的。因此,动态语言通常被解释或JIT编译。

当AOT编译是在开发过程中进行的,它必然会导致开发周期变慢(从对程序进行修改到能够执行程序以查看修改结果之间的时间)。但AOT编译的结果是,程序在运行时可以更可预测地执行,而且不需要暂停分析和编译。AOT编译后的程序的启动速度也更快(因为它们已经被编译过了)。

相反,JIT编译器提供了更快的开发周期,但会导致程序执行速度更慢或更笨拙。特别是,JIT编译器的启动时间更慢,因为当程序启动时,JIT编译器必须先进行分析和编译,然后才能执行代码。研究表明,如果一个程序的启动时间超过几秒钟,就会被人们放弃。

开胃菜已经上完了,如果能将AOT和JIT编译的优点结合在一起,啧啧啧,岂不是无敌?请继续看。

编译和执行Dart

在开发Dart之前,Dart团队的成员们已经在高级编译器和虚拟机方面做了一些开创性的工作,包括动态语言(如JavaScript的V8引擎和Smalltalk的Strongtalk)和静态语言(如Java的Hotspot编译器)。他们利用这些经验,使Dart在编译和执行方式上变得异常灵活。

适合同时被AOT和JIT编译的语言中,Dart是为数不多的语言之一(也许也是唯一的 "主流 "语言)。支持这两种编译方式为Dart和(特别是)Flutter提供了显著的优势。

在开发过程中使用JIT编译,编译速度会特别快。然后,当应用程序准备好发布时,再进行AOT编译。因此,在先进的工具和编译器的帮助下,Dart可以实现两个世界的最佳效果:极快的开发周期,以及快速的执行和启动时间。

Dart在编译和执行方面的灵活性并不止于此。例如,Dart可以被编译成JavaScript,这样它就可以被浏览器执行。这使得移动应用和Web应用之间的代码可以重用。开发者们说,他们的移动应用和Web应用之间的代码重用率高达70%。Dart也可以在服务器上使用,可以编译成原生代码,也可以编译成JavaScript并与node.js一起使用。

最后,Dart还提供了一个独立的虚拟机,它使用Dart语言本身作为中间语言(本质上就像一个解释器)。

Dart可以有效地编译AOT、JIT、作为解释器使用或移植到其他语言中。Dart的编译和执行不仅非常灵活,而且速度特别快。

接下来,我们将以Dart的编译速度为例,说明Dart的编译速度是如何改变游戏规则的.....

Stateful热重载

Flutter最受欢迎的特点之一是其极快的热重载速度。在开发过程中,Flutter使用了一个JIT编译器,通常在一秒钟之内就能重新加载并继续执行代码。在可能的情况下,App的状态会被保留在整个重载过程中,这样App就可以从原来的地方继续执行。

除非你亲自体验过,否则很难体会到开发过程中真正快速(可靠)的热重载是多么的重要。开发者们说,这改变了他们创建APP的方式——就像把他们的APP画成了生活。

以下是一位移动应用开发者对Flutter热重载的评价。

我想测试一下热重载,于是换了个颜色,保存了我的修改,然后...........爱了爱了! 这个功能真的是太神奇了。原本以为Visual Studio中的Edit & Continue功能已经很好,但这个简直让人震惊。有了这个特性,我觉得一个移动开发者的工作效率可以提高2倍。 对我来说,这真的是改变了游戏规则。当我部署代码时,我的代码需要很长时间,我就会失去专注力,我会做其他事情,当我回到模拟器/设备时,我已经忘记了我想要测试的内容。还有什么比让我失去5分钟的时间来移动一个控件的2px像素更令人沮丧呢?有了Flutter之后,这种情况将不复存在。

Flutter热重载让我们更容易尝试新的想法或实验替代方案,为创造力提供了巨大的推动力。

到目前为止,我们已经讨论了Dart是如何让开发者更好地完成任务的。接下来的内容是关于Dart如何也能让用户更轻松地创造出流畅的App,让用户满意。

避免卡顿

APP执行速度快只是基准线,执行流畅才是终极目标。即使是运行快速的动画,如果不稳定,也会显得很糟糕。然而,由于众多原因,防止卡顿极其困难。Dart有大量特性,可以避免常见的导致卡顿的情况。

当然,(与任何语言类似)用Flutter编写一个卡慢的app仍然是可能的;Dart的作用在于它的可预测性更强,并让开发者对其app的流畅性有更多的主导权,从而更容易提供最好的用户体验,这一点毋庸置疑。

使用Flutter创建的用户界面以60帧/秒的速度运行,其性能远远优于其他跨平台开发框架创建的用户界面。

而且不只是比跨平台的应用好,还不亚于最好的原生应用。

UI非常的流畅...........我从来没有见过这么流畅的安卓应用。

AOT编译器和“桥”

我们已经讨论过一个有助于保持流畅的特性,那就是Dart能够被AOT编译成原生机器码的能力。预编译的AOT代码比JIT更具有可预测性,因为在运行时没有停下来执行JIT分析或编译。

然而,AOT编译的代码还有一个更大的优势,那就是避免了"JavaScript桥"。当动态语言(如JavaScript)需要与平台上的原生代码进行互操作时,它们必须通过进行通信,这就导致了上下文切换,必须保存大量状态(可能是二级存储)。这些上下文切换是一个双重打击,因为它们不仅会让速度变慢,还会造成严重的卡顿。

注意:即使是编译后的代码也可能需要一个接口来与平台代码对话,这也可以称为桥,但它通常比动态语言所需要的桥要快几个数量级。此外,由于Dart允许像widget这样的东西被移入到app中,所以减少了桥的必要。

抢占式调度、时间分片和共享资源

大多数支持多线程并发执行的计算机语言(包括Java、Kotlin、Objective-C和Swift)都使用了抢占式调度来切换线程。每个线程被分配了一个"时间片"来执行,如果超过了分配的时间,线程就会使用上下文切换来抢占执行。但是,如果在线程之间共享的资源(如内存)被更新时发生抢占,那么就会造成数据竞态

数据竞态是一个双重打击,因为它们会导致严重的bug,包括导致应用程序崩溃和数据丢失,而且它们特别难以发现和修复,因为它们取决于独立线程的相对时间。当你在调试器中运行应用程序时,数据竞态不发生出来是很常见的。

修复数据竞态的典型方法是使用锁来保护共享资源,防止其他线程执行,但锁本身可能会导致卡顿,甚至更严重的问题(包括死锁和饿死)。

对于这个问题,Dart采取了不同的方法。Dart中的线程,称为隔离线程,不共享内存,这避免了大多数锁的需要。隔离线程通过通道传递消息进行通信,这类似于Erlang中的actors或JavaScript中的web worker。

Dart,就像JavaScript一样,是单线程的,这意味着它完全不允许被抢占。相反,线程明确地让出控制权(使用async/await、Futures或Streams)。这让开发者在执行层面有了更多的主导权。单线程可以帮助开发者确保关键函数(包括动画和过渡)在没有被抢占的情况下顺利执行完成。这通常是一个很大的优势,不仅是对用户界面,而且对其他客户端-服务器代码也是如此。

当然,如果开发者忘记了让出控制权,这可能会延缓其他代码的执行。不过我们发现,忘记让出控制权通常比忘记锁更容易发现和修复(因为数据竞态很难找到)。

内存分配和垃圾回收

导致卡顿的另一个严重原因是垃圾回收。事实上,这只是许多需要使用锁的语言访问共享资源(内存)的一个特殊情况。但在收集空闲内存的同时,锁可能会阻止整个应用程序的运行。但是,Dart几乎可以在没有锁的情况下执行垃圾回收。

Dart使用了一种先进的多生代垃圾收集和分配方案,对于分配许多短生命周期的对象来说,它的速度特别快(因为Flutter每帧都要重建不可变的视图树,对于像Flutter这样的响应式界面来说是完美的)。Dart可以通过单个指针碰撞来分配一个对象(不需要锁)。所以,这让界面滚动和动画效果变得平滑,而不会出现卡顿。

统一布局

Dart另一个优势是,Flutter不需要在你的程序和额外的模板化或布局语言(如JSX或XML)之间分割布局,也不需要单独的可视化布局工具。下面是一个简单的Flutter视图,用Dart写的。

new Center(child:
  new Column(children: [
    new Text('Hello, World!'),
    new Icon(Icons.star, color: Colors.green),
  ])
)

请注意,这段代码产生的输出是多么的可视化(即使你没有使用Dart的经验)。

请注意,现在Flutter使用了Dart 2,布局变得更加简单明了,因为新的关键字是可选的,所以静态布局看起来更像是用声明式布局语言写的,像这样:

Center(child:
  Column(children: [
    Text('Hello, World!'),
    Icon(Icons.star, color: Colors.green),
  ])
)

然而,我知道你可能会想——没有专门的布局语言怎么能叫优势呢?但实际上它是一个改变游戏规则的工具。以下是一位开发者在一篇题为 "为什么原生应用开发者应该谨慎看待Flutter"的文章中写道。

在Flutter中,布局只使用Dart代码来定义,没有XML/模板语言,也没有可视化设计器/故事板工具。 我的预感是,当听到这句话时,你们中的一些人甚至可能会有一点畏惧。我也是这么想的。用视觉工具来做布局不是更容易吗?在代码中写各种约束逻辑不是会让事情变得过于复杂吗? 对我来说,答案是否定的。天啊,真是大开眼界。

答案的第一部分就是上面提到的热重载。

我怎么强调都不为过,这比安卓的Instant Run或任何类似的解决方案都要领先很多年。它就这么跑起来了,即使在大型非核心的应用程序上也能跑得很好。它的速度非常快,这就是Dart为你赋予的能力。 在实践中,这让可视化的编辑器界面变得多余。我完全没有怀念XCode那个还挺不错的自动布局。

Dart创建了简洁、易于理解的布局,而"绝对高速"的热重载特性让你可以立即看到结果,而且包括你布局中的非静态部分。

因此,我在Flutter (Dart)中写布局的效率比Android/XCode都要高。一旦你掌握了它的窍门(对我来说,这意味着要花几个星期的时间),由于很少有上下文的切换,所以开销大大减少。人们不需要切换到设计模式,用鼠标点点点,然后想是否需要用程序化的方式来完成一些事情,如何实现等等。在Flutter里面,一切都是程序化的,而且API设计得非常好、很直观,比自动布局/XML布局提供的结构要强大得多。

例如,这里是一个简单的列表布局,在每一项之间增加一个分隔线(横线),以程序化方式定义:

return new ListView.builder(itemBuilder: (context, i) {
  if (i.isOdd) return new Divider(); 
  // rest of function
});

在Flutter中,所有的布局都存在于一个地方,不管是静态布局还是程序化布局。而新的Dart工具,包括Flutter检查器和大纲视图(利用所有的布局都在一个地方)使复杂、漂亮的布局变得更加容易。

Dart是一种专利语言吗?

不,Dart(就像Flutter一样)是有清晰许可证和ECMA标准的完全开源的程序。Dart在Google内部和外部都很受欢迎。在Google内部,它是发展最快的语言之一,Adwords、Flutter、Fuchsia等都在使用它;在外部,Dart仓库有100多个贡献者。

Dart的开放性的一个更好的指标是Google之外的社区的增长。例如,我们看到源源不断的来自第三方的关于Dart(包括Flutter和AngularDart)的文章和视频,我在这篇文章中引用了其中的几篇。

除了Dart本身的外部贡献者之外,Dart公共库中还有超过3000个包,包括Firebase、Redux、RxDart、国际化、加密、数据库、路由、集合等。

Dart程序员会很好找吗?

如果知道Dart的程序员不多,是否会更难找到合格的程序员?具有讽刺意味的是,Dart让找程序员变得更容易,因为它是一门很容易学习的语言。已经懂得Java、JavaScript、Kotlin、C#或Swift等语言的程序员几乎可以立即开始使用Dart编程。除此之外,Dart的热重载功能鼓励用户玩转Dart和实现想法,这使得学习Dart的速度更快、更有乐趣。

以下是一位程序员在一篇题为《2018年为什么会有Dart》的文章中说的。

Dart,这个用于开发Flutter应用的语言,学习起来太简单了。Google在创建简单、文档化语言方面有丰富的经验,比如说像Go这样的语言。到目前为止,对我来说,Dart让我想起了Ruby,学习它是一种享受。而且它不仅适用于移动端,也适用于Web开发。

来自另一篇关于Flutter和Dart的文章,标题为 "为什么是Flutter?而不是其他XX框架?或者说,为什么我要全身心投入到Flutter世界。"

Flutter使用的Dart语言也是由google创建的,说实话,我并不喜欢C#或JAVA这样的强类型语言,但我不知道为什么Dart的编码方式似乎不一样,我觉得写起来很舒服,也许是因为学习它的时候感到简单而直观。

通过大量的用户体验研究和测试,Dart是专门为熟悉和易学而设计的。例如,在2017年上半年,Flutter团队与8名开发者一起做了一个用户体验研究。我们给他们做了一个关于Flutter的简短介绍,然后让他们在一个小时左右的时间里,让他们自由活动,创建一个简单的视图。所有的参与者都能马上开始编程,尽管他们之前从未使用过Dart,但都能马上开始编程。他们的专注点从语言转移到编写响应式界面,Dart就这样成功了。

最后,有一位学员(在任务中进步特别大)没有提到任何关于语言本身的事情,所以我们问他们是否意识到自己用的是什么语言?他们不知道。所以说,语言并不重要,他们在几分钟内就能用Dart编程。

学习一个新系统最难的部分通常不是学习语言本身的语法,而是学习它所有的库、框架、工具、模式以及写好代码的最佳实践。而Dart的库和工具都非常好,并且有很好的文档。有一篇文章宣称,"作为奖励,他们还对代码库进行了极大的保护,他们的文档是我见过的最好的。" 学习Dart所花费的精力,很容易被学习其它部分所节省的时间所弥补。

最直接的证据是,谷歌内部的一个大项目希望将他们的移动应用移植到iOS上。他们准备雇佣一些iOS程序员,但他们决定尝试一下Flutter。他们监测了开发人员快速入门flutter花了多长时间。结果显示,一个程序员可以在三周内学会Dart和Flutter,并在三周内变得富有成效。与之比较的是,他们之前观察到,程序员需要5周的时间才能在Android系统上达到熟练的水平(更何况他们还得雇佣和培训iOS的开发人员)。

最后,"我们为什么选择Flutter,以及它是如何让我们的公司变得更好的 "这篇文章来自于一家公司,他们将他们的大型企业级应用在三个平台(iOS、Android和web)上都搬到了Dart上。他们的结论是:

更加容易招聘。不管是来自Web、iOS还是Android,我们现在都会选择最优秀的候选人。 现在我们有3倍的带宽,因为我们所有的团队都集中在一个代码库中。 知识共享达到了前所未有的高度

通过使用Dart和Flutter,他们的工作效率提高了三倍。考虑到他们之前所做的事情,这应该并不奇怪。和许多公司一样,他们也在为每个平台(web、iOS和Android)构建单独的应用程序,使用不同的语言、工具和程序员。切换到Dart意味着他们不再需要雇佣三种不同的程序员。而且他们很容易将现有的程序员转移到Dart上。

大家发现,一旦程序员开始使用Flutter,他们往往会爱上Dart。他们喜欢这种语言的亲切感,以及缺乏仪式感。他们喜欢语言的特性,如级联、命名参数、异步/等待和流等。而最重要的是,他们喜欢Flutter的特性(如热重载),而这些功能都是由Dart实现的。最重要的是,Dart帮助他们构建漂亮、有表现力的应用程序。

Dart 2

就在这篇文章发布的时候,Dart 2正在发布。Dart 2的重点是改善客户端App的构建体验,包括开发者速度、改进的开发者工具,以及类型安全等。例如,Dart 2的功能是完善的类型系统和类型推断。

Dart 2还让new关键词变得可有可无。这意味着可以在完全不使用任何关键字的情况下描述很多Flutter视图,使其不那么杂乱,更容易阅读。比如说:

Widget build(BuildContext context) =>
  Center(child:
    Column(children: [
      Text('Hello, World!'),
      Icon(Icons.star, color: Colors.green),
    ])
  )

Dart 2还使用了类型推断,通过不要求在const上下文中多余地指定const,使const关键字使用变为可选的。例如:

const breakfast = {
   const Doughnuts(): const [const Cruller(), const BostonCream()], 
};

现在可改写成:

const breakfast = {
   Doughnuts(): [Cruller(), BostonCream()],
};

因为breakfast是常量,所以其他东西都可以推断为常量。

诀窍就是专注

Dart 2中的改进主要集中在优化客户端开发上。但Dart仍然会是一种很好的语言,可以用来构建服务器端、桌面端、嵌入式系统或其他程序。

专注是个好东西。几乎所有经久不衰的流行语言都得益于深度专注。比如说:

  • C语言是一种用于编写操作系统和编译器的系统编程语言。但它的作用远不止于此。

  • Java 是一种为嵌入式系统设计的语言。

  • JavaScript 是一种为网络浏览器设计的脚本语言(!)。

  • 即使是备受诟病的PHP也成功了,因为它专注于编写个人主页(它的名字就是从那里来的)。

另一方面,许多语言明确地尝试(失败了)完全通用化,如PL/1和Ada等。最常见的问题是,如果没有重点,这些语言就成了谚语中的厨房水槽。(译者注:指无所不包,一应俱全)

许多特性使Dart成为优秀的客户端语言,也使它成为一种更适合在服务器端使用的语言。例如,Dart避免了抢占式多任务处理,使其在服务器端具有与Node相同的优势,但拥有更好、更安全的类型。

编写嵌入式系统的软件也是如此。关键在于,Dart能够可靠地处理并发输入。

最后,Dart在客户端上的成功,必然会让人们对在服务器上使用它产生更多的兴趣——就像JavaScript和Node。为什么要强迫人们使用两种不同的语言来构建客户端-服务器软件?

总结

对于Dart来说,这是一个激动人心的时刻。使用Dart的人都很喜欢它,而Dart 2中的新特性使它成为你的工具库中更有价值的补充。如果你还没有使用过Dart,我希望这篇文章已经为你提供了关于Dart的有价值的信息(新特性或不同之处),并希望你能尝试Dart和Flutter。

Last updated