在 ArkTS 开发中,“假异步”是一个隐蔽的性能杀手。它指的是代码虽然写成了 async/await 形式,但其实际逻辑仍然在 主线程(UI 线程) 上同步执行,从而导致界面卡死。
1. 如何避免“假异步”导致的性能问题?
什么是“假异步”?
在 ArkTS 中,仅仅给函数加上 async 关键字并不会自动开启新线程。async/await 只是改变了代码的执行顺序(协程化),它依然运行在调用它的那个线程上。
-
典型错误示范:
TypeScript
async function computeTask() { // 这是一个循环 1 亿次的同步计算 let sum = 0; for (let i = 0; i < 100000000; i++) { sum += i; } return sum; } // 在 UI 线程调用 this.computeTask().then(res => { ... }); // 界面依然会卡死,因为循环是在主线程跑的
如何实现“真异步”?
要真正释放主线程,必须将任务物理搬运到其他线程:
- 使用
taskpool.execute():将纯计算逻辑封装在@Concurrent函数中。 - 使用
worker.postMessage():将长生命周期任务交给 Worker。 - 使用系统提供的异步 I/O:鸿蒙系统的
fs.read、http.request等内置 API 底层由 C++ 线程池实现,它们是真正的非阻塞异步。
2. 如何判断任务是否真的在子线程?
你可以通过以下三种简单方法进行实时验证:
A. 打印线程 ID(最直接)
使用 process.tid(Thread ID)在逻辑开始处打印。如果 ID 与主界面启动时打印的一致,说明任务在主线程。
TypeScript
import process from '@ohos.process';
console.info("Current Thread ID: " + process.tid);
B. 观察 UI 交互
在执行任务的同时,尝试点击界面上的按钮或滑动列表。
- 真异步:界面依然丝滑,响应灵敏。
- 假异步:界面完全失去响应,直到任务结束。
C. 故意制造阻塞测试
在任务中加入一个长达 5 秒的同步死循环。如果是真异步,DevEco Studio 的日志窗口会持续滚动,且手机界面不黑屏。
3. 如何用工具分析线程使用?
DevEco Studio 提供的 Profiler 是分析线程占用的“显微镜”。
A. 使用 CPU Profiler (重点)
-
在 DevEco Studio 下方打开 Profiler,选择 CPU 录制。
-
选择 Time Line 视图。
-
观察线程条:
- Main Thread:这是 UI 线程。如果你发现这里出现了长段的绿色(Running)或红色(Jank)条块,说明有耗时任务阻塞了 UI。
- TaskPool / Worker Thread:如果你提交了任务,下方会出现对应的子线程条。如果子线程条在波动而主线程是空白/轻量跳动,说明异步成功。
B. 查看 Trace 轨迹
通过录制一段 Trace(.htrace 文件),可以清晰看到任务在不同线程间的“跳转”过程:
- 查找
js_task_execute标签,看它所属的线程名称。 - 分析 Total Duration,如果主线程的任务耗时超过 16ms,系统会自动标记为红色警告。
总结:性能防御 Checklist
| 检查项 | 验证手段 | 优化方案 |
|---|---|---|
| 是否阻塞主线程 | 查看 Profiler 的 Main Thread 占用。 | 将逻辑移入 TaskPool。 |
| 任务是否运行 | 在子线程打印 process.tid。 | 确保使用了 @Concurrent 装饰器。 |
| 线程切换频率 | 观察 Trace 中的任务切换点。 | 合并细碎任务,减少跨线程通信频率。 |
| 异步是否生效 | 检查是否误用了同步 API(如 fs.readSync)。 | 全部替换为 Promise 版本的异步接口。 |
架构师建议:
永远保持对主线程的“敬畏”。如果你不确定一段代码是否耗时,默认把它当成耗时的,并使用 TaskPool 封装。