鸿蒙 Native 开发复盘:把算法搬到 C++,为什么页面仍然卡顿?
鸿蒙 Native 开发复盘:把算法搬到 C++,为什么页面仍然卡顿?
在接入本地图像处理能力时,我遇到过一种很有迷惑性的代码结构:
ArkTS 负责界面,C++ 负责计算。看上去已经把耗时逻辑“下沉”到了 Native 层,页面应该轻松很多。
但如果 ArkTS 通过同步接口调用 C++,调用期间 UI 线程依然可能一直等待。
问题的关键是:跨语言调用,并不等于跨线程执行。
第一次实现的问题在哪里?
假设 Native 模块暴露了一个同步方法:
const result = nativeProcessor.processSync(input);
如果这段代码在 UI 线程调用,而 processSync() 内部执行了较长时间的计算,那么函数返回之前,调用线程仍然被占用。
换成 C++ 可能缩短算法执行时间,但不会自动改变同步调用的阻塞性质。
所以,排查时我会先确认线程,再讨论算法优化。
正确的拆分不只是返回一个 Promise
我会把接口分为三个阶段:
调用方 ArkTS 线程
解析参数、校验输入、创建任务
↓
Native 工作线程
执行耗时计算
↓
原 ArkTS 环境所属线程
转换结果、完成 Promise、释放资源
在 Node-API 中,napi_create_async_work() 和 napi_queue_async_work() 可以用于组织这类异步工作。
官方明确指出:执行回调运行在工作线程中,不能使用传入的原线程 env 在那里构造 napi_value。参考:使用 Node-API 接口进行异步任务开发。
最容易写错的是 Execute 阶段
下面是线程职责示意,不是完整可编译代码:
void Execute(napi_env env, void* data)
{
auto* context = static_cast<WorkContext*>(data);
// 只操作任务拥有的 Native 数据。
// 捕获异常并记录结果,避免异常越过回调边界。
context->result = RunAlgorithm(context->input);
// 不在这里使用原 env 创建 ArkTS 对象。
// 不在这里直接调用 ArkTS 回调。
}
完成回调再处理结果:
void Complete(
napi_env env,
napi_status status,
void* data)
{
auto* context = static_cast<WorkContext*>(data);
// 检查任务状态和算法执行结果。
// 在当前 env 中创建返回值。
// resolve 或 reject Promise。
// 删除 async work,并释放 context。
}
实际实现必须补齐参数检查、Node-API 返回值检查、异常处理和失败路径清理。
这里要特别区分:“没有在工作线程调用 UI”还不够,跨线程使用 ArkTS 对象和运行环境同样存在约束。
异步化之后,输入数据由谁持有?
同步调用时,输入数据通常在函数返回前就用完了。
改成异步后,任务可能在调用结束很久以后才开始执行。原先“拿到一个指针就使用”的方式,需要重新检查生命周期。
对于图像缓冲区,我会先明确:
- 缓冲区由谁分配?
- 工作线程执行时是否仍然有效?
- 其他线程能否修改它?
- 是否存在转移、分离或释放的可能?
- 最终由谁清理?
一种容易审查的方案,是在入队前把输入复制到任务拥有的 Native 容器中。它有复制成本,但所有权关系清楚。
如果数据量较大,需要进一步减少复制,也应该先建立明确的生命周期和并发访问规则,再讨论零拷贝方案。
比正常返回更难的是失败返回
一个异步接口至少需要考虑这些情况:
| 失败位置 | 需要处理的事项 |
|---|---|
| 参数校验失败 | 及时返回错误,不启动任务 |
| 创建工作项失败 | 清理已经分配的上下文 |
| 入队失败 | 释放工作项,并结束对应 Promise |
| 算法执行失败 | 保存错误信息,在完成阶段报告 |
| 正常执行结束 | 返回结果,并且只释放一次资源 |
尤其需要防止一种情况:某个错误分支释放了资源,却没有让 Promise 结束,页面就可能一直停留在加载状态。
页面已经退出,任务还没结束怎么办?
这个问题不能简单理解为“页面退出就把任务内存释放掉”。
如果工作线程还在使用这段内存,提前释放反而会制造更严重的问题。
我会分开处理两件事:
- 结果是否还应该提交给页面。
- 底层任务是否已经安全停止。
页面可以通过请求标识忽略失效结果;底层计算如果需要取消,则应设计安全的取消协议。不能把取消请求当成“任务已经停止”的证明。
我会怎样验证修复?
除了正常输入,还应覆盖:
- 空输入与非法尺寸。
- 连续发起多个请求。
- 请求完成顺序与发起顺序不同。
- 处理过程中离开页面。
- 算法失败和任务入队失败。
- 多轮执行后的 Native 内存变化。
把计算搬到 C++ 之后,工作的重点不仅是算法速度,还有线程归属、对象生命周期和错误处理。异步接口越复杂,这些约束越值得明确写进设计里。
更多推荐



所有评论(0)