为什么 Flutter 选择了 Dart 语言

2020-07-08

很多语言学家认为人们说的自然语言会影响思考本身。这种想法同样适用于计算机语言吗?使用不同编程语言的程序员针对相同问题往往会想出截然不同的解决方案。举个极端的例子,计算机科学家为了鼓励大家写出更具结构化的程序而去除了 goto 语句(并不完全像小说 1984 中极权主义者故意从自然语言中抹去异端词汇来消灭思想罪,不过也大差不离啦)。

但这跟 Flutter 和 Dart 有什么关联呢?实际上相当有关系。早期的 Flutter 团队评估了不下十余种语言,最后选择 Dart 是因为它最适合他们构建用户界面的方式。

Dart 是很多开发者喜欢 Flutter 的一个重要原因。就像这条推说的:

下面是一个 Dart 特性的快速查阅清单,所有这些特性一起让 Dart 之于 Flutter 变得不可或缺。

  • Dart 支持 AOT (Ahead Of Time)编译,生成执行快、可预测的机器码。这使得几乎所有的 Flutter 组成都能用 Dart 来实现。这不仅使 Flutter 执行很快,实际上也使所有东西(包括所有 widgets)都能被定制。
  • Dart 同时也支持 JIT (Just In Time)编译,这极大加快了开发流程,甚至可以说带来了颠覆性的工作流(包括 Flutter 广受欢迎的次秒级维持状态的热更新)。
  • Dart 使写出 60fps 流畅运行的动画变得简单。Dart 还能做对象内存分配和垃圾回收,并且不用锁。并且就像 Javascript 一样,Dart 避免了抢占式任务调度和内存共享(也因此避免了锁)。因为 Flutter 应用会被编译成机器码,所以它们不需要低效率的桥来沟通不同的领域(比如,Javascript 到 native)。
  • Dart 让 Flutter 不需要一种分离的声明式布局语言比如 JSX 或者 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 在编译和执行上变得异乎寻常的灵活而具有弹性。

Dart 是少有的(可能是唯一的“主流”语言)同时适合 AOT 和 JIT 编译的语言。这也是 Dart,尤其是 Flutter 的一个相当重要的优势。

开发阶段使用 JIT 编译,获得极快的开发体验。当应用准备发布的时候,再通过 AOT 编译。同时借助高级工具和编译器的帮助,Dart 做到了鱼和熊掌兼得:极快的开发效率,以及快速的启动和执行。

Dart 在编译和执行上的灵活性并没有到此为止。比如,Dart 还可以被编译成 Javascript,因此可以被浏览器执行。这样就能在移动应用和Web应用之间复用代码。开发者已经反馈说他们可以在移动应用和Web应用间复用高达 70% 的代码。Dart 还能用在服务端,可以编译成机器码执行,或者编译成 Javascript 在 Node.js 中运行。

最后,Dart 还提供了独立的虚拟机,虚拟机本身也是将 Dart 作为中间代码执行(这本质上就像解释器)。

Dart 可以高效地被 AOT 或 JIT 编译,可以被解释执行,也可以被转译成其他语言。不仅如此,它编译和运行的速度也很快。

维持状态的热更新

Flutter 一个最受欢迎的特性是其极快的热更新。在开发阶段,Flutter 使用 JIT 编译,可以做到在1秒内更新并继续执行代码。并且应用的状态在可能的情况下也会被保留,所以更新后应用还是维持原来的状态。

你如果没有亲自体验,你很难体会快速(并且可靠)的热更新对于开发有多么重要。一些开发者反馈说这已经改变了他们创造应用的方式,就像使得他们的应用变得有生命了一样。

下面是一位开发者对于 Flutter 热更新的评价:

我曾想测试一下热更新,所以我改了一个颜色并保存了我的修改,然后...我就爱上了❤️。

这个特性真的太神奇了。我曾认为 Visual Studio 中的“编辑和继续(Edit & Continue)”已经很好了,但这个简直让人震惊。我想也只有这样,移动端开发者才可以获得双倍的生产力。

这对我真的是天翻地覆的改变。以前每当我要部署我的代码,它都需要花费很长时间,因此我就失去了专注去做其他事情。当我重新把注意力转回模拟器或者设备的时候,我已经忘了需要测试什么了。还有什么比调整一个控件2像素而需要浪费5分钟更让人沮丧的呢?有了 Flutter 这一切都变成了过眼云烟。

Flutter 的热更新让试验新想法或者尝试替代方案更快更容易,因此极大地提升了创造力。

目前为止,我们讨论了 Dart 是如何地对开发者友好。下面我们来讨论 Dart 又是如何更容易开发出流畅顺滑愉悦用户的应用。

避免抖动

