Dart简介
目标是高效地开发多平台应用,提供灵活的运行时环境和编译工具。
理论上所有高级语言都可跨平台,关键在于语法、平台库好不好用,编译工具成不成熟。下面会详细介绍下Dart的编译和执行
Dart SDK安装
参考Dart SDK安装。Dart SDK中包含了Dart核心库、编译器、命令行工具等。Dart SDK是由Dart源码编译出来的归档文件。
- Flutter SDK内置了Dart SDK工具(Dart SDK归档文件,包括工具和核心库),位于
{flutter_sdk}/bin/cache/dart-sdk
中,不需要再单独下载。 - Flutter Engine中依赖了Dart的源码,位于
{flutter_engine}/third_party/dart
中,通过ninja构建出Dart SDK可执行程序,供Flutter SDK使用。
Dart语言
支持众多特性:类型安全(静态类型检查、dynamic运行时检查)、空安全、异步调用、流、箭头函数、getter函数等。基本语法参考Dart语言
Dart库
Dart项目文件
Dart使用pubspec.yaml
文件保存项目信息、发布信息、依赖包等。pubspec说明
类似npm的
package.json
,本地会缓存依赖包,不同项目可以共用本地缓存的依赖包
可以使用dart pub <subcommand>
命令管理项目,如add添加依赖,get获取依赖等。可以用dart pub -h
查看帮助,也可以看dart pub说明
如果用dart开发Flutter程序,使用
flutter pub <subcommand>
命令替代,flutter对dart命令进行了一层包装
新建pubspec.yaml
文件
1 | name: myapp # 项目名称 |
执行dart pub get
获取依赖,会生成几个文件。不需要提交,加到.gitignore
中
pubspec.lock
:保存项目信息.packages
:已经弃用,替换为package_config.json
文件.dart_tool/package_config.json
:将依赖包映射到系统缓存该包的路径
main.dart
中可以导入包使用,运行时会从package_config.json
中查找依赖包路径
1 | import 'package:js/js.dart' as js; |
dart compile
和dart --snapshot
可以使用--packages=<path>
选项指定.packages
文件或者package_config.json
文件,用于编译时查找依赖包路径
Dart工具
Dart SDK中提供了一些工具,使用-h
查看帮助或者参考Dart命令行工具。源码入口位于{dart_sdk}/pkg/dartdev/
中
dart
:用于创建、格式化、分析、测试、编译和运行dart代码dartaotruntime
:用于执行aot预编译过机器码dartdoc
:用于生成API文档
除了上面三个工具外,还有dart2js
、dart2native
、dartanalyzer
、dartdevc
、dartfmt
、pub
等工具。这些工具从2.10版本开始全部被封装到了dart
中,通过dart <subcommand>
的方式执行:
dart2native
,dart2aot
,dart2js
工具被dart compile
替代dartanalyzer
被dart analyze
替代dartfmt
被dart format
替代pub
被dart pub
替代
{flutter_sdk}/bin/dart
对dart-sdk
的工具做了一层包装,执行的时候会调用dart-sdk
中的工具。
由于配置Flutter SDK环境变量
{flutter_sdk}/bin
的时候没有配置dart-sdk
的环境变量,如果要使用dartaotruntime
工具,需要进入对应目录执行,或者给dart-sdk
也配置环境变量
Dart的编译和执行
Dart虚拟机
Dart虚拟机源码位于{dart_sdk}/runtime/vm
中,包含以下几个部分:
DartVM作为虚拟机为Dart高级语言提供执行环境,但这并不表示Dart一定运行在虚拟机中。Dart的运行主要有几种方式:
- 虚拟机执行:通过JIT即时编译+解释器,执行Dart源文件或者Kernel二进制文件,运行在Dart虚拟机中。对应
dart run
命令 - 目标代码执行:通过AOT预编译成目标代码,运行在预编译运行时环境(Precompiled Runtime)中。不包含编译器,因此无法动态加载Dart源码。对应
dartaotruntime
命令
- 开发阶段:运行在Dart虚拟机中,通过Dart虚拟机提供的即时编译器(JIT)执行,支持增量编译,热重载和调试。
- 发布阶段:通过Dart的AOT编译器编译成目标平台的代码,在Dart预编译运行时(Precompiled Runtime)中执行,提高启动速度和执行效率。
Dart 2之后,Dart VM不支持直接执行源代码,只接收Kernel AST序列化成的Kernel二进制文件(即.dill文件)。通过Dart的编译前端(CFE,common front-end)编译,并被其他工具所依赖使用,例如Dart VM、dart2js、Dart Dev Compiler。
Dart运行时会被打包到Self-Contained
的目标可执行程序中,同时也是Dart虚拟机的一部分,包含以下功能
- 内存管理:提供对象分配和分代垃圾回收功能。
- 运行时类型检查和强制转换
- 管理
isolates
:包括主isolate和应用自行创建的isolate
虚拟机执行
使用dart run
命令启动虚拟机执行程序,如下
新建
main.dart
文件1
2
3
4//main.dart
void main() {
print('Hello, World!');
}执行
dart main.dart
,输出”Hello, World!”
run子命令启动一个Dart虚拟机,执行未编译过的源码或者部分快照类型(JIT、Kernel快照),不支持执行aot快照。
可以省略,例如
dart main.dart
,dart main.dill
Dart编译
compile命令
dart compile
命令封装了不同场景下的编译,不需要手动执行编译前端和编译后端。源码位于{dart_sdk}/pkg/dartdev/lib/src/commands/compile.dart
,分为以下几种方式:
exe
:生成Self-Contained
可执行文件,包含生成的目标代码和一个小型的Dart运行时,可以直接运行- 编译:
dart compile exe main.dart
,生成main.exe
文件 - 运行:
./main.exe
,输出”Hello, World!”
- 编译:
aot-snapshot
:生成AOT快照文件,包含生成的目标代码,但不包含Dart运行时,需要使用dartaotruntime
执行- 编译:
dart compile aot-snapshot main.dart
,生成main.aot
文件 - 执行
dartaotruntime main.aot
,输出”Hello, World!”
- 编译:
jit-snapshot
:生成JIT快照文件,包含生成的目标代码,不同的是在训练运行期间已经加载和解析过代码,使用dart run
运行。由于在训练运行期间已经解析和编译过,Dart虚拟机不需要再进行解析和编译,因此可以更快的执行代码。(经过训练和优化,有可能比aot执行更快)- 编译:
dart compile jit-snapshot main.dart
,生成main.jit
文件,并且会执行一遍程序训练,输出”Hello, World!” - 运行:
dart run main.jit
,输出”Hello, World!”
- 编译:
kernel
:生成.dill
二进制的kernel快照文件,是一种中间代码,和平台无关,具有可移植性。包含二进制格式的Dart抽象语法树(Kernel AST)- 编译:
dart compile kernel main.dart
,生成main.dill
文件 - 执行:
dart run main.dill
,输出”Hello, World!”
- 编译:
js
:生成js文件- 编译:
dart compile js main.dart
,生成out.js
文件 - 可以使用
webdev serve
命令启动开发服务器运行js
- 编译:
exe
和aot-snapshot
存在一些限制:
- 不支持交叉编译、只能本地编译本地运行:需要在macOS、Windows、Linux主机上分别编译出三个目标程序
- 生成的可执行程序不支持签名
- 不支持
dart:mirrors
(用于动态反射)和dart:developer
(用于调试检查)库,参考Dart核心库说明
对比下编译产物文件,如下:exe > jit-snapshot > aot-snapshot > kernel > dart source code
,一般情况下执行效率刚好相反。
查看dart compile
源码,如下:
1 | //{dart_sdk}/pkg/dartdev/lib/src/commands/compile.dart |
dart –snapshot
在Flutter SDK中经常看到.snapshot
后缀的文件,如flutter_tools.snapshot
,查看Flutter命令脚本的源码中使用了dart --snapshot
命令,官网没有说明。使用dart --snapshot
查看帮助如下:
--snapshot
用于生成快照文件,--snapshot-kind
指定生成JIT快照还是kernel快照。默认生成kernel快照。
dart --snapshot
源码入口位于{dart-sdk}/runtime/bin/main.cc
CompileSnapshotCommand
查看CompileSnapshotCommand
代码,对应dart compile kernel
和dart compile jit-snapshot
命令。实际就是调用dart --snapshot-kind=$formatName
执行。如下:
1 | //{dart_sdk}/pkg/dartdev/lib/src/commands/compile.dart |
dart compile kernel/jit-snapshot
等价于dart --snapshot-kind=kernel/app-jits
。只不过是新版本Dart工具统一封装到compile中而已。
例如
dart --snapshot=main.snapshot main.dart
生成main.snapshot
,dart compile kernel main.dart
生成main.dill
。main.dill
和main.snapshot
实际上是一样的,都是Kernel快照文件,文件大小也相同。
CompileNativeCommand
查看CompileNativeCommand
代码,对应dart compile aot
和dart compile exe
命令,调用了dart2native
的generateNative
方法,
1 | //{dart_sdk}/pkg/dartdev/lib/src/commands/compile.dart |
generateNative
流程如下:
1 | //{dart-sdk}/pkg/dart2native/lib/generate.dart |
编译流程
Dart编译前端(frontend,
{dart_sdk}/pkg/front_end/lib/src/api_prototype/front_end.dart
):将Dart源码编译为Kernel二进制文件,是一种平台无关的中间代码。Dart编译前端生成的.dill文件类似于Java编译前端的.class文件,通过虚拟机执行,和平台无关。
Dart编译后端(gen_snapshot,
{dart_sdk}/runtime/bin/gen_snapshot.cc
):将Kernel二进制文件编译出目标代码- 将Kernel二进制代码生成一个控制流图(CFG,control flow graph),CFG由中间语言(IL,Intermediate Language)指令组成。
- 对IL指令进行优化
- CFG编译成机器码,每个IL指令对应多个机器指令
IL指令类似于虚拟机指令,从堆栈中获取操作数,执行操作,将结果推送到堆栈中
创建exe可执行文件:
writeAppendedExecutable
方法合并dartaotruntime
和aot目标代码
命令如下:
1 | 编译前端1 |
gen_kernel.dart.snapshot
和frontend_server.dart.snapshot
都是调用front_end
的方法(可以分析源码,或者执行命令异常查看方法调用栈),参数稍微有些差异
例如
frontend_server.dart.snapshot
支持--import-dill
参数,可以加载和链接其他dill文件。
gen_kernel.dart.snapshot
的--platform
参数用于指定sdk完整路径,用于将平台库加载到产物中。与frontend_server.dart.snapshot
的--sdk-root
和--platform
参数作用相同,--sdk-root
指定文件夹路径,--platform
指定文件名,加起来是完整路径。
- Dart平台库位于
{dart-sdk}/lib/_internal/vm_platform_strong.dill
。 - Flutter平台库中包含Dart库和Flutter框架本身,位于
{flutter_sdk}/bin/cache/artifacts/engine/common/flutter_patched_sdk/platform_strong.dill
--target
参数:使用Dart平台库时,target值需要为vm,使用Flutter平台库时,target值需要为flutter,否则会编译失败。说明如下:
1 | ./dart-sdk/bin/dart ./dart-sdk/bin/snapshots/frontend_server.dart.snapshot -h |
target相关源码位于
{dart-sdk}/pkg/vm/lib/target/
下
注:target为flutter时,无法使用dartaotruntime
执行aot文件,由于找不到对应的平台库,会报错Dart_LookupLibrary: library 'dart:_builtin' not found.
。
Kernel文件踩坑
dart compile kernel
生成的Kernel文件不能用于gen_snapshot
后端编译,会报错:Unable to use class Library:'dart:core' Class: _List@0150898 which is not loaded yet.
。但是可以使用dart run main.dill
运行。- 编译前端
--no-aot
参数生成的Kernel文件不能用于gen_snapshot
后端编译,会报错:error: Missing table selector metadata! Probably gen_kernel was run in non-AOT mode or without TFA.
。但是可以使用dart run main.dill
运行 - 编译前端
--aot
参数生成的Kernel文件可以用于gen_snapshot
后端编译,但是不能直接用dart运行,会报错:error: vm.procedure-attributes.metadata metadata is allowed in precompiled mode only
上述三者都生成了Kernel文件,但是不太一样。具体区别暂时不清楚。
Dart SDK版本一致
Dart编译前端、编译后端、以及Dart运行时的版本必须一致,否则会报错版本不匹配。例如
1 | Dart运行不同版本的编译前端报错 |
生成的dill文件和aot文件中带了版本信息,执行的时候会进行校验。
主机上有多个版本Dart SDK,例如Flutter引擎编译出来的Dart SDK和Flutter SDK中内置的Dart SDK版本不一致。此时需要分别进入对应路径下执行命令,或者配置多个环境变量。为了避免麻烦,可以切换Flutter引擎到对应的commit id,保持版本一致。
Web平台
dart支持在Web平台上执行,既不是JIT也不是AOT:生成JavaScript代码,运行在浏览器中,而不是目标平台代码
- 开发阶段使用
dartdevc
增量式编译器 - 生产环境使用
dart2js
编译器,高版本替换为dart compile js
命令
官方建议使用webdev工具,而不是直接使用dartdevc
和dart2js
工具。
webdev serve
:编译并部署到开发服务器,使用localhost:8080
访问。默认使用dartdevc
编译。添加--release
选项,替换为dart2js
编译webdev build
:默认使用dart2js
编译,添加--no-release
选项,替换为dartdevc
编译
Dart源码下载和编译
Dart SDK是Dart源码的编译产物。
源码下载和编译
安装
depot_tools
- 下载:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
- 设置环境变量:
.bash_profile
文件中添加export PATH=/your_path/depot_tools/:$PATH
- 下载:
下载Dart源码和DEPS依赖
1
2
3
4
5创建目录
mkdir dart-sdk
cd dart-sdk
通过gclient下载dart源码和DEPS依赖
fetch dart编译Dart SDK源码,生成Dart SDK归档文件
1
2
3
4cd sdk
./tools/build.py --no-goma --mode release --arch x64 create_sdk
也可以指定多个build_targets编译
./tools/build.py --mode=release --arch=x64 create_sdk runtime gen_snapshot frontend_server.dart.snapshot
build_targets
参数没有找到具体说明,不过可以参考tools/bots/test_matrix.json
文件中的builder_configurations
踩坑:FlutterEngine中下载的Dart SDK(
{flutter_engine}/src/third_party/dart
)不包含编译需要的依赖项目,无法直接用于编译,因此需要使用gclient
重新下载Dart源码。应该也可以手动创建
.gclient
文件,执行gclient sync
下载依赖
交叉编译
编译生成Dart虚拟机
交叉编译Android平台的Dart VM,可以在Android平台执行Dart应用程序
- 下载Android需要的依赖,如NDK,SDK等
- 修改dart-sdk目录下的
.gclient
文件:最后一行添加target_os = ['android']
- 下载依赖项目:执行
gclient sync
- 修改dart-sdk目录下的
- 交叉编译Arm64的Android平台的Dart VM:
./tools/build.py --no-goma --arch=arm64 --mode=release --os=android runtime
- 使用adb将编译后的Dart VM工具push到Android平台中:
adb push ./xcodebuild/ReleaseAndroidARM64/dart /data/local/tmp/dart
- 将Dart程序push到Android平台中:
adb push main.dart /data/local/tmp/
- 运行Dart程序:
adb shell /data/local/tmp/dart /data/local/tmp/main.dart
踩坑:这里无法使用adb shell进入Android终端执行,会报错,缺少工具和库。
编译生成Dart SDK
可以将编译出来的整个SDK归档文件push到Android平台中并设置环境变量,此时可以像在主机上一样使用Dart SDK,编译和执行Dart应用。如下
1 | 交叉编译Android平台的Dart SDK |
结语
了解Dart编译方式和不同产物,以及执行原理,主要有以下场景
- 为动态化,分包等提供一些思路
- 使用交叉编译在嵌入式平台中执行Dart或Flutter程序
- 定制虚拟机、编译器等。
参考资料: