HarmonyOS APP开发---“算力引擎“科学计算App,需要用到这个库
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 都是那座最省事的桥。
更多推荐


所有评论(0)