HarmonyOS APP开发—"算力引擎"科学计算App,需要用到这个库

1000×1000 矩阵乘法,纯 ArkTS 跑要 25 秒,C++ Eigen 只要 800 毫秒——30 倍差距。科学计算没有第二种选择,必须用 C++。问题是 NAPI 样板代码太多,@ohos/aki 把它压缩到一行。

📦 仓库地址 | ohpm install @ohos/aki


场景

"算力引擎"面向工科学生和科研人员:

  • 矩阵运算:乘法、求逆、特征值分解
  • FFT 变换:信号处理的频域分析
  • 数值求解:微分方程(RK4)、非线性方程求根
  • 数据拟合:最小二乘、多项式拟合

共同点:计算密集 + 已有成熟 C++ 实现(Eigen、FFTW、GSL)。用 ArkTS 重写不现实,正确做法是 C++ 复用现成库、ArkTS 负责 UI 和参数输入,中间用 AKI 桥接。


为什么是 AKI

维度裸写 NAPI@ohos/aki
绑定一个函数40+ 行样板✅ 1 行 JSBIND_FUNCTION
绑定一个类极其繁琐✅ JSBIND_CLASS + JSBIND_METHOD
类型转换手写 napi_get/set✅ 全自动
返回 Promise手写 AsyncWorker✅ aki::Promise
代码量100%✅ ~40%

关键实现

同步:矩阵乘法

// calc.cpp
#include <aki/jsbind.h>
#include <Eigen/Dense>
#include <vector>

JSBIND_FUNCTION(matrixMultiply) {
    // ✅ 在 JS 线程完成类型转换
    auto dataA = args[0].As<std::vector<double>>();
    int rowsA = args[1].As<int>(), colsA = args[2].As<int>();
    auto dataB = args[3].As<std::vector<double>>();
    int rowsB = args[4].As<int>(), colsB = args[5].As<int>();

    if (colsA != rowsB) return std::vector<double>();   // 维度校验

    Eigen::MatrixXd A = Eigen::Map<Eigen::MatrixXd>(dataA.data(), rowsA, colsA);
    Eigen::MatrixXd B = Eigen::Map<Eigen::MatrixXd>(dataB.data(), rowsB, colsB);
    Eigen::MatrixXd C = A * B;    // Eigen 自带 SIMD 加速

    std::vector<double> result(C.size());
    Eigen::Map<Eigen::VectorXd>(result.data(), C.size()) =
        Eigen::Map<const Eigen::VectorXd>(C.data(), C.size());
    return result;
}

JSBIND_ADDON(calc)
import { matrixMultiply } from 'libcalc.so'

const C = matrixMultiply([1,2,3,4], 2, 2, [5,6,7,8], 2, 2)
console.info(JSON.stringify(C))   // [19,22,43,50]

同样的功能裸写 NAPI 要 40 多行——取参数、转类型、创建返回数组、错误处理,全是样板。

异步:跨线程只传数据,不传 Value

微分方程求解要几秒,必须异步。这里是 AKI 最容易踩的坑:

// ❌ 危险:aki::Value 带到子线程
JSBIND_FUNCTION(solveODE_Bad) {
    aki::Value params = args[0];
    std::thread([params]() {
        auto p = params.As<std::vector<double>>();   // ❌ 子线程访问 JS 堆
    }).detach();
}

// ✅ 正确:JS 线程取出纯 C++ 数据,只把数据带到子线程
JSBIND_FUNCTION(solveODE) {
    // 1. JS 线程完成所有类型转换
    auto initValues = args[0].As<std::vector<double>>();
    double tStart = args[1].As<double>();
    double tEnd = args[2].As<double>();
    int steps = args[3].As<int>();

    // 2. 主动做边界校验(框架有兜底,但主动校验更稳)
    if (initValues.empty() || steps <= 0 || tEnd <= tStart) return std::vector<double>();

    // 3. 异步执行,捕获的全是纯 C++ 数据
    aki::Promise promise;
    std::thread([initValues, tStart, tEnd, steps, promise]() mutable {
        auto result = rungeKutta4(initValues, tStart, tEnd, steps);   // 纯 C++ 计算
        promise.Resolve(result);
    }).detach();

    return promise;
}

