鸿蒙 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 结束,页面就可能一直停留在加载状态。

页面已经退出,任务还没结束怎么办?

这个问题不能简单理解为“页面退出就把任务内存释放掉”。

如果工作线程还在使用这段内存,提前释放反而会制造更严重的问题。

我会分开处理两件事:

  1. 结果是否还应该提交给页面。
  2. 底层任务是否已经安全停止。

页面可以通过请求标识忽略失效结果;底层计算如果需要取消,则应设计安全的取消协议。不能把取消请求当成“任务已经停止”的证明。

我会怎样验证修复?

除了正常输入,还应覆盖:

  • 空输入与非法尺寸。
  • 连续发起多个请求。
  • 请求完成顺序与发起顺序不同。
  • 处理过程中离开页面。
  • 算法失败和任务入队失败。
  • 多轮执行后的 Native 内存变化。

把计算搬到 C++ 之后,工作的重点不仅是算法速度,还有线程归属、对象生命周期和错误处理。异步接口越复杂,这些约束越值得明确写进设计里。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