应用运行快当然是好的,如果还能流畅顺滑那就更好了。如果一个动画有抖动即便再快也是糟糕的。然而,避免抖动是困难的因为有各种原因会造成这种情况。Dart 有很多特性来避免这些通常会造成抖动的原因。

当然,就像其他语言一样,你用 Flutter 还是可能写出体验糟糕的应用。Dart 使开发结果变得更可预测,并且给了开发者更多的掌控来写出平滑的应用,这使得实现最佳的用户体验变得更为容易,简直无人能比。

那结论究竟怎样呢?

相比其他跨平台的开发框架,实现 60fps 的渲染 Flutter 的表现要好太多了。

不特比其他跨平台框架要好,甚至和原生应用一样好。

UI 是如此的顺滑...我从来没有见过如此顺滑的安卓应用。

AOT 编译和 “桥”

我们已经讨论过一个帮助应用变得顺滑的特性,那就是 Dart 可以通过 AOT 编译成机器码。预编译好的 AOT 代码要比 JIT 更可预测,因为不用在运行时进行分析和编译而暂停执行。

其实,AOT 编译的代码还有一个巨大的优势,那就是避免了 “Javascript bridge”。当动态语言(比如 Javascript)需要和平台的原生代码交互的时候,他们之间需要通过一个桥来通信,这会导致上下文切换因而需要存储大量的状态数据(可能存到二级存储)。这些上下文切换是对性能的双重打击,他们不但让应用运行更慢,也会带来严重的抖动。

提醒:即便编译好的代码某种程度上也需要和平台代码交互,这也可以被称为桥。但这通常比动态语言需要的桥要快很多,可以说是数量级上的差距。除此之外,Dart 允许比如像 Widgets 之类打包进应用,所以对桥的依赖更低了。

抢占式调度,时间分片,以及共享资源

许多计算机语言支持多线程并行执行(包括 Java,Kotlin,Objective-C,以及 Swift),通过抢占式多任务处理来在线程间切换。每一个线程会被分配一个时间切片来执行,并且如果执行超过了所分配的时间就会通过上下文切换被其他线程抢占替代。然而,如果抢占发生的时候某个共享的资源(比如内存)正好在更新中,那就会导致 竞态 的问题。

竞态也是一种双重打击,因为它们会导致严重的 bug,包括会导致应用崩溃以及数据丢失,并且这些 bug 还很难被复现和修复因为它们依赖独立线程的相对执行时间。当你在 debugger 模式下运行应用,竞态问题不显现的情况真的太常见了。

一种典型的修复竞态问题的方式是通过锁来阻止其他线程执行以此保护共享的资源。但锁本身也会引起抖动,甚至其他更严重的问题(包括死锁和饥饿)。

Dart 采用了另一种方式来处理这个问题。Dart 中的线程被称为 isolate,它们不共享内存,这避免了大部分锁的使用。Isolate 通过 channel 来传递信息,这就像 Erlang 里的 actor 或者 Javascript 里的 web worker。

Dart,就像 Javascript 一样,是单线程执行的,这意味着它根本不允许抢占式多任务处理。替而代之的,线程显式地让出控制(通过使用 async/await,Future 或者 Stream)。这给了开发者对于执行过程更多的掌控力。单线程帮助开发者确保重要的功能(包括动画和过渡)能够被完整执行,而不被抢占。这不仅对于用户界面开发是巨大的优势,对于其他客户端-服务端代码同样如此。

当然,如果开发者忘记让出控制(yield control),这会阻塞其他代码执行。然而,相比忘记锁,我们发现忘记让出控制的问题更容易被发现和修复(因为竞态真的很难被发现)。

内存分配和垃圾回收

另一个会引起严重抖动问题的来源是垃圾回收。事实上,这只是许多语言通过锁的机制访问共享资源(内存)的另一种特殊形式。因为在内存回收的时候,锁可能会导致整个应用停止执行。不同的是,Dart 可以几乎至始至终不用锁而进行垃圾回收。

Dart 使用先进的分代式垃圾回收和内存分配机制,这对于处理许多短生命周期的对象内存分配会非常快(尤其适合响应式的用户界面比如 Flutter 每次都会构建不可更改的视图树)。Dart 可以使用单指针碰撞(不需要锁)来分配对象。再一次,这带来了平滑的滚动和动画效果,没有任何抖动。

统一的布局

Flutter 使用 Dart 的另一个好处是不需要将布局从程序中分离,也不需要模板或者另一种布局语言比如 JSX 或 XML,更不需要单独的视觉布局工具。下面是用 Dart 写的一个简单的 Flutter 视图:

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

即便你没有用过 Dart,通过代码你也能很直观地想像出布局效果。

需要强调的是,现在 Flutter 使用 Dart 2, new 关键字变成了可选,所以布局也因此变得更简单和清晰。上面的静态布局代码可以写得更像声明式布局语言,就像这样:

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