为什么:JS 对象是线程绑定的。napi_value 在哪个线程产生,就只能在该线程使用——跨线程访问 JS 堆会触发多线程检测,即使侥幸跑通也存在稳定性隐患。

import { solveODE } from 'libcalc.so'

async function solve() {
  this.computing = true
  try {
    this.result = await solveODE([1.0, 0.0], 0, 10, 1000)   // UI 不卡
    this.plotCurve(this.result)
  } finally {
    this.computing = false
  }
}
做法结果
子线程访问 aki::Value❌ 跨线程访问 JS 堆
JS 线程取纯数据 → 子线程计算✅ 安全
需要跨线程持有 JS 引用✅ 用 aki::Persistent

有状态的计算:绑定类

class ODESolver {
public:
    ODESolver() : stepSize_(0.01) {}
    void setStepSize(double h) { stepSize_ = h; }
    double getStepSize() const { return stepSize_; }

    std::vector<double> solve(std::vector<double> init, double tEnd) {
        if (init.empty() || tEnd <= 0) return {};
        lastResult_ = rungeKutta4(init, 0, tEnd, (int)(tEnd / stepSize_));
        return lastResult_;
    }
    std::vector<double> getLastResult() const { return lastResult_; }

private:
    double stepSize_;
    std::vector<double> lastResult_;
};

JSBIND_CLASS(ODESolver) {
    JSBIND_CONSTRUCTOR();
    JSBIND_METHOD(setStepSize);
    JSBIND_METHOD(getStepSize);
    JSBIND_METHOD(solve);
    JSBIND_METHOD(getLastResult);
}
JSBIND_ADDON(calc)
const solver = new ODESolver()
solver.setStepSize(0.001)
const result = solver.solve([1.0, 0.0], 10)

solver.setStepSize(0.01)          // 改步长重新算,对象状态保留
const coarse = solver.solve([1.0, 0.0], 10)

一个必须做的事:归档符号表

release 包的 .so 是 strip 过的,崩溃堆栈只有地址没有符号——不归档符号表,线上崩溃根本无法定位。

#!/bin/bash
# release_archive.sh —— 每次发版执行
VERSION=$1
COMMIT=$(git rev-parse --short HEAD)
DIR="symbols/${VERSION}-${COMMIT}"
mkdir -p $DIR

for abi in arm64-v8a armeabi-v7a; do
  cp entry/build/default/intermediates/libs/default/${abi}/libcalc.so \
     $DIR/libcalc.so.${abi}
done

echo "commit=${COMMIT}" > $DIR/build-info.txt
echo "date=$(date -Iseconds)" >> $DIR/build-info.txt

崩溃后还原:

llvm-symbolizer --obj=symbols/1.0.0-abc1234/libcalc.so.arm64-v8a \
  --functions --demangle 0x1a2b3c
# 输出: solveODE(...)  calc.cpp:127

符号表要纳入制品库、文件名带版本和 commit、至少保留一年(用户可能长期不升级),但不要提交到公开仓库。


小结

"算力引擎"这个场景里,@ohos/aki 解决了三件事:Eigen/FFTW 等成熟 C++ 库一行绑定给 ArkTS 调用(性能提升 30 倍)、aki::Promise 让长任务不卡 UI、代码量比裸写 NAPI 少一半以上。

两条铁律记住就行:跨线程只传纯 C++ 数据不传 Value,发版必须归档符号表。

科学计算、图像处理、音视频编解码、加密算法、游戏引擎——凡是需要"ArkTS 界面 + C++ 算力"的鸿蒙 App,AKI 都是那座最省事的桥。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