Dart编译命令知多少

673 阅读8分钟

当我们写Dart代码时,我们希望它能够在不同的平台上运行,例如Web、移动端、桌面端等。而Dart编译产物就是将我们的Dart代码编译成可执行的程序或库,让我们的代码能够在不同的平台上运行。

熟悉Dart的都知道,Dart编译器支持两种编译方式:JIT和AOT。JIT编译器可以在程序运行时对代码进行优化,提高程序的执行效率。而AOT编译器则将Dart代码编译成本地机器码,生成的文件可以直接在目标平台上运行,无需再次编译。

Dart提供了很多编译命令,我们来一一解析。

如何分析Dart编译命令

我们可以下载Dart SDK源码对其进行编译,通过源码来分析编译具体的实现方式及编译过程,官方文档。Dart SDK下载流程如下

  1. 找到对应版本的SDK 找到本地Flutter对应的Dart SDK的commitId,其位于${FLUTTER_ROOT}/bin/cache/dart-sdk/revision

  2. 下载源码 Dart跟Flutter编译流程类似,需要下载gclient,参考我之前的文章Flutter引擎编译与调试,在本地创建dart文件夹,然后创建.gclient文件,如果上一步找到的commidId是18df0aae67183931b1b6d5d12cc04d89f7137919,那么填入的内容如下

solutions = [
  {
    "name": "sdk",
    "url": "https://dart.googlesource.com/sdk.git@18df0aae67183931b1b6d5d12cc04d89f7137919",
    "deps_file": "DEPS",
    "managed": False,
    "custom_deps": {},
  },
]

然后执行gclient sync进行下载。Dart命令的源码就下载下来了,入口函数在sdk/runtime/bin/main.cc中,Dart相关的代码在sdk/pkg中。

为了更好分析,我新建了案例工程并在bin/main.dart中写一段简单的dart代码用于测试

void main() {
  print('hello world');
}

Kernel Snapshot

kernel是Dart编译的一个中间表示形式,它是一个二进制形式,同时也可以转换为文本,也可作为编译后端的输入。我们可以通过下面命令来进行kernel编译。

dart compile kernel bin/main.dart -o build/main.dill

上面代码等同于dart --snapshot-kind=kernel --snapshot=build/main.dill bin/main.dart,编译完成后,会生成bin/main.dill文件,这个就是编译的中间产物,我们可以用dart命令来运行程序

dart build/main.dill

如果我们想看看编译的产物,可以通过dart提供的工具来将二进制转成文本。

{DART_SDK_DIR}/pkg/vm/tool/dump_kernel build/main.dill build/main.dill.txt

会生成的AST如下

main = main::main;
library from "file:///Users/xzp/Documents/myCode/dart/dart_demo/bin/main.dart" as main {
  static method main() → void {
    core::print("hello world");
  }
}

dart compile kernel 还支持传入参数

  • packages: 传入项目依赖文件,默认指向.dart_tool/package_config.json(dart pub get会自动生成),如果没有这个文件且项目还依赖了其它三方库,将会编译失败。
  • define: 传入到VM的变量,可以简写成-Da=1,b=2形式

实际上dart compile kernel的编译入口在dart代码的sdk/pkg/vm/bin/kernel_service.dart的main中,有兴趣的可以去看看源码。

JIT

dart compile jit-snapshot bin/main.dart -o build/main.jit

等同于dart --snapshot=build/main.jit --snapshot-kind=app-jit bin/main.dart

然后可以用dart命令来运行

dart build/main.jit

实际编译入口仍在sdk/pkg/vm/bin/kernel_service.dart

AOT

dart compile aot-snapshot bin/main.dart -o build/main.aot

上面命令等同于dart --snapshot=build/main.aot --snapshot-kind=app-aot bin/main.dart,这个命令用于生成的AOT,它其实中间包含了两个过程,它先会生成kernel放到本地缓存中,然后再调用gen_snapshot工具生成AOT。

所以gen_snapshot工具才是生成AOT的主角,下面执行一个简单例子

${DART_SDK}/bin/utils/gen_snapshot --snapshot_kind=app-aot-elf --elf=build/main.aot --verbose build/aot_kernerl.dill