不过,我知道你会怀疑——为什么缺乏一种专门的布局语言也能被称为一种优势呢?但这的确是种开创性的改变。某个开发者在“为什么原生应用开发者需要认真对待 Flutter” 这篇文章中写到:

在 Flutter 中,布局是用 Dart 写的,没有其他。没有 XML/模板语言,没有视觉设计工具/脚本设计工具。

我预感,当听到这里,你们中的很多人会有点退缩。这也是我最初的反应。使用一个视觉布局工具难道不是更简单吗?把所有的布局约束逻辑都写在代码里难道不会太复杂了吗?

对于上面的问题我最终得到的答案是否定的。你懂的,这简直大开眼界。

答案的第一部分就是上面已经提到的热更新。

我简直没法强调这比安卓的 Instant Run 或其他类似的方案要先进几光年。它工作得如此好,即便在大型的应用中也表现出色。并且它真的非常快。这就是 Dart 赋予你的能力。

在实践中,这让视觉编辑器变得多余。

Dart 能创建出简练而易懂的布局,然后通过 “难以置信的快” 的热更新让你即时看到结果。并且,这也包含你布局中动态的部分。我甚至一点也不怀念 XCode 相当不错的自动布局功能。

结果是,用了 Flutter(Dart) 之后,在布局上,我比之前用 Android/XCode 都更有生产力。因为不用频繁切换上下文,减少了相当可观的思维负担。因为你的大脑不再需要切换到设计模式,然后拿起鼠标点来点去,再然后开始疑惑有些东西是否需要通过程序或如何通过程序来解决。在 Flutter 中所有内容都通过编程井然有序。并且 API 的设计也相当好。这种布局方式会很快变得符合直觉,比自动布局 (auto layout)或者 XML 布局提供的构造器都要强大很多。

举个例子,下面的代码展示如何用程序在每间隔一个列表项添加一根分割线(水平线):

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

在 Flutter 中,所有的布局都在一起,无论它是静态的还是包含程序逻辑的布局。通过使用一些新的 Dart 工具,包括 Flutter Inspector 和 outline view(它充分利用了所有布局在一起) 等,使得构建复杂漂亮的布局变得更容易。

Dart 是一种专有的语言吗?

不是的,Dart (就像 Flutter) 是完全开源的,并且有一个干净的开源协议,同时也是一个 ECMA 标准。Dart 无论在 Google 内外都很受欢迎。在 Google 内部,它是成长最快的语言,被 Adwords,Flutter,Fuchsia 以及其他产品所使用;在外面,Dart 仓库有超过 100 个外部代码贡献者。

另一个更好的 Dart 开放性迹象是 Google 之外 Dart 社区的成长。举个例子,我们可以看到有非常稳定的来自第三方的 Dart 相关文章和视频的输出(包括 Flutter 和 AngularFlutter),其中一些我在这篇文章中有引用。

外部的社区参与者除了给 Dart 本身提交代码外,还积极开发 Dart 包。现在在公共的 Dart 包仓库中有超过 3000 个包,内容涵盖 Firebase,Redux,RxDart,国际化,加密,数据库,路由,数据集合等等。

Dart 程序员容易招聘吗?

如果没有很多程序员了解 Dart,招聘一个合格的 Dart 程序员是不是会很难呢?相反的是,Dart 使招聘程序员变得更为容易,因为它是一门非常容易快速学习的语言。程序员如果已经理解了一些语言比如 Java,Javascript,Kotlin,C#,或者 Swift 几乎可以直接上手 Dart 开发。在此之上,热更新能鼓励程序员开心地和 Dart 玩耍并且尝试新东西,这让学习 Dart 变得更快更愉悦。

某个程序员在题为“为什么 Flutter 会在 2018 年起飞”文章中写到:

Dart,作为开发 Flutter 应用的语言,简直易学到有些蠢。Google 有创造简单、文档完备的语言比如 Go 的经验。目前为止对我来说,Dart 让我想起了 Ruby,学习它真的非常愉悦。并且它不止于移动端开发,也适合 web 开发。

另一篇题为“为什么选择 Flutter?而不是 X 框架?或者更准确地说我为什么全心投入 Flutter 呢”文章中写到:

Flutter 使用的 Dart 语言也是 Google 创造的,老实说,我并不是强类型语言比如 C# 或 Java 的粉丝,但我不知道为什么用 Dart 写代码的方式似乎有些不一样。用 Dart 写代码我感到非常舒适。也许是它很容易学习,非常简单直白。

通过大范围的用户体验研究和测试,Dart 被有意设计成让人感觉熟悉和容易学习。举个例子,2017 上半年 Flutter 团队面向 8 位开发者做了一个用户体验研究。我们向他们简单介绍了 Flutter,然后让他们自由发挥一个小时或者创建简单的视图。所有的参与者都能够马上开始编程,即便他们之前完全没有用过 Dart。他们能够专注于写响应式的视图,而不是语言本身。Dart 就是这么神奇。

