Dart VM 有多种执行代码的方式,例如:
- 使用源代码或内核二进制文件的 JIT 模式;
- 使用快照:
- 来自 AOT 快照;
- 来自 AppJIT 快照;
然而它们之间的主要区别在于: VM “何时”以及“如何”将 Dart 源代码转换为可执行代码,然后保证执行的运行时环境保持不变。
VM 中的任何 Dart 代码都在某个 isolate
中运行,可以将其描述为:具有自己的内存(堆)并且通常具有自己的控制线程(mutator 线程)的 Dart 隔离宇宙。
VM 可以有许多 isolate
同时执行 Dart 代码,但它们不能直接共享任何状态,只能通过端口传递消息进行通信(不要与网络端口混淆!)。
这里的 OS 线程和 isolate
之间的关系有点模糊,并且高度依赖于虚拟机嵌入到应用程序的方式,但是主要需要保证以下内容:
- 一个 OS 线程一次只能进入一个
isolate
,如果它想进入另一个isolate
,它必须离开当前isolate
; - 一次只能有一个与
isolate
相关联的 Mutator 线程,Mutator 线程是执行 Dart 代码并使用 VM 的公共 C API 的线程。
然而同一个 OS 线程可以先进入一个 isolate
执行 Dart 代码,然后离开这个 isolate
并进入另一个 isolate
继续执行;或者有许多不同的 OS 线程进入一个 isolate
并在其中执行 Dart 代码,只是不会同时发生。
当然,除了单个 Mutator 线程之外,isolate
还可以关联多个辅助线程,例如:
- 一个后台 JIT 编译器线程;
- GC sweeper 现场;
- 并发 GC marker 线程;
VM 在内部使用线程池 (dart::ThreadPool
) 来管理 OS 线程,并且代码是围绕 dart::ThreadPool::Task
概念而不是围绕 OS 线程的概念构建的。
例如在 GC VM 中将 dart::ConcurrentSweeperTask
发布到全局 VM 的线程池,而不是生成专用线程来执行后台清除,并且线程池实现要么选择空闲线程,要么在没有可用线程时生成新线程;类似地,用于 isolate
来消息处理事件循环的默认实现实际上,并没有产生专用的事件循环线程,而是在新消息到达时将dart::MessageHandlerTask
发布到线程池。
通过 JIT 运行源代码
本节将介绍当从命令行执行 Dart 时会发生什么:
// hello.dart
main() => print(‘Hello, World!’);
$ dart hello.dart
Hello, World!
Dart 2 VM 开始不再具有从原始代码直接执行 Dart 的能力,相反 VM 希望获得包含序列化内核 AST 的内核二进制文件(也称为 dill 文件)。将 Dart 源代码翻译成 Kernel AST 的任务是由通用前端 (CFE)处理的,CFE 是用 Dart 编写并在不同 Dart 工具上共享(例如 VM、dart2js、Dart Dev Compiler)。
为了保持直接从源代码执行 Dart ,这里托管一个名为 kernel service 的辅助 isolate
,它处理将 Dart 源代码编译到内核中,然后 VM 运行生成的内核二进制文件。
然而这种设置并不是 CFE 和 VM 运行 Dart 代码的唯一方法,例如 Flutter 是将编译到 Kernel 的过程和从 Kernel 执行的过程完全分离,并将它们放在不同的设备上实现:编译发生在开发者机器(主机)上,执行在目标移动设备上处理,目标移动设备接收由 flutter 工具发送给它的内核二进制文件。
这里需要注意,该 Flutter 工具不处理 Dart 本身的解析, 相反它会生成另一个持久进程 frontend_server
,它本质上是围绕 CFE 和一些 Flutter 特定的 Kernel-to-Kernel 转换的封装。
frontend_server
将 Dart 源代码编译为内核文件, 然后 flutter 将其发送到设备, 当开发人员请求热重载时 frontend_server
开始发挥作用:在这种情况下 frontend_server
可以重用先前编译中的 CFE 状态,并重新编译实际更改的库。
一旦内核二进制文件加载到 VM 中,它就会被解析以创建代表各种程序实体的对象,然而这个过程是惰性完成的:首先只加载关于库和类的基本信息,源自内核二进制文件的每个实体都保留一个指向二进制文件的指针,以便以后可以根据需要加载更多信息。
只有在运行时需要它时(例如查找类成员、分配实例等),有关类的信息才会完全反序列化,在这个阶段,类成员会从内核二进制文件中读取,然而在此阶段不会反序列化完整的函数体,只会反序列化它们的签名。
此时 methods 在运行时可以被成功解析和调用,因为已经从内核二进制文件加载了足够的信息,例如它可以解析和调用 main
库中的函数。
最初所有的函数都会有一个占位符,而不是它们的主体的实际可执行代码:它们指向 LazyCompileStub
,它只是要求运行时系统为当前函数生成可执行代码,然后 tail-calls
这个新生成的代码。
第一次编译函数时,是通过未优化编译器完成的。
未优化编译器分两遍生成机器代码:
- 1、遍历函数体的序列化 AST 以生成函数体的控制流图( CFG ),CFG 由填充有中间语言( IL ) 指令的基本块组成。在此阶段使用的 IL 指令类似于基于堆栈的虚拟机的指令:它们从堆栈中获取操作数,执行操作,然后将结果推送到同一堆栈。
- 2、生成的 CFG 使用一对多的底层 IL 指令直接编译为机器代码:每个 IL 指令扩展为多个机器语言指令。
在此阶段没有执行任何优化,未优化编译器的主要目标是快速生成可执行代码。
这也意味着:未优化的编译器不会尝试静态解析内核二进制文件中未解析的任何调用,VM 当前不使用基于虚拟表或接口表的调度,而是使用内联缓存实现动态调用。
内联缓存背后的核心思想,是在特定的调用点中缓存方法解析的结果,VM 使用的内联缓存机制包括:
-
一个调用特定的缓存(
dart::UntaggedICData
),它将接收者的类映射到一个方法,如果接收者是匹配的类,则应该调用该方法,缓存还存储一些辅助信息,例如调用频率计数器,用于跟踪给定类在此调用点上出现的频率; -
一个共享查找 stub ,它实现了方法调用的快速路径。这个 stub 搜索给定的缓存,以查看它是否包含与接收者的类匹配的条目。如果找到该条目,则 stub 将增加频率计数器和
tail-calls
用缓存方法。否则 stub 将调用一个运行时系统助手来实现方法解析逻辑。如果方法解析成功,则缓存将被更新,后续调用将不需要进入运行时系统。
如下图所示,展示了与 animal.toFace()
调用关联的内联缓存的结构和状态,该缓存使用 Dog
的实例执行了两次,使用 Cat
的实例执行了一次C。
未优化的编译器本身足以执行任何 Dart 代码,然而它产生的代码相当慢,这就是为什么 VM 还实现了自适应优化编译管道的原因,自适应优化背后的想法是:使用运行程序的执行配置文件来驱动优化决策。
当未优化的代码运行时,它会收集以下信息:
- 如上所述,内联缓存收集有关在调用点观察到的接收器类型的信息;
- 函数和函数内的基本块相关联的执行计数器跟踪代码的热点区域;
当与函数关联的执行计数器达到一定阈值时,该函数被提交给后台优化编译器进行优化。
优化编译的启动方式与非优化编译的启动方式相同:通过遍历序列化内核 AST ,为正在优化的函数构建未优化的 IL。
然而不是直接将 IL 处理为机器代码,而是基于表单的优化 IL, 优化编译器继续将未优化的 IL 转换为静态单赋值(SSA) ,然后基于 SSA 的 IL 根据收集的类型反馈进行专业化的推测,并通过一系列Dart 的特定优化,例如:
- 内联(inlining);
- 范围分析(range analysis);
- 类型传播( type propagation);
- 代理选择(representation selection);
- 存储加载和加载转发(store-to-load and load-to-load forwarding);
- 全局值编号(global value numbering);
- 分配下沉(,allocation sinking)等,;
最后使用线性扫描寄存器和简单的一对多降低 IL 指令,将优化的 IL 转化为机器代码。
编译完成后,后台编译器会请求 mutator 线程进入安全点并将优化的代码附加到函数中。
下次调用此函数时, 它将使用优化的代码。 某些函数包含非常长的运行循环,对于那些函数,在函数仍在运行时,将执行从未优化代码切换到优化代码是有意义的。
这个过程被称为堆栈替换( OSR ),它的名字是因为:一个函数版本的堆栈帧被透明地替换为同一函数的另一个版本的堆栈帧。
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
高级UI与自定义view;
自定义view,Android开发的基本功。
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
NDK开发;
未来的方向,高薪必会。
前沿技术;
组件化,热升级,热修复,框架设计
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;《Android架构视频+学习笔记》
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md)**
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!