
开篇
HarmonyOS NEXT 上跑 AI 推理模型,启动速度是个绕不开的坎。大多数开发者会选择在 onPageShow 或 aboutToAppear 里加载模型,然后发现 App 从点击图标到 UI 完全渲染,中间黑屏个两三秒很正常。很多人第一反应是优化模型体积或调整加载时机,但实际效果有限。
真正影响启动速度的核心因素之一,是 ArkCompiler 的编译模式。默认情况下应用以 JIT(Just-In-Time)方式运行,这意味着每次冷启动时,关键的热点代码都需要边解释边编译,AI 推理这类计算密集型任务尤其吃亏。AOT(Ahead-Of-Time)编译能提前生成机器码,把编译这个步骤从启动时挪到构建时,效果立竿见影。
但这个功能在 DevEco CLI 里默认不开启,而且配置起来有不少细节需要注意。
它解决什么问题
AOT 编译的核心思路很简单:在应用安装到设备上之前,就把 TypeScript/ArkTS 代码编译成高效的机器码。这样启动时 ArkCompiler 不需要花时间做 JIT 编译、类型推导和内联缓存填充,直接执行已经优化好的机器码。
对 AI 应用来说,这个优势非常明显。推理引擎初始化、模型文件解析、张量内存分配这几步往往涉及大量循环和条件判断,是典型的编译热点。AOT 能把这些路径提前编译好,启动时直接运行,省掉 JIT 的编译时间和性能损耗。
适合的场景:
- AI 推理模型的加载和初始化
- 首页复杂 UI 的渲染链
- 计算密集型的启动任务
不适合的场景:
- 动态加载的模块(AOT 只覆盖构建时可确定的代码路径)
- 频繁反射调用的代码(ArkCompiler 对动态调用优化有限)
| 编译模式 | 启动速度 | 包体积增量 | 代码覆盖 |
|---|---|---|---|
| JIT | 慢 | 无 | 所有代码 |
| AOT | 快 | 约 5-8% | 静态可确定路径 |
DevEco CLI 默认使用 JIT,需要通过 aot: true 显式启用 AOT。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(真机测试)
模拟器上 AOT 行为与真机有差异,下文会讲。
核心实现:启用 AOT 编译
1. 修改 DevEco CLI 构建配置
DevEco CLI 的构建配置在项目根目录下的 build-profile.json5 中。找到 "buildOption" 段,添加 "aot": true:
json5
{
"app": {
"buildOption": {
"aot": true,
"strictMode": "loose"
}
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"buildOption": {
"aot": true,
"sourceMap": false
}
}
]
}
注意点:
aot需要在app和对应的module层级都配置,缺一不可- 启用了
strictMode: "strict"时 AOT 可能编译失败,建议先用 "loose" 测试 sourceMap: false能减少 AOT 构建时间,调试阶段可以关闭
配置完成后执行 hvigorw assembleHap --mode module 重新构建,安装到设备上即可。
2. 性能对比:加载 AI 模型
先用一段简单的测试代码来验证效果。假设我们在 aboutToAppear 里加载一个 ONNX 模型:
typescript
// demo/entry/src/main/ets/pages/Index.ets
import { AILoader } from '@kit.AI';
@Entry
@Component
struct Index {
@State modelLoaded: boolean = false;
private startTime: number = 0;
aboutToAppear() {
this.startTime = performance.now();
this.loadModel();
}
async loadModel() {
// 模拟加载模型文件
const model = new AILoader();
await model.load('/path/to/model.onnx');
const loadTime = performance.now() - this.startTime;
console.log(`[AOT Test] Model load time: ${loadTime}ms`);
this.modelLoaded = true;
}
build() {
// AI 推理 UI 呈现
if (!this.modelLoaded) {
Column() {
Text('Loading AI model...')
.fontSize(30)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else {
Column() {
Text('Model loaded, UI ready')
.fontSize(30)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
}
在没有启用 AOT 的情况下,冷启动日志输出:
[AOT Test] Model load time: 3152ms
启用 AOT 后,重复测试:
[AOT Test] Model load time: 1784ms
效果:加载时间缩短了 43%。这个数字会根据模型大小和代码复杂度变化,但 AOT 的优势非常明显。

3. 深入 AOT 编译原理
ArkCompiler 的 AOT 编译流程分为三个阶段:
第一阶段:Profiling 信息收集(JIT 预热)
构建过程中,DevEco CLI 会先以 JIT 模式运行应用的热点代码,收集类型信息和调用频率。这个过程会生成一个 .aprof 文件,里面记录了变量的实际类型、函数调用次数、内联缓存命中情况。
第二阶段:类型推导与内联缓存生成
ArkCompiler 根据 Profiling 数据,对函数参数、返回值、属性访问做静态类型推导。比如代码里 let x = 1; x + 2;,JIT 阶段知道 x 是 number,AOT 就知道直接生成加法的机器码,不需要在运行时做类型检查。内联缓存(Inline Cache)会把频繁调用的函数内联展开,减少函数调用的开销。
第三阶段:机器码生成
把优化后的中间表示(IR)编译成本地机器码,打包到 HAP 包里。安装到设备后,应用启动时直接加载这些预编译的 .so 文件,跳过 JIT 编译。
为什么对 AI 应用特别有效? AI 模型加载和推理过程里有大量 for 循环、张量运算、条件分支,这些代码在 Profiling 阶段会被标记为热点。AOT 把它们全部提前编译成机器码,设备上运行时只需要按顺序执行指令,不需要反复触发编译。
常见问题
问题 1:AOT 编译卡在 99%
现象: 执行 hvigorw assembleHap 时,进度条在 99% 卡住,最终超时报错。
原因: 某些第三方库或动态加载的模块无法被 AOT 静态分析。常见的是 @kit.AI 中某些推理引擎的初始化逻辑包含动态注册回调的模式,ArkCompiler 无法在构建阶段确定调用链。
解决方案:
- 在
build-profile.json5中对特定模块排除 AOT:
json5
"buildOption": {
"aot": true,
"aotExclude": ["libai-core.so", "modules/ai/*"]
}
- 或者把模型加载逻辑封装成独立模块,对这个模块只做 JIT 编译,其他模块启用 AOT。
问题 2:模拟器上 AOT 无效
现象: 在模拟器中运行,启动速度没有明显提升,甚至比 JIT 还慢。
原因: 模拟器的 CPU 架构和指令集与真机不同。AOT 生成的机器码是针对真机 ARM64 架构的,模拟器上运行的 x86 或自定义指令集无法直接使用,ArkCompiler 会在运行时回退到 JIT 模式。
解决方案: AOT 优化必须在真机硬件上验证。开发阶段可以先用 JIT,发布前在目标设备机型(如 Mate 60 Pro、Pura 70 等)上做 AOT 测试。
最佳实践
-
不要在 AOT 开启下大规模使用
eval或动态函数创建ArkCompiler 的 AOT 编译器只能处理静态可确定的代码。如果代码里频繁出现
eval(str)或new Function(...),这些动态执行片段会被 JIT 编译,AOT 的整体收益会被稀释。AI 应用的模型加载逻辑里尤其要注意,避免动态拼接推理参数。 -
把模型文件放在
resources/rawfile下AOT 编译时,ArkCompiler 会尝试分析文件读取路径。如果把模型放在沙箱目录里动态下载,AOT 编译阶段无法预知文件路径,相关代码可能无法被充分优化。放在
rawfile下可以让 ArkCompiler 确定文件是只读的、固定的,从而做更多的静态分析。 -
使用
@Performance装饰器做分层监控启用了 AOT 后,应该把启动性能的监控代码和业务逻辑解耦。推荐在
Index组件上加@Performance装饰器,自动记录aboutToAppear到build完成的耗时,而不是自己手写performance.now():
typescript
@Entry
@Performance({ log: true, threshold: 2000 })
@Component
struct Index {
build() {
// ...
}
}
这样既能监控 AOT 优化的效果,又不会把性能测试代码带到生产环境。
FAQ
Q:AOT 和 JIT 可以同时用吗?
A:可以。AOT 编译时,对于动态加载的代码或 JIT 阶段没有收集到 Profiling 数据的模块,ArkCompiler 会生成 JIT 编译桩。设备上运行时,AOT 代码直接执行,如果遇到未预编译的部分,会触发 JIT 编译。这种混合模式在 DevEco CLI 的 aot: true 配置下默认启用。
Q:为什么 AOT 后包体积变大了?
A:AOT 把一部分编译结果放到包内,通常是 .so 文件,大小约为原代码的 5-8%。对于 10MB 的 HAP 来说,增加 500-800KB 是可以接受的。这个增量在首次安装时会被解压到运行时目录,后续启动不再需要编译,所以启动速度的提升远大于存储成本的增加。
Q:如何确认 AOT 是否生效?
A:在设备上通过 hdc shell 进入应用目录,检查 /data/app/cur/0/com.example.app/cache/ark-cache/ 下是否有 AOT 编译生成的 .so 文件。或者在 DevEco Studio 的 Log 中搜索 AOT 关键字,ArkCompiler 会在启动时打印 AOT mode: enabled 日志。