在实验最后,一个参与者(他在完成任务中表现最快)没有提及任何关于语言的内容,所以我们问他们是否意识到自己在用哪种语言。他们并不知道。语言并不重要;他们可以在几分钟内就开始用 Dart 编程。

学习一个新的系统难点不在语言本身,而是围绕语言的库,框架,工具,模式和写出好代码的最佳实践。Dart 的库和工具都异常好用且文档漂亮完备。有篇文章这样描述道,“作为一个额外的好处,他们非常关心代码库并且他们有我所见过的最好的文档”。所以轻松学习 Dart 而省下的多余时间可以用来学习其他内容。

作为一个直接的证据,Google 内部的一个大型项目想要把他们的应用移植到 iOS。他们曾想招聘一些 iOS 程序员但最终决定尝试一下 Flutter。他们观察开发者需要多长时间才能熟练使用 Flutter。他们的结果是,一个程序员可以在三周内熟练掌握 Dart 和 Flutter 开发。与之可以对比的是,他们曾经观察过如果让一个程序员熟悉安卓开发需要5周时间(更不用提他们本来还要招聘和培训 iOS 开发)。

最后,“为什么我们选择了 Flutter 并且它又是如何使我们的公司变得更好”这篇文章中提到,他们把他们的大型企业应用在三个平台(iOS、Android 和 Web)都迁移到了 Dart。他们的结论是:

更容易招聘了。我们现在只招最好的候选人,无论他来自Web、iOS 或者 Android。

现在我们有了3倍的开发能力,因为我们所有团队都统一到了同一个代码库。

知识分享变得前所未有地高效。

用上 Dart 和 Flutter 后,他们的生产力可以提升到3倍。考虑到他们之前的做法,这其实没什么可惊讶的。他们,就像其他很多公司一样,为三个平台(Web, iOS and Android)开发三个独立的应用,使用不同的语言、工具和程序员。转换到 Dart 后,他们就不再需要雇佣三类不同的程序员。并且把现存的应用迁移到 Dart 也相当容易。

他们以及其他公司发现,一旦程序员开始使用 Flutter,他们就会爱上 Dart。他们喜欢这门语言的简洁没有套路。他们喜欢这门语言的一些特性,比如级联调用,命名参数,async/await 以及 stream。除此之外,他们还喜欢 Flutter 的一些特性,比如因为 Dart 才成为可能的热更新,以及 Dart 帮助他们构建的漂亮且性能佳的应用。

Dart 2

就在这篇文章发表的时候,Dart 2 也发布了。Dart 2 致力于提高创建客户端应用的体验,包括开发速度,提升的开发者工具,类型安全等。比如,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()],
};

因为 breakfastconst,所以其他都被推断为 const

专注是秘诀

Dart 2 专注于优化客户端开发的体验。但是,Dart 仍然会是构建服务端、桌面端、嵌入式系统以及其他应用的绝佳语言。

专注是好的。几乎所有长期受欢迎的语言都从专注中获益。比如:

  • C 是用于开发操作系统和编译器的系统编程语言。它变得越来越专注于此。
  • Java 是一门设计用于嵌入式系统开发的语言。
  • Javascript 是一门用于浏览器的脚本语言(!)。
  • 甚至广受非议的 PHP 之所以成功也是因为它专注于写个人主页(Personal Home Pages),这正是它名字由来。

另一方面,有很多语言明显想尝试(但失败了)成为全能型的语言,比如 PL/1 和 Ada,以及其他等。他们最大的问题是缺乏专注,因而变成了众所周知的垃圾(kitchen sinks)。

许多使得 Dart 极擅长客户端开发的特性,同样使得它是一门适合服务端开发的语言。比如,Dart 避免抢占式多任务使得它和 Node 具有相同的服务端开发优势,并且有更好的更安全的类型。

相同的情况也适用于嵌入式系统开发。Dart 能够处理多个并发输入是关键。

最后,Dart 在客户端的成功不可避免地会引起更多在服务端使用它的兴趣,就像发生在 Javascript 和 Node 上的一样。为什么要强迫开发者在构建客户端-服务端软件的时候使用两种不同的语言呢?

结论

对于 Dart,现在是令人激动的时候。使用过 Dart 的人都喜欢它,Dart 2 中新的特性使得它更值得成为你工具库中的一员。如果你还没有使用过 Dart,我希望这篇文章提供了关于 Dart 如何新颖而不同的有价值的信息,然后希望你能尝试一下它以及 Flutter。

(欢迎转载,但请保留文章地址)

郑超的独立博客