Google Dart精粹:应用构建,快照和隔离体
英文原文:The Essence of Google Dart: Building Applications, Snapshots, Isolates
(作者:Werner Schuster 译者:曹如进)
既然程序设计语言已经有了上千种,为什么 Google 还要推出 Google Dart 呢?它又会增加什么样的特性呢?答案很简单:Google Dart 团队想要开发一门适合现代应用程序开发的语言,它既能在服务端工作,也能在(移动)客户端工作。
Dart 的一些特性解决了像 Java 或 Javascript 语言长久以来存在的问题。它的快照功能类似于 Smalltalk 的映像(image),使用快照不仅可以带来(接近)即时的应用程序启动速度,而且还没有映像遗留的一些问题。隔离体特性可以确保代码在无共享状态的单线程内执行,它的消息传递并发类似于 Javascript 中的 WebWorker 和 Erlang 中的进程。Dart 的这些语言特性使得我们可以开发可扩展的和模块化的应用。Dart 代码既可以被 DartC 编译器编译成普通的 Javascript,也可以在 Dart 虚拟机(Dart VM)中执行。
下面将介绍应用程序开发中使用 Dart 的乐趣所在,并重点介绍 Dart 虚拟机以及一些值得关注的语言特性。
Dart 是一门应用程序语言:快照和初始化
应用程序的启动时间真得有那么重要吗?用户在每一天又会重启集成开发环境(IDE)或者字处理器软件多少次呢?随着内存有限的移动设备的兴起,应用程序启动变得非常频繁;移动设备操作系统中的内存不足(OOM)杀手进程显得战意十足,因为它会毫不犹豫地杀死处于中止状态的应用程序。iOS 中的多任务模型及其赫赫有名的物理 Home 按钮,也同样在缩短移动应用程序的平均寿命。在 iOS4 之前,按下 Home 按钮总会杀死正在运行的应用程序;而在 iOS4 中,虽然情况变得稍显复杂,但是应用程序仍然必须准备随时“死”去,“死”在用户手里或是 OOM 进程下。
然而这样的行为在移动操作系统上并不会长久。“突然终止”和“自动终止”是 OS X 最近版本中才推出的应用程序属性,它们被用来声明该应用程序可以处理在任何时候被杀死(例如,当可用内存非常低的时候)并重新启动,且整个过程对用户透明。
启动缓慢从 Java 1.0 开始就是 Java GUI 应用程序的头疼问题。启动一个大型的 Java 应用程序需要大量的工作:需要读取、解析、加载和链接数以千计的类;在 Java 1.6 之前,这个过程包含了为方法生成堆栈图以验证字节码。然而一旦类被加载,仍然需要初始化(静态初始化等)。
对于当下的 Java GUI 应用程序来说,仅仅显示初始 GUI 就需要大量的工作。虽然这个问题一直影响着开发人员和用户,但是 Java 6 中引入的闪屏 API (SplashScreen API)标志着该问题尚未得到解决。
快照 vs Smalltalk 映像
为了解决启动缓慢的问题,Dart 使用了堆快照功能,它类似于 Smalltalk 的映像系统。所谓堆快照,其实就是遍历应用程序堆并将所有的对象写入文件。目前 Dart 的发布版本上有一个工具可以帮助激活 Dart 虚拟机、加载应用程序代码并在调用 main 之前采集一份堆快照。随后 Dart 虚拟机可以使用这个快照文件快速地加载应用程序。
快照还可以用于对 Dart 虚拟机中的隔离体之间发送的对象图进行序列化。
在 Dart 的最初技术预览版本中,似乎并没有什么 API 可以用作初始化快照,尽管这很有必要。
快照的技术细节
Dart 团队在快照格式上投入了很多努力。首先,快照需要能够移动到不同的机器上工作,不管目标机器是 32 位,64位还是其他。同时,快照还需要能够被快速地被读进内存并尽量减少的额外工作,如指针修正。
更多详细信息,请参考 runtime/vm/snapshot.cc 和 runtime/vm/snapshot_test.cc 以了解快照系统的使用方法:导出完整快照,读取快照,从快照启动隔离体等等。
快照 vs Smalltalk 映像
Smalltalk 映像特性并没有普及;Gilad Bracha 曾写过一篇文章讨论实践中 Smalltalk 映像的问题。Smalltalk 开发通常发生在某个映像之中,其中的无用代码会被剥离且映像会被冻结以进行部署。Dart 的快照与之不同,因为它们不仅是可选的,而且需要通过启动应用程序并采集快照才可以生成。由于 Dart 缺少动态代码评估以及其他的代码加载特性,因此剥离过程变得更加彻底。
Dart 的快照功能目前在 DartC 编译后的 Javascript 的代码中不受支持。
快照也被用作隔离体间的消息传递;消息发送前在一端为对象使用SnapshotWriter
进行序列化而后从另一端进行反序列化并读取。
不管怎样,快照功能就在 Dart 虚拟机和工具中。同 Dart 的许多其他特性一样,快照该怎么用要由社区说的算。
最后我要说的是,Google 的 V8 Javascript 引擎中也有了快照的功能,它通过从快照中加载 Javascript 标准库来改善启动速度。
初始化
即使没有快照,Dart 也被设计成尽量避免在启动时进行初始化。Dart 中的类是声明性的,也就是说创建它们并不需要执行什么代码。库可以定义final
的顶级元素,即类之外的函数和变量,但需保证它们为编译期常量(查看语言规范中的 10.1 节)。
与 Java 中的静态构造器,或是那些在启动时依赖各种元编程方法生成数据结构、对象系统等语言相比,Dart 最优化了应用程序的启动时间。
Dart 目前并没有反射机制,但是看起来基于 Mirrors (PDF)的实现会在不久的将来出现在语言中,通过它也许能够使用 API 构建代码并将其加载入新的隔离体中,从而将元编程带进 Dart。
并发单元,安全和应用:隔离体
并发
隔离体是 Dart 中的基本并发单元。每个隔离体是单线程的。为了能够在后台执行工作或是利用多核或多处理器,需要启动新的隔离体。
Google V8 最近同样增加了隔离体特性,隔离体的加入为 V8 的内置程序提供了方便,通过在同一个系统进程中启动多个隔离体可以实现成本更低的 Web Worker;这个特性暂未对 Javascript 开放。
这种为并发而存在的多个独立隔离体模型类似于 Javascript 和 Erlang。Node.js 也需要使用进程来利用多处理器或多核;许多管理 Node.js 进程的解决方案已经横空出世。
其他的单线程或绿色线程(Green Thread)语言都有类似的进程管理方案。Ruby 中的 Phusion Passenger 就是一个例子,它试图解决在多个进程内加载相同代码的开销问题:Phusion Passenger 首先会加载 Rails 应用程序,并使用系统调用fork
快速创建具有相同程序内容的多个进程,从而避免重复多次地解析和初始化相同的应用程序。Dart 的快照特性用另一种方式解决了这个问题。
可靠性
Dart 的首个技术预览版本为每个隔离体使用一个线程,但是与此同时其他模型也正在考虑之中,如将多个隔离体多路传输到一个线程中或将隔离体运行在不同的系统进程中,这些做法同样可以让隔离体运行在不同的机器上。
将应用程序分割成多个独立的进程(或隔离体)有助于提供可靠性:如果某个隔离体崩溃,其他隔离体将不受影响,同时隔离体的干净重启也成为可能。Erlang 的监控树(supervision trees)在这个模型中十分受用,体现在它能够监控进程群的存活和死亡状态并写入自定义策略用来处理进程死亡。
这篇 Akka 和 Erjang 创始人访谈 文章中对 Erlang 模型的优势给出了一个很好的概述。
安全性
不受信任的代码可以运行在自己的隔离体中。所有与之的通信必须通过消息传递发生,附加的能力-风格(capability-style)机制可以限制隔离体中谁可以与哪个端口进行会话。隔离体必须给定一个信息发送的端口,否则它什么也干不了。
内存划分
将应用程序分割成隔离体还有一个好处:每个隔离体的堆都是独立的;其中所有的对象都完全的属于它自身,这不同于共享内存环境下的对象。其实关键的好处在于:如果为某个任务启动了隔离体,那么在任务结束时整个隔离体可以一次性被回收而不需要运行垃圾回收器。
此外,如果一个应用程序被分割成多个隔离体,那么就意味着该应用程序使用的内存被分割成更小的堆,即小于应用程序使用的内存总量。每个堆由它自己的垃圾回收器所管辖,这带来的效果就是一个隔离体中的完整垃圾回收器只能工作在该隔离体之中而不会影响其他隔离体。 对那些易受垃圾回收器中断(GC pauses)影响的 GUI 应用程序和服务器应用程序,一则好消息是:那些让垃圾回收器一直忙个不停的糟糕隔离体将不会影响到易受时间影响的组件。因此,让每个隔离体拥有各自的堆可以提高模块化程度:每个隔离体控制自己的垃圾回收器中断行为,且不会受到其他组件的影响。
虽然 Java 和 .NET 中的垃圾回收器已经改善了许多,然而垃圾回收器中断对于 GUI 应用程序以及时间敏感的服务器应用程序仍然是一个重要的问题。虽然类似 Azul 垃圾回收器的解决方案已经可以使中断变得可控甚至几乎消失,但是它们要么需要特殊的硬件,要么需要访问系统基础设施中的底层,如基于 x86 架构的 Zing,实时垃圾回收器确实存在,尽管如此,它们还是牺牲了执行速度来换取可预测的中断。
将内存分割成单独的堆意味着垃圾回收器的实现可以变得更加简单且依旧足够得快。当然,这些都取决于开发人员——想要受益于这些特性,一个应用程序就必须分割成多个隔离体。
依赖注入不再必要:Dart 中的接口和工厂
常常听说应当“面向接口编程”,然而这么做在实践中显得有点困难,因为在调用时用户不得不在new
调用后跟上实际的类名。在 Java 世界里,这个问题导致了依赖注入(Dependency Injection, DI)框架的产生。采用依赖注入框架,就意味着首先要在项目中注入特定的 DI 框架依赖。
那么依赖注入解决了什么问题?在特定类上调用 new 不仅需要硬编码类,还会给测试以及代码灵活性带来问题。毕竟,如果所有的代码都基于某个接口编写的话,那么具体怎么实现将没有关系,且用户应当为使用案例选择正确的实现方式。
Dart 目前的版本中也有一种依赖注入的解决方案,使用它可以让许多选项需要选择的情况变得不再必要。Dart 通过在语言之中将接口链接至代码来实例化对象。所有这一切需要的灵活性都隐藏在了工厂(Factory)之中,无论是决定实例化某个类,还是分配某个新的对象,抑或只是返回一个缓存对象。
接口通过名称来引用某个工厂,而工厂可以用库的方式进行提供;工厂的不同实现可以存在于它们自己的库当中,且由开发人员决定包含最好的实现。
语言
Google Dart 是一门新型语言,但它设计成让多数开发人员看起来很熟悉。Dart 语言类似花括号语言,它提供面向对象编程(OOP)并重点关注接口。Dart 的面向对象编程系统有着类的概念,这与最近一些其他的语言不大一样,如 Clojure(它使用协议(protocols)和类型完成面向对象编程)或者 Google Go 语言(Go 有接口,但是没有类)。 语言内置面向对象编程特性的好处在于可以像 Javascript 一样,拥有一套新的面向对象系统和范式约定的库。
详细信息请参阅官方的 Dart 语言规范;如果想要快速了解下的话,可以查看 Dart 网站上的‘Dart 惯用法’(Idiomatic Dart)文章。
模块化
Dart 中的命名空间采用了库机制,它不同于 Java 中仅能使用类名来定义方法或变量命名空间。很重要的一点是:Dart 中的库除了包含类之外,还可以包含顶级元素,即类之外的变量和函数。
print
函数正是一个例子,因为它是无类的顶级函数。库系统也为名字冲突提供了一种解决方案:库A可以导入另一个库B,为了避免A和B间的命名冲突,所有从库B中导入的名字都会被加上前缀,也就是说,使用#import ("foo.dart", "foo")
导入库后,所有其中的可用元素都会拥有前缀"foo."。
可选类型(Optional Typing)
“可选类型”关键在于“可选”。开发人员可以为代码加上类型标注,但这些标注又对代码行为根本没有影响。事实上,Dart 中还可以指定一个无意义类型——而同时代码仍然能够正常运行。
在代码中拥有类型可以让各种类型检查器各司其职。Dart 附带的编辑器中拥有一种类型检查器,它能够高亮类型错误并将其当做警告。Dart 中还有一种检查模式,在该模式中类型标注可用来检查代码,任何违规都会被报告成警告或错误。
实际上可选类型标注在代码中会具有类型信息,这些信息对编制文档会有帮助;使用可选类型不再需要更多的文档来解释某个参数必须实现一个特定的方法才可以接受。接口的存在(即带有方法签名的方法名称集合,以及可选类型标注)可以帮助文档化 API。
关键在于,语言总是动态的而且参数也可以被指定为动态的,也就是Dynamic
类型。
运行时可扩展性和可变性——暂缺
让我们看看这些:没有猴子补丁(Monekypatching),没有eval
,目前没有反射。虽然基于 Mirror 的系统正在准备阶段(详情请见介绍 Mirrorpaper 的论文),但是计划里似乎限制了在新隔离体中加入新的代码,因此这些还不在当前运作的进程当中。
noSuchMethod
Dart 的noSuchMethod
特性,类似于 Ruby 的method_missing
、Smalltalk 的 DNU 或其他一些语言中类似的特性。未来版本的 Javascript 也应当以动态代理(Dynamic Proxies)的形式提供类似的功能,这个功能正在缓慢的进入到 Javascript 虚拟机中(如 V8)。
闭合类(Closed Classes),没有 Eval
像 Ruby 这样的语言甚至可以允许在运行时对类进行修改,这些类被称为开放类(open class)。然而不具备这种特性会对性能有所帮助:所有的成员在编译期可见,这样可以对代码进行分析以移除没有被引用过的部分。查看下面的‘批评’段落可以了解 Dart 中该特性的当前状态以及其他语言中当前已有的解决方案。
未来的语言特性
一种 async/await 风格的扩展正在被考虑用作帮助编写I/O代码。Dart 中的多数I/O API 都是异步的,因此那些让这项工作变得更加简单的事情都是受欢迎的。不添加像协程(Coroutine)、纤程(Fiber)及其变种等特性的原因是为了避免增加同步的功能。 一旦协程存在于系统中,就有可能要安排和交错它的执行,此外,想要编写正确的代码就有必要同步共享资源。因此,单线程会是重点;并发会则在隔离体中完成:显式会话、无共享和隔离体等都会参与其中。
批评
没什么能比一门新的编程语言更能惹恼开发人员了。下面让我们快速地看下一些常见的批评。
DartC 将 Dart 代码编译成大量的 Javascript 文件
这个链接展示了一个 Dart 的“Hello Word”应用程序被编译成了上千行的 Javascript 代码。对此一个简单的方案是:增加像 tree shaking 类似的优化,也就是删除不被使用的函数。目前它已经在 Google Dart 和 DartC 团队的待办事项列表中。
Dart 的某些特质使得这些优化变为可能,尤其是封闭类,这意味着所有的函数在编译期均为已知。缺少eval
就意味着编译器知道编译期间哪些函数被使用,更重要的是:知道哪些函数不被使用,从而可以把那些不被使用的函数安全地从输出中移除。
使用 Google Closure 工具的用户可能知道其中的高级编译功能。Closure 为 Javascript 带来了一套类系统并允许使用信息标注类。在高级模式中,Closure 编译器假定开发人员遵循了特定的规则,基于这个假定,如果某个函数在代码中没有被显式的引用的话,就会被丢掉。显然,如果程序员违反了这些规则,如使用eval
等其他功能,那么代码将不能工作。
Google Dart 并不需要依赖于程序员遵守某些规则,语言本身的限制为编译器提供了必要的保障。
另外一个使用 Google Closure 高级编译功能的例子是 ClojureScript 语言(这是一个带'j'的 Clojure)。ClojureScript 注定也是一门应用程序语言,它没有 eval 以及其他的动态代码加载特性。它通过使用 Google Closure 的高级编译工具编译成 Javascript 代码,以消除未被使用的库函数。
为什么不使用静态类型对运行时进行优化?
为什么类型是可选的?它既然存在,为什么又不用来提高生成的代码?诚然,知道类型为int
必然对优化生成的代码有帮助。
事实证明,Dart 背后的团队知道这些想法,因为他们过去做出过一到两个的虚拟机,Google V8 和 Oracle 的 Hotspot 就是两个例子。
出于若干原因,在 Dart 中使用静态类型信息对于运行时代码并不会有帮助。原因之一在于:开发人员指定的类型对语义毫无影响,事实上,他们 可能完全不正确。如果是这样的话,虽然类型检查器会发出警告,但是程序还是运行得很好。此外,由于给定的类型可能是无意义的,因此虚拟机并不能使用它们进行优化,因为它们并不可靠。也就是说,int
相关的代码可能仅仅是因为开发人员错误地进行了指定,如果实际运行时中的对象的真正类型是 String 的话。
虽然静态类型系统对工具和文档会有帮助,但是它与执行的代码并不相关。
静态类型没法帮助生成优化的代码还有一个原因:Dart 是基于接口的。如int
中的操作符实际上是方法调用——接口上的方法调用。Dart 不是在开玩笑,int
事实上是一个接口而非一个类。
调用接口方法就意味着需要基于实际的对象及它的类信息在运行时进行加载。类似动态调用站点(CallSite)的内联缓存(多态)的概念可以帮助消除方法查找的开销。StrongTalk 和它的直接派生 HotSpot 根据优化后的反馈,以找出实际执行的代码并生成优化后的代码。V8最近同样以 Crankshaft 的方式加入了这些优化。
我最喜欢的语言特性在那里?
Google 发布的 Dart 还处在一个相当早期的阶段。用户很容易被语言规范、集成开发环境、虚拟机、DartC 等搞混。Dart 团队给出的明确消息是:现在可以尝试一下 Dart,并提供一些反馈。Dart 的许多功能已经在计划当中,但是有的尚未完成或者有的尚未开始;反射和混合类型已经提到会被当做潜在功能加入未来特性中。
如果某个特性不在 Dart 的存储库或语言规范中,现在可以提供一些反馈意见并给出针对语言和运行时环境的修复或改动的建议。
结束语
Dart 的许多工作已经完成:语言规范、Dart 虚拟机、能够将 Dart 代码编译成 Javascript 的 DartC 编译器、基于 SWT 和 Eclipse 绑定包的编辑器等等。
然而,最初发布的 Dart 只是一个技术预览,语言、API 和工具包相关的工作还在进行中。现在可以给 Dart 团队一些反馈,而且有机会能够影响到该语言。语言将会发生改变,包括本文中提及的一些推荐和计划的改动。
有些人已经开始尝试 Dart,例如 Dart 的 Java 迁移版本 JDart 项目已经开始了,它大量使用了 Java 7 中的invokedynamic
功能。
在本文中,我们重点关注了语言以及 Dart 虚拟机特性。使用 DartC 可以将 Dart 代码编译成普通的 Javascript 文件;简介部分展示的一些样例,包括那个能够运行在 iPad 上的例子,实际上是 Dart 应用被编译成 Javascript 之后运行在标准浏览器中。
虽然 Google Dart 最初的开发是秘密完成的,但是整个项目、源码、工具及签单系统等等现在都是对外开放的。至于 Dart 能够用在何处仍然有待观察。正如前面所述,Dart 虚拟机提供的功能使得 Dart 对客户端开发人员以及服务端开发人员都颇具吸引力。
链接:
- Dart 网站
- Google Dart 在 Google 中的代码托管
- Google Dart 源码获取指南。请注意:有些朋友抱怨说需要输入 Google 密码才能访问需要的工具和源码。正如链接页面上所说的,这仅会发生在使用
https
访问源的时候;代码是可以被匿名下载的。 - Try Dart 可以在浏览器中编写和运行 Dart 代码。
- Google Dart 邮件列表
关于作者
Werner Schuster (murphee) 时而编写软件,时而撰写软件相关的文章。
查看英文原文:The Essence of Google Dart: Building Applications, Snapshots, Isolates