注意,上面命令执行成功的条件是使用frontend_server.dart.snapshot或者gen_kernel.dart.snapshot工具并添加aot参数编译的Kernel Snapshot

AOT的产物需要使用Dart提供的dartaotruntime来运行。

dartaotruntime build/main.aot

实际编译入口在sdk/pkg/vm/bin/gen_kernel.dart

exe

在一些情况下,我们还可以将Dart代码编译成本地的可执行文件,下面是编译命令

dart compile exe bin/main.dart -o build/main.exe

然后执行终端就可以运行该文件

./build/main.exe

exe编译流程跟AOT编译前面都是一样的,只是最后会将编译的AOT产物和dartaotruntime进行整合,形成一个独立的可执行文件。

实际编译入口仍在sdk/pkg/vm/bin/gen_kernel.dart

Flutter在编译不同的目标平台的流程都有区别,但是核心流程都几乎一致,我们先看看其中比较重要的编译流程

frontend_server.dart.snapshot

对于Flutter的编译,Dart专门提供了frontend_server来编译Flutter的Kernel Snapshot,它针对Flutter进行了Kernel to Kernel的转化。在编译桌面端常用的flutter build bundle过程中,就有使用frontend_server。如编译Debug版本,会使用以下命令

${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/dart 
--disable-dart-dev 
${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/snapshots/frontend_server.dart.snapshot 
--sdk-root ${FLUTTER_ROOT}/bin/cache/artifacts/engine/common/flutter_patched_sdk/ 
--target=flutter 
--no-print-incremental-dependencies 
-Ddart.vm.profile=false 
-Ddart.vm.product=false 
--enable-asserts 
--track-widget-creation 
--no-link-platform 
--packages .dart_tool/package_config.json 
--output-dill build/app.dill 
--depfile build/kernel_snapshot.d 
lib/main.dart

这个命令会从lib/main.dart入口开始,对整个Flutter的dart代码进行查找并编译,同时也会将dart的一些自带的库如dart:ui写入到AST中,常用参数解读如下

  • sdk-root: sdk路径,编译时会使用其下的platform_strong.dill,它是一个包含依赖库如dart:async的kernel,产物会将其和dart代码合并
  • target: 编译目标,提供的可选参数有flutter、vm、flutter_runner、dart_runner、dartdevc
  • print-incremental-dependencies: 默认为ture,表示在编译时打印从源码中添加的源码列表
  • Ddart.vm.product: D开头的是向Flutter中写入全局变量,dart.vm.product会影响Flutter中kReleaseMode的值
  • enable-asserts: 默认false,是否启用断言,Flutter会在debug中启用断言,在release中不启用
  • track-widget-creation: 默认false,运行kernel transformer来跟踪widget的位置,是为了在debug模式下给inspector找到widget的位置使用的
  • link-platform: 默认true,在产物中链接其它dill文件,可以配合platform参数使用
  • platform: 传入依赖的kernel文件,默认是platform_strong.dill
  • packages: dart或flutter在pub get时会生成的用于存储依赖路径的文件地址,,目前(Flutter3.0.2版本)传入.pacakges或者.dart_tools/package_config.json两个文件都兼容,后续版本将不会再生成.packages文件
  • output-dill: 生成的文件存储路径
  • depfile: 用于指定生成的输出文件的依赖关系文件
  • split-output-by-packages: 默认false,可以将产物按库的方式分组并生成不同的kernel文件
  • aot: 默认false,如果产物最终编译为AOT产物,需要带上aot标志
  • tfa: 默认false,在AOT模式中启用全局类型流分析和相关转换
  • rta: 默认true,在AOT模式下使用快速类型分析来加快编译速度
  • tree-shake-write-only-fields: 默认true,将仅定义但未使用的变量从产物中删除
  • -DDFE_VERBOSE=true:可以输出更加详细日志

如果我们需要编译AOT产物,也需要先编译kernel,需要在参数中加入--aot,同时使用gen_snapshot命令进行AOT编译,下面一起来看看gen_snapshot

gen_snapshot

gen_snapshot工具是用于将kernel产物转换为AOT产物的工具,我们在不同的系统平台上编译不同的目标平台所使用gen_snapshot工具都不一样,比如我在MacOS平台编译iOS软件,所使用的是${FLUTTER_ROOT}/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64,尽管使用的工具是不一样的,但编译参数都基本相同。以MacOS中编译iOS为例,它的编译命令是:

/Users/xzp/application/flutter/versions/3.0.2/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=.dart_tool/flutter_build/arm64/snapshot_assembly.S --strip build/app.dill

snapshot_kind

snapshot_kind用于指定生成的快照文件类型。从命令的-h中可以看到提供的可选参数有coreapp-aot-assemblyapp-aot-elf,还有一些隐藏的可选参数core-jitappapp-jitvm-aot-assembly,我重点分析前三个

  1. core

core 表示生成一个Core Library快照文件。Core LibraryDart SDK的一部分,它包含了Dart语言的核心库和运行时支持。生成的Core Library快照文件包含了这些核心库的编译代码,以供在运行时加载和执行。 在编译Dart SDK时,会用到该命令

gen_snapshot --sound-null-safety --deterministic --snapshot_kind=core --vm_snapshot_data=gen/runtime/bin/vm_snapshot_data.bin --vm_snapshot_instructions=gen/runtime/bin/vm_snapshot_instructions.bin --isolate_snapshot_data=gen/runtime/bin/isolate_snapshot_data.bin --isolate_snapshot_instructions=gen/runtime/bin/isolate_snapshot_instructions.bin vm_platform_strong_stripped.dill

它有两个参数是必传的,vm_snapshot_dataisolate_snapshot_data,表示生成的vm_snapshot_dataisolate_snapshot_data的路径。在Flutter中,Dart代码可以在两种不同的执行环境中运行:隔离区和虚拟机。隔离区适用于并发执行代码,而虚拟机适用于在单个执行上下文中执行代码。vm_snapshot_data文件包含了序列化的Dart虚拟机快照数据。它包含了虚拟机运行所需的状态、代码和资源,以便在运行时进行加载和执行。通过加载这个快照数据,Flutter 应用程序可以在虚拟机中运行Dart代码,实现单线程的执行模式。而isolate_snapshot_data文件包含了序列化的Dart隔离区快照数据。它包含了隔离区运行所需的状态、代码和资源,以便在运行时加载和执行。通过加载这个快照数据,Flutter应用程序可以创建和管理多个隔离区,从而支持并发执行代码的能力。在SDK编译时就生成了这个文件,在Flutter应用程序编译Debug模式时,会将这两个文件复制到编译结果中以加快应用程序的启动。

  1. app-aot-assembly

使用app-aot-assembly选项生成的AOT快照文件是以汇编代码的形式表示的。这种快照文件包含了Dart代码和依赖项的汇编表示,可以直接在目标平台上运行。这种快照文件通常具有较小的体积,但在运行时需要进行汇编代码的解析和执行。通常我们在使用flutter build命令编译MacOS、iOS应用时都是使用的这种方式。

  1. app-aot-elf

使用app-aot-elf选项生成的AOT快照文件是以ELF(Executable and Linkable Format)格式表示的。这种快照文件包含了Dart代码和依赖项的机器码表示,可以直接在目标平台上运行。这种快照文件通常具有较大的体积,但在运行时无需进行额外的解析和执行步骤。flutter build命令编译AndoridWindowsLinux工程会使用这种编译模式。

其它参数介绍:

  • deterministic: 用于生成确定性的输出文件。默认情况下,编译的输出文件中可能会包含时间戳或其它非确定性的标识符,这些非确定性元信息可能会导致每次生成的输出文件都有微小的差异。传入deterministic可以消除这些非确定性的元数据,保证输入不变的情况下输出结果都保持一致。
  • strip: 第一步生成的kernel产物地址,前面说了,gen_snapshot需要根据kernel AST进行二次编译。
  • obfuscate: 是否混淆
  • save-debugging-info: 保存生成的AOT的调试信息如位置、变量名、函数名等。
  • save-obfuscation-map: 在生成的AOT快照中保存混淆映射。混淆映射是一个文件,记录了混淆前后的代码标识符(如类名、方法名、变量名)之间的对应关系。一般用于开发测试使用,会增加AOT包大小。

const_finder.dart.snapshot

const_finder用于分析应用程序的常量表达式及其结果。在Flutter中,常量表达式是指在编译时可以计算出结果的表达式,例如数字计算、字符串拼接、布尔运算等。常量表达式的使用可以提高应用程序的性能和内存效率。const_finder.dart.snapshot文件中包含了用于在运行时查找并优化常量表达式的代码和数据。它可以帮助Flutter框架在应用程序运行时分析和优化常量表达式,以减少运行时的开销和资源占用。下面来举个例子:

创建一个叫dart_demo的纯Dart项目,在lib中添加card.dart

class Card {
  final String name;
  final double price;
  const Card(this.name, this.price);
  @override
  String toString() {
    return 'cardName: $name price: $price';
  }
}

在lib中再添加main.dart

import 'card.dart';

void main() {
  Card bmw = const Card('BMW', 30.3);
  print(bmw);
}

然后我们对其进行kernel AST编译

${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/dart  --disable-dart-dev  ${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/snapshots/frontend_server.dart.snapshot  --sdk-root ${FLUTTER_ROOT}/bin/cache/artifacts/engine/common/flutter_patched_sdk/  --target=flutter  --no-print-incremental-dependencies  -Ddart.vm.profile=false  -Ddart.vm.product=true  --enable-asserts --aot --tfa --track-widget-creation  --packages .dart_tool/package_config.json  --output-dill build/app.dill  --depfile build/kernel_snapshot.d  --filesystem-scheme org-dartlang-root  pacakge:dart_demo/main.dart 

然后使用const_finder.dart.snapshot

${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/dart --disable-dart-dev  ${FLUTTER_ROOT}/bin/cache/artifacts/engine/darwin-x64/const_finder.dart.snapshot --kernel-file build/app.dill --class-library-uri package:dart_demo/card.dart --class-name Card

将会输出如下:

{"constantInstances":[{"name":"BMW","price":30.3}],"nonConstantLocations":[]}

const_finder在静态分析代码并进行优化时非常有用,这在Flutter编译优化MaterialIcon中有使用,因为我们项目中一般使用极少MaterialIcon,但是完整的MaterialIcon有1.6MB,所以Flutter编译时会对IconData进行查找拿到项目中使用图标的codePoint,使用其对原来的图标文件进行优化,让最终生成的MaterialIcons-Regular.otf仅包含项目中使用的图标,缩小了最终包大小。

其它快照文件介绍

Dart还提供了其它编译服务相关的Dart快照(位于${FLUTTER_ROOT}/bin/cache/dart-sdk/bin/snapshots中),部分文件功能如下

  • dart2js: 将Dart代码转成JavaScript(已废弃,可以使用dart compile代替)。
  • analysis_server: 是Dart编程语言中的一个分析工具,它提供了静态分析、代码补全、重构、错误检查等多种功能。analysis_server通常用于集成开发环境(IDE)中,以提高开发人员的效率和代码质量。
  • dartdev: 提供开发工具,如静态分析(analyze)、格式化(format)等。
  • dartdevc: 是Dart SDK中的一个编译器,它用于将Dart代码编译为JavaScript代码。与dart2js编译器不同,dartdevc编译器主要用于开发和调试阶段,目标是生成更容易调试和优化的JavaScript代码。
  • dds: 是一个命令行工具,用于远程调试Dart应用程序。DDS全称为Dart Debug Service,它基于Dart VM和Dart开发工具链,提供了一系列调试工具和服务,以帮助开发者远程调试Dart应用程序。
  • gen_kernel: 是Dart SDK中的一个编译器,它将Dart代码转换为Flutter运行时使用的内部表示形式,也就是用于描述Dart代码的一种中间表示形式,称为“内核”(Kernel)。它是Flutter的编译工具链中的一个非常重要的组件,用于将Dart代码编译为运行在移动设备和桌面端的本地代码。
  • kernel-service: kernel-serviceDart SDK中的一个服务,它用于管理和操作Dart 内核(Kernel)的表示形式。它是Dart应用程序的编译工具链中的一个重要组件,为编译器和开发工具提供了内核的管理和操作接口。
  • kernel_worker: 它用于在单独的进程中运行Dart内核表示形式(Kernel)。它是 Dart 应用程序编译工具链中的一个重要组件,为编译器和开发工具提供了内核的运行环境。