【HarmonyOS开发小实践】ArkTS 运行时全貌四个子系统怎么协作
写完 ArkTS 代码,点运行,代码就跑起来了。中间发生了什么?ArkTS 源码先被编译工具链编译成方舟字节码(.abc 文件),再由 ArkTS 运行时在设备上执行。运行时内部不是一坨代码,而是分成四个子系统各管一摊。这篇就把这四个子系统的职责、它们之间怎么协作、以及解释器/AOT/JIT 三种执行模式的关系讲清楚。
运行时在整体架构里的位置
先把运行时放在整个编译执行链路里看:
方舟编译运行时(ArkCompiler)是支持 ArkTS、TS、JS 编译运行的整套东西,分两部分:
- ArkTS 编译工具链:把高级语言编译成方舟字节码文件(
*.abc) - ArkTS 运行时:在设备侧运行字节码文件,执行程序逻辑
ArkTS 运行时是 HarmonyOS 上应用的默认语言运行时,支持 ArkTS、TS 和 JS 语言的字节码及相关标准库。它提供解释器、AOT 和 JIT 高效执行方式,并通过 Node-API 实现完善的跨语言调用接口,支持多语言混合开发。
四个子系统的职责划分
ArkTS 运行时主要由四个子系统组成:Core、Execution、Compiler、Runtime。每个子系统的职责边界比较清晰。
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);
执行过程大概是这样:
拆开看每一步:
- 应用启动,Core 子系统的 File 组件加载
.abc字节码文件,解析出字节码和元数据 - Execution 子系统的解释器拿到字节码,开始逐条解释执行
- 执行过程中遇到
new对象,Runtime 子系统的内存分配器分配内存 - 解释器跑着跑着,Execution 的内联缓存记录下热点操作的类型信息,后续同类型操作走快速路径
- 如果某个函数被调用很多次,触发 Compiler 子系统编译优化:字节码转 IR → IR 优化 → 生成机器码
- 后续这个函数就走机器码执行,不用再解释
- 运行期间 Runtime 的 GC 持续回收无用对象,内存管理一直在转
四个子系统的协作关系是:Core 提供地基,Execution 负责执行,Compiler 负责把热点代码编译快,Runtime 提供执行所需的各种运行时服务。不是单向流水线,而是互相配合的循环——Execution 执行时找 Runtime 要内存,发现热点找 Compiler 编译,编译完的机器码又回到 Execution 执行。
三种执行模式
运行时提供三种执行方式:解释器、AOT、JIT。这三种不是互斥的,而是配合使用。
| 执行方式 | 编译时机 | 性能特征 | 适用场景 |
|---|---|---|---|
| 解释器 | 不编译,逐条解释 | 启动快,执行慢 | 应用刚启动、冷代码 |
| AOT | 安装/启动前预编译 | 启动有编译开销,执行快 | 热点代码预编译 |
| JIT | 运行时动态编译 | 有编译开销,执行最快 | 运行时新发现的热点 |
解释器是兜底的执行方式。所有代码都能解释执行,启动时不用等编译,响应快。但解释执行比跑机器码慢,所以热点代码要编译。
AOT 在应用安装或启动前就把字节码编译成机器码。HarmonyOS 应用安装时会有这个步骤,把热点代码预编译好。这样应用启动后这些代码直接跑机器码,不用解释。AOT 的好处是编译开销不在运行时,坏处是编译时不知道运行时的实际数据,优化可能不够精准。
JIT 在运行时根据实际执行情况动态编译。能根据真实的类型分布、调用频率做更精准的优化。但有编译开销,编译期间会占用 CPU。目前 JIT 在 ArkTS 里还是实验性质。
实际运行时,三种方式是配合的:刚启动走解释器,AOT 预编译的代码走机器码,运行时新发现的热点走 JIT。这样兼顾启动速度和运行性能。
与 ArkUI 框架的配合
ArkTS 运行时不是孤立跑的,要跟 ArkUI 框架配合。ArkUI 负责组件体系、布局、渲染,ArkTS 运行时负责执行 ArkTS 代码(包括状态管理装饰器的逻辑)。
配合的要点:
- 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 频率多少。数据驱动调优比拍脑袋靠谱。
更多推荐



所有评论(0)