[关闭]
@qidiandasheng 2021-04-20T23:36:35.000000Z 字数 6326 阅读 1682

Flutter 编译原理(😁)

Flutter


编译模式

介绍

这两种编译方式的主要区别在于是否在“运行时”进行编译,即在运行前是否会全部被翻译为机器码。

语言类型

程序的编译模式和具体的语言没有强制关系,比如Java,既可以JIT,也可以AOT。它需要经过编译,但编译的结果不是机器码,而是Java字节码(Java byte codes),这个时候是运行前编译是AOT。被编译的Java程序产生Java字节码,之后计算机模拟JVM对其进行解释,这个时候是JIT。

Dart编译模式

安装Dart SDK

  1. # 安装
  2. $ brew tap dart-lang/dart
  3. $ brew install dart
  4. $ brew install dart --devel // 安装dev版
  5. # 更新
  6. $ brew update
  7. $ brew upgrade dart
  8. $ brew cleanup dart
  9. # 查看安装路径等信息
  10. $ brew info dart

Script

最普通的JIT模式,在PC命令行调用dart vm执行dart源代码文件即是这种模式。

  1. // hello.dart
  2. main() => print('Hello, World!');
  3. $ dart hello.dart
  4. Hello, World!

自从 Dart 2 版本之后,VM 已经没有了直接从源代码执行 Dart 的功能,取而代之的是,VM 只能执行那些由内核抽象语法树(Kernel ASTs)序列化成的内核二进制文件(Kernel binaries)(又被称作 dill files)。而将 Dart 源码翻译成内核抽象语法树的任务则交给了由 Dart 编写的通用前端(common front-end(CFE)),这个工具被不同的 Dart 模块所使用(举个例子:虚拟机(VM),dart2jsDart Dev Compiler)。

dart.png-41.5kB

那么为了保持直接执行Dart源码的便捷性,所以有一个叫做kernel service的isolate,负责将Dart源码处理成Kernel,VM再将Kernel binary拿去运行。

kernel-service.png-34.6kB

根据源码生成内核二进制文件:

  1. $ dart compile kernel hello.dart -o hello.dill
  2. //运行Kernel binaries
  3. $ dart hello.dill
  4. Hello, world!

反序列化获取内核抽象语法树

从github下载dart sdk源码

  1. $ dart sdk/pkg/vm/bin/dump_kernel.dart hello.dill hello.kernel.txt
  2. //hello.kernel.txt
  3. main = hel::main;
  4. library from "file:///Users/dasheng/Work/dart_sdk/Demo/hello.dart" as hel {
  5. static method main() dynamic
  6. return core::print("Hello, World!");
  7. }

Snapshots

什么是Snapshots?

VM 能够将 isolate 中的堆,或者更多具体的对象图序列化成二进制快照。当Dart VM再次启动时,可以利用快照恢复到之前相同的状态。

快照是为了启动速度而优化的底层格式——他实质上是一个创建对象的列表,以及如何将对象关联起来的指令。快照背后的根本思想是:不需要解析Dart源码然后逐渐创建出VM的数据结构,而是直接将所有必要的数据从快照中解压缩出来,然后快速的生成一个isolate

Snapshots包含以下三种:

为最初的JIT类型Kernel Snapshots不包含机器码,将isolate中的堆,或者更多具体的对象图序直接列化成二进制快照,反序列化时再快速的生成一个isolate

snapshot.png-66.1kB

随着 AOT 编译器的加入,JIT Application Snapshots也被引入了进来,即包含机器码的Snapshots。开发AOT的动机是为了让VM能够在那些限制使用 JIT 的平台上(iOS)也能使用快照。含有代码(machine code)的快照的运行方式几乎与普通的快照一样,只有一点不同:他们包含了一个代码区,和快照其他部分不同的是,他们不需要反序列化。代码区在映射到内存后会直接称为堆的一部分:

snapshot2.png-132.3kB

AOT Application Snapshots主要就是给无法JIT的平台用的,所以Snapshots就是经过全局静态分析(type flow analysis or TFA)的机器码:

snapshot-with-code.png-45.6kB

Kernel Snapshots

内核快照能把dart程序打包成一个文件以减少启动时间。

  1. $ dart --snapshot-kind=kernel --snapshot=hello.dart.snapshot hello.dart
  2. $ dart hello.dart.snapshot arguments-for-use
  3. Hello, world!

这份快照包含有内核抽象语法树(Kernel AST)生成的内核二进制文件。但是缺少解析类和方法以及编译的代码。他是架构无关的,因此可以在平台之间迁移使用。

但是依旧需要Dart VM转换抽象语法树为内部的表示,以及在每一次运行时编译对应的函数,这将花费一部分运行时的计算能力。

注:其实hello.dart.snapshot就是上面我们提到的hello.dill,我们使用反序列化看一下生成的AST,发现是一样的

  1. $ dart /Users/dasheng/Work/dart_sdk/sdk/pkg/vm/bin/dump_kernel.dart hello.dart.snapshot hello.kernel2.txt
  2. //hello.kernel2.txt
  3. main = hel::main;
  4. library from "file:///Users/dasheng/Work/dart_sdk/Demo/hello.dart" as hel {
  5. static method main() dynamic
  6. return core::print("Hello, World!");
  7. }

