写完 ArkTS 代码,点运行,代码就跑起来了。中间发生了什么?ArkTS 源码先被编译工具链编译成方舟字节码(.abc 文件),再由 ArkTS 运行时在设备上执行。运行时内部不是一坨代码,而是分成四个子系统各管一摊。这篇就把这四个子系统的职责、它们之间怎么协作、以及解释器/AOT/JIT 三种执行模式的关系讲清楚。

运行时在整体架构里的位置

先把运行时放在整个编译执行链路里看:

ArkCompiler

ArkTS 源码

ArkTS 编译工具链

方舟字节码 .abc

ArkTS 运行时

设备上执行

方舟编译运行时(ArkCompiler)是支持 ArkTS、TS、JS 编译运行的整套东西,分两部分:

  • ArkTS 编译工具链:把高级语言编译成方舟字节码文件(*.abc
  • ArkTS 运行时:在设备侧运行字节码文件,执行程序逻辑

ArkTS 运行时是 HarmonyOS 上应用的默认语言运行时,支持 ArkTS、TS 和 JS 语言的字节码及相关标准库。它提供解释器、AOT 和 JIT 高效执行方式,并通过 Node-API 实现完善的跨语言调用接口,支持多语言混合开发。

四个子系统的职责划分

ArkTS 运行时主要由四个子系统组成:Core、Execution、Compiler、Runtime。每个子系统的职责边界比较清晰。

ArkTS Runtime

Core Subsystem
基础运行库

Execution Subsystem
执行引擎

Compiler Subsystem
编译优化

Runtime Subsystem
运行时服务

File 组件
承载字节码

Tooling 组件
支持 Debugger

Base 库组件
适配系统调用

解释器

快速路径内联缓存

文件模块化管理

Stub 编译器

IR 编译优化框架

AOT 静态编译器

JIT 动态编译器

内存管理
对象分配 + GC

分析工具
DFX/Profiling

并发管理
Actor 模型

标准库
ECMAScript + Container

Node-API 接口

Core Subsystem:基础运行库

Core 子系统主要由与语言无关的基础运行库组成。这里的关键词是"与语言无关"——不管跑的是 ArkTS、TS 还是 JS,这些基础能力都要用到。

包含三个组件:

  • File 组件:承载字节码。.abc 文件的加载、解析、管理在这里
  • Tooling 组件:支持 Debugger。断点、单步、变量查看这些调试能力的底座
  • Base 库组件:负责适配系统调用。把运行时的需求对接到操作系统的接口上

Core 是其他子系统的地基。Execution 要执行字节码,得先从 File 组件拿到字节码;Debugger 要工作,得用 Tooling 组件;所有子系统的系统调用都走 Base 库。

Execution Subsystem:执行引擎

Execution 子系统负责真正执行字节码。包含三部分:

  • 解释器:逐条解释执行方舟字节码。启动快,不用等编译
  • 快速路径内联缓存:解释执行的优化手段。把热点操作的类型信息缓存下来,下次直接走快速路径,不用重新查找
  • 文件模块化管理运行:管 ES Module 的加载和依赖关系

解释器是执行的主力。应用刚启动时,所有代码都走解释器,不用等编译,启动速度有保障。内联缓存(Inline Cache)是解释器性能的关键优化——同一个方法调用,第一次要查虚表确定目标,查完把结果缓存下来,下次同类型调用直接走缓存,省掉查找开销。

Compiler Subsystem:编译优化

Compiler 子系统负责把字节码编译成机器码,提升执行性能。包含:

  • Stub 编译器:生成运行时辅助代码(Stub)。处理一些运行时的胶水逻辑
  • 基于 IR 的编译优化框架:先把字节码转成中间表示(IR),在 IR 上做优化,再生成机器码。这是编译器的核心
  • AOT 静态编译器:在应用安装或启动前就把字节码编译成机器码
  • JIT 动态编译器:运行时根据热点信息动态编译,实验中

AOT 和 JIT 是两种不同的编译时机选择。AOT 提前编译,运行时直接用机器码,没有编译开销,但编译结果可能不够精准(因为不知道运行时的实际数据);JIT 运行时编译,能根据实际热点做更精准的优化,但有编译开销。

Runtime Subsystem:运行时服务

Runtime 子系统管运行时的各种服务,内容最多:

  • 内存管理:对象分配器与垃圾回收器。GC 用的是并发标记和部分内存压缩的 CMS-GC 和 Partial-Compressing-GC
  • 分析工具:DFX 工具、CPU 和 heap 的 profiling 工具。性能分析和问题定位用的
  • 并发管理:Actor 并发模型中的方舟字节码文件管理器。多线程并发的底座
  • 标准库:ECMAScript 规范定义的标准库、高效的 container 容器库与对象模型
  • 其他:异步工作队列、C++ 交互的 Node-API 接口等

Runtime 子系统是跟应用代码接触最密的部分。每次 new 对象走内存管理,每次用数组走标准库,每次跨语言调用走 Node-API。

四个子系统怎么协作

把四个子系统串起来看一次完整的代码执行流程。假设应用里有一段代码:

function add(a: number, b: number): number {
  return a + b;
}

let result = add(1, 2);

执行过程大概是这样:

Runtime 子系统Compiler 子系统Execution 子系统Core 子系统应用启动Runtime 子系统Compiler 子系统Execution 子系统Core 子系统应用启动解释器开始执行检测到热点后续走机器码执行loop[运行期间]加载 .abc 字节码文件File 组件解析字节码交付字节码给执行引擎new 对象/分配内存对象分配器分配执行 add 函数触发编译优化字节码转 IRIR 优化生成机器码返回优化后的机器码GC 回收无用对象

拆开看每一步:

  1. 应用启动,Core 子系统的 File 组件加载 .abc 字节码文件,解析出字节码和元数据
  2. Execution 子系统的解释器拿到字节码,开始逐条解释执行
  3. 执行过程中遇到 new 对象,Runtime 子系统的内存分配器分配内存
  4. 解释器跑着跑着,Execution 的内联缓存记录下热点操作的类型信息,后续同类型操作走快速路径
  5. 如果某个函数被调用很多次,触发 Compiler 子系统编译优化:字节码转 IR → IR 优化 → 生成机器码
  6. 后续这个函数就走机器码执行,不用再解释
  7. 运行期间 Runtime 的 GC 持续回收无用对象,内存管理一直在转

四个子系统的协作关系是:Core 提供地基,Execution 负责执行,Compiler 负责把热点代码编译快,Runtime 提供执行所需的各种运行时服务。不是单向流水线,而是互相配合的循环——Execution 执行时找 Runtime 要内存,发现热点找 Compiler 编译,编译完的机器码又回到 Execution 执行。

三种执行模式

运行时提供三种执行方式:解释器、AOT、JIT。这三种不是互斥的,而是配合使用。

执行方式编译时机性能特征适用场景
解释器不编译,逐条解释启动快,执行慢应用刚启动、冷代码
AOT安装/启动前预编译启动有编译开销,执行快热点代码预编译
JIT运行时动态编译有编译开销,执行最快运行时新发现的热点

冷代码

预编译热点

运行时热点

字节码

执行方式选择

解释器执行

AOT 机器码

JIT 机器码

内联缓存优化

直接执行

解释器是兜底的执行方式。所有代码都能解释执行,启动时不用等编译,响应快。但解释执行比跑机器码慢,所以热点代码要编译。

AOT 在应用安装或启动前就把字节码编译成机器码。HarmonyOS 应用安装时会有这个步骤,把热点代码预编译好。这样应用启动后这些代码直接跑机器码,不用解释。AOT 的好处是编译开销不在运行时,坏处是编译时不知道运行时的实际数据,优化可能不够精准。

JIT 在运行时根据实际执行情况动态编译。能根据真实的类型分布、调用频率做更精准的优化。但有编译开销,编译期间会占用 CPU。目前 JIT 在 ArkTS 里还是实验性质。

实际运行时,三种方式是配合的:刚启动走解释器,AOT 预编译的代码走机器码,运行时新发现的热点走 JIT。这样兼顾启动速度和运行性能。

与 ArkUI 框架的配合

ArkTS 运行时不是孤立跑的,要跟 ArkUI 框架配合。ArkUI 负责组件体系、布局、渲染,ArkTS 运行时负责执行 ArkTS 代码(包括状态管理装饰器的逻辑)。

组件树管理

状态变化通知

执行 @State 逻辑

对象分配

异步任务

状态变量更新

ArkUI 框架

布局计算

渲染管线

ArkTS 运行时

解释器/AOT/JIT

GC 内存管理

异步工作队列

配合的要点:

  • ArkUI 的组件树构建和布局计算,最终调的是 ArkTS 代码,由运行时执行
  • @State 状态变量的依赖追踪和变化通知,逻辑在 ArkTS 代码里,运行时负责执行
  • UI 刷新触发的重渲染,执行的是 ArkTS 写的 build() 方法
  • 异步任务(网络请求回调、定时器)通过运行时的异步工作队列调度

运行时的性能直接影响 UI 流畅度。如果 build() 方法里写了耗时操作,运行时执行慢,UI 就卡。这也是为什么 HarmonyOS 强调耗时操作要放子线程——主线程的运行时资源要留给 UI。

性能优势从哪来

ArkTS 运行时的性能优势,来源可以归结到几个点:

静态类型带来的优化空间。 ArkTS 强制静态类型,编译器知道每个变量的类型,可以做基于类型的优化。比如属性访问,类型固定就能按偏移量取,不用运行时查表。动态类型语言做不到这点。

AOT 预编译。 热点代码提前编译成机器码,运行时直接执行,没有解释开销。相比纯解释执行的语言,启动后性能好。

内联缓存。 解释执行时用内联缓存记录热点操作的类型信息,后续走快速路径。这个优化对方法调用和属性访问特别有效。

固定对象布局。 ArkTS 禁止运行时变更对象布局,对象的属性偏移量在编译期就确定了。访问属性直接按偏移取,不用动态查找。

并发 GC。 GC 用并发标记,回收时不用完全停顿应用线程,对帧率影响小。配合部分内存压缩,控制内存碎片。

这些优化不是孤立起作用的,是叠加的。静态类型让 AOT 能做更激进的优化,固定对象布局让内联缓存更有效,并发 GC 保证优化后的执行不被回收打断。

一个观察运行时行为的实践

写代码时一般不直接感知运行时,但有些场景能观察到运行时行为。比如热点代码的编译:

// 一个会被频繁调用的函数
function computeHash(data: string): number {
  let hash: number = 0;
  for (let i = 0; i < data.length; i++) {
    hash = (hash * 31 + data.charCodeAt(i)) | 0;
  }
  return hash;
}

// 大量调用,触发热点编译
let start = Date.now();
for (let i = 0; i < 1000000; i++) {
  computeHash(`test_${i}`);
}
let end = Date.now();
console.info(`耗时: ${end - start}ms`);

这段代码跑起来,前几次调用走解释器,慢;调用次数到阈值后触发编译,后续走机器码,快。整体耗时分布是"先慢后快"。如果用 profiling 工具看,能看到编译事件。

实际开发中,这种热点效应意味着:性能测试别只跑一两次就下结论,要跑够量让热点编译生效,测到的才是稳态性能。

小经验分享

别在 build 方法里写耗时逻辑。 build 方法每次 UI 刷新都会执行,走的是主线程运行时。里面写循环、写大对象创建,运行时执行慢,直接卡帧。耗时的放子线程,build 里只做轻量的 UI 描述。

AOT 不是万能的。 AOT 预编译的热点是固定的,运行时如果出现新的热点(比如用户操作触发的代码路径),AOT 没覆盖到,还是会走解释器。对性能要求高的场景,预热阶段要把热点路径都跑一遍。

GC 行为影响帧率。 短时间内创建大量临时对象,GC 压力大,可能触发停顿。写高频执行的代码(比如动画回调)要控制对象创建,能复用就复用。

跨语言调用有开销。 通过 Node-API 调 C++ 不是零成本,每次调用有跨语言切换的开销。高频调用的逻辑别拆成很多次小调用,要么批量传,要么纯 ArkTS 实现。

模拟器上跑的性能数据不能当真。 官方文档明确说 ArkTS 基础库与 ArkTS 并发暂不支持模拟器。模拟器上的运行时行为跟真机不一样,性能测试要在真机上做。

profiling 工具要用起来。 Runtime 子系统提供了 CPU 和 heap 的 profiling 工具。性能问题别靠猜,用工具看热点在哪、内存分配在哪、GC 频率多少。数据驱动调优比拍脑袋靠谱。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