JIT Application Snapshots (app-jit)

从1.21版本开始,Dart VM支持应用级别的快照(app-jit snapshots),这份快照包含了解析的类以及编译代码。

  1. $ dart --snapshot=hello.dart.snapshot --snapshot-kind=app-jit hello.dart arguments-for-training
  2. Hello, world!
  3. $ dart hello.dart.snapshot arguments-for-use
  4. Hello, world!

当运行这份快照时,Dart VM将不会解析编译那些在生成快照时使用过的类或者函数。通过这个快照启动的 Dart VM 仍然可以进行 JIT——就是当实际上的执行数据和生成快照时执行的数据不匹配的时候就会运行。

简单的说就是运行了一遍dart代码,把运行期间使用过的类或者函数做一份快照,没使用过的则在运行时进行JIT

AOT Application Snapshots (app-aot)

AOT Application Snapshots最初是为了无法进行JIT编译的平台而引入的,但也可以用来优化启动速度。无法进行JIT就意味着:

AOT模式在2.4版本需要通过dart2aot编译,通过dartaotruntime运行。

  1. $ dart2aot hello.dart hello.dart.aot
  2. $ dartaotruntime hello.dart.aot
  3. Hello, world!

在2.7的时候dart2aotdart2native替代了:

  1. $ dart2native hello.dart -k aot -o hello.dart.aot
  2. $ dartaotruntime hello.dart.aot
  3. Hello, world!

Native Machine Code机器码

dart2native还可以生成产物为平台相关的机器码,这时候生成的即为可执行程序,不需要依赖于dartaotruntime运行。即在生成的机器码中,自带了精简版的Dart运行环境。

  1. $ dart2native hello.dart -o hello.dart.machine.aot
  2. $ ./hello.dart.machine.aot
  3. Hello, World!

五种产物大小比较

  1. $ ls -al
  2. -rw-r--r-- 1 dasheng staff 33 3 1 10:29 hello.dart
  3. -rw-r--r-- 1 dasheng staff 424 3 1 14:57 hello.dart.snapshot
  4. -rw-r--r-- 1 dasheng staff 1014768 3 1 16:06 hello.dart.aot
  5. -rw-r--r-- 1 dasheng staff 4065280 3 1 14:59 hello.dart.app.jit.snapshot
  6. -rwxr-xr-x 1 dasheng staff 5475328 3 1 16:11 hello.dart.machine.aot

dart-compile-output.png-26.9kB

Flutter编译模式

介绍

Core Snapshot:对应上面Dart的编译模式Kernel Snapshot,Dart的bytecode模式,bytecode模式是不区分架构的。bytecode模式可以归类为 AOT编译。

Core JIT:对应上面的Dart的编译模式JIT Application Snapshots,Dart的一种二进制模式,将指令代码和 heap数据打包成文件,然后在vm和 isolate启动时载入,直接标记内存可执行,可以说这是一种 AOT模式。Core JIT也被叫做AOTBlob

AOT Assembly: 即Dart的AOT模式。直接生成汇编源代码文件,由各平台自行汇编。包体积比较大,区分架构。

开发阶段

调试模式时使用虚拟机 (VM) 来运行 Dart 代码 (因此这时会显示 “Debug” 字样,以提醒开发者速度会稍微变慢),这样便可以启用有状态热重载 (Stateful Hot Reload)。

app-aot-blobs模式使用snapshotssnapshots文件是需要Dart VM去加载执行的。使用snapshots使得动态执行代码变成可能。

上一节讲到Dart编译模式时需要使用CFE将Dart转换为Kernel binary交给VM运行。其实我们可以将dart代码转换为Kernel binary执行Kernel binary这两个过程也可以分离开来。

Flutter的Debug模式就是这么做的,在两个不同的机器执行,比如host机器(用户的开发机)执行编译,移动设备执行Kernel binaryFlutter tools负责从开发机上将Kernel binary发送到移动设备上。

flutter-cfe.png-35.9kB

图解:

生产阶段

Flutter 打包编译

命令行编译

1.flutter_assets

编译生成flutter_assets

  1. $ flutter build bundle

下图为dart_tool编译生成的中间产物,其中app.dill就是我们的业务代码:

bundle.png-40kB

下图为最终生成的产物,其中kernel_blob.bin即为app.dill的拷贝:

bundle2.png-33.2kB

2.Framework

编译生成App.frameworkFlutter.framework,包含第一步:

  1. $ flutter build ios-framework

3.安装包

编译生成.app目标文件,包含第一第二步:

  1. flutter build ios
  2. flutter build ios debug

编译产物

iOS编译产物在下图所示的目录中,分为Debug产物和Release产物:

build.png-45.4kB

产物分为以下两种:

Release产物下的App.framework

release.png-33.8kB

Debug产物下的App.framework

debug.png-39.3kB

我们能看到Debug下的产物多了isolate_snapshot_datakernel_blob.binvm_snapshot_data这几个文件,这几个文件主要用于

参考

Flutter 编译原理
Dart VM介绍
Snapshots
Dart构建产物与使用
浅谈Flutter构建
Dart VM 是如何运行你的代码的

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注