HarmonyOS APP开发---“算力引擎“科学计算App,需要用到这个库
HarmonyOS APP开发—"算力引擎"科学计算App,需要用到这个库
做一个科学计算 App,矩阵运算、FFT 变换、微分方程数值求解——这些算法用 ArkTS 跑慢到无法接受,必须用 C++。但 NAPI 样板代码让人头秃,
@ohos/aki一行绑定就能让 ArkTS 调 C++,本文还会讲跨线程数据传递和线上崩溃定位的工程实践。
📦 仓库地址:https://gitcode.com/CPF-ApplicationTPC/aki | 安装:
ohpm install @ohos/aki
写在前面
"算力引擎"面向工科学生和科研人员,提供:
- 矩阵运算:乘法、求逆、特征值分解
- FFT 变换:信号处理的频域分析
- 数值求解:微分方程(RK4)、非线性方程求根
- 数据拟合:最小二乘、多项式拟合
这些算法的共同点:计算密集 + 已有成熟 C/C++ 实现(Eigen、FFTW、GSL 等开源库)。用 ArkTS 重写不现实——一个 1000×1000 矩阵乘法,ArkTS 可能要几十秒,C++ 用 Eigen 只要几百毫秒。
正确做法:C++ 复用现成库算,ArkTS 负责 UI 和参数输入,中间用 AKI 桥接。
裸写 NAPI 的话,绑定一个矩阵乘法要写几十行 napi_get_value_* / napi_create_*;绑定一个 C++ 类更是一场噩梦。AKI 把这些压缩到一行。
本文除了基础用法,重点讲两个工程实践:跨线程传数据的正确姿势(科学计算必然要子线程跑长任务)和发版符号表归档(线上崩溃要能定位)。
这篇文章聊什么
- 绑定矩阵运算——C++ Eigen 库一行暴露给 ArkTS
- 长任务异步化——子线程跑计算,跨线程只传数据
- 符号表归档——让 release 包的崩溃可定位
第一步:依赖配置
cd entry/src/main/cpp
git clone https://gitcode.com/CPF-ApplicationTPC/aki.git
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(calc)
set(NATURAL_RENDER_LIB_PATH ${NATURAL_RENDER_LIB_PATH} aki)
add_subdirectory(aki)
# Eigen 矩阵运算库
find_package(Eigen3 REQUIRED)
add_library(calc SHARED calc.cpp)
target_link_libraries(calc PUBLIC
aki_jsbind
Eigen3::Eigen
libace_napi.z.so
)
第二步:绑定矩阵运算(同步)
小矩阵计算很快(< 100ms),直接同步调用:
// 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>();
int colsA = args[2].As<int>();
auto dataB = args[3].As<std::vector<double>>();
int rowsB = args[4].As<int>();
int colsB = args[5].As<int>();
// 维度校验:A 的列数必须等于 B 的行数
if (colsA != rowsB) {
return std::vector<double>(); // 维度不匹配返回空
}
// Eigen 矩阵运算(SIMD 加速)
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;
// 转回一维数组返回给 ArkTS
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_FUNCTION(matrixInverse) {
auto data = args[0].As<std::vector<double>>();
int n = args[1].As<int>();
Eigen::MatrixXd M = Eigen::Map<Eigen::MatrixXd>(data.data(), n, n);
// 判断是否可逆
double det = M.determinant();
if (std::abs(det) < 1e-10) {
return std::vector<double>(); // 奇异矩阵,不可逆
}
Eigen::MatrixXd Inv = M.inverse();
std::vector<double> result(Inv.size());
Eigen::Map<Eigen::VectorXd>(result.data(), Inv.size()) =
Eigen::Map<const Eigen::VectorXd>(Inv.data(), Inv.size());
return result;
}
JSBIND_ADDON(calc)
ArkTS 侧调用:
import { matrixMultiply, matrixInverse } from 'libcalc.so'
// 2x2 矩阵乘法
const A = [1, 2, 3, 4] // [[1,2],[3,4]]
const B = [5, 6, 7, 8] // [[5,6],[7,8]]
const C = matrixMultiply(A, 2, 2, B, 2, 2)
console.info('A×B = ' + JSON.stringify(C)) // [19,22,43,50]
// 求逆
const inv = matrixInverse(A, 2)
if (inv.length === 0) {
promptAction.showToast({ message: '该矩阵不可逆' })
} else {
console.info('A⁻¹ = ' + JSON.stringify(inv))
}
对比裸 NAPI:同样功能,NAPI 要写 40+ 行样板(取参数、转类型、创建返回数组、错误处理),AKI 就是普通 C++ 函数 + 一行 JSBIND_FUNCTION。
第三步:长任务异步化(跨线程数据传递的正确姿势)
FFT 变换、微分方程求解可能耗时几秒,必须异步执行避免卡 UI。这里是跨线程的关键点:
⚠️ 核心原则:JS 对象是线程绑定的。在 JS 线程创建的
aki::Value拿到子线程访问,等于跨线程访问 JS 堆,会出问题。正确做法:在 JS 线程把数据取成纯 C++ 类型,只把纯数据带到子线程。
// ❌ 危险写法:把 aki::Value 带到子线程
JSBIND_FUNCTION(solveODE_Bad) {
aki::Value params = args[0]; // JS 对象
aki::Promise promise;
std::thread([params, promise]() mutable {
auto p = params.As<std::vector<double>>(); // ❌ 子线程访问 JS 堆
// ...
}).detach();
return promise;
}
// ✅ 正确写法:JS 线程取出纯数据,只把数据带到子线程
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 {
// 纯 C++ 数值求解(RK4 算法),不碰任何 JS 对象
std::vector<double> result = rungeKutta4(initValues, tStart, tEnd, steps);
promise.Resolve(result); // Promise 内部处理线程安全
}).detach();
return promise; // 返回 Promise 给 ArkTS
}
// FFT 变换(同样模式)
JSBIND_FUNCTION(fftTransform) {
auto signal = args[0].As<std::vector<double>>();
if (signal.empty()) return std::vector<double>();
aki::Promise promise;
std::thread([signal, promise]() mutable {
auto spectrum = performFFT(signal); // 纯 C++ 计算
promise.Resolve(spectrum);
}).detach();
return promise;
}
ArkTS 侧 await:
import { solveODE, fftTransform } from 'libcalc.so'
@Entry
@Component
struct CalcPage {
@State computing: boolean = false
@State result: number[] = []
async solve() {
this.computing = true
try {
// await 等待 C++ 子线程计算完成,UI 不卡
this.result = await solveODE([1.0, 0.0], 0, 10, 1000)
console.info(`求解完成,共 ${this.result.length} 个点`)
this.plotCurve(this.result)
} catch (err) {
console.error('计算失败: ' + JSON.stringify(err))
promptAction.showToast({ message: '计算失败,请检查参数' })
} finally {
this.computing = false
}
}
build() {
Column() {
if (this.computing) {
LoadingProgress().width(48).height(48)
Text('正在计算...').fontSize(14).fontColor('#999')
}
Button('求解微分方程').onClick(() => this.solve()).enabled(!this.computing)
}
}
}
跨线程对照表:
| 做法 | 结果 |
|---|---|
子线程访问 aki::Value | ❌ 跨线程访问 JS 堆,出问题 |
| JS 线程取出纯数据 → 子线程计算 | ✅ 安全 |
| 需要跨线程持有 JS 引用 | ✅ 用 aki::Persistent |
| 子线程回调 JS | ✅ 用 aki::Promise 或线程安全函数 |
第四步:绑定 C++ 求解器类
科学计算通常有状态(保存中间结果、可迭代求解),用类绑定更合适:
class ODESolver {
public:
ODESolver() : stepSize_(0.01), method_(RK4) {}
void setStepSize(double h) { stepSize_ = h; }
double getStepSize() const { return stepSize_; }
void setMethod(int m) {
method_ = (m >= 0 && m <= 2) ? static_cast<Method>(m) : RK4;
}
std::vector<double> solve(std::vector<double> init, double tEnd) {
if (init.empty() || tEnd <= 0) return {};
int steps = static_cast<int>(tEnd / stepSize_);
lastResult_ = integrate(init, 0, tEnd, steps, method_);
return lastResult_;
}
std::vector<double> getLastResult() const { return lastResult_; }
double getErrorEstimate() const { return errorEstimate_; }
private:
enum Method { EULER, MIDPOINT, RK4 };
double stepSize_;
Method method_;
std::vector<double> lastResult_;
double errorEstimate_ = 0.0;
};
// AKI 绑定类:构造函数 + 成员方法 + 成员属性
JSBIND_CLASS(ODESolver) {
JSBIND_CONSTRUCTOR();
JSBIND_METHOD(setStepSize);
JSBIND_METHOD(getStepSize);
JSBIND_METHOD(setMethod);
JSBIND_METHOD(solve);
JSBIND_METHOD(getLastResult);
JSBIND_METHOD(getErrorEstimate);
}
JSBIND_ADDON(calc)
import { ODESolver } from 'libcalc.so'
const solver = new ODESolver()
solver.setStepSize(0.001) // 精细步长
solver.setMethod(2) // RK4
const result = solver.solve([1.0, 0.0], 10)
console.info(`步长 ${solver.getStepSize()},解出 ${result.length} 个点`)
console.info(`误差估计: ${solver.getErrorEstimate()}`)
// 改变步长重新求解(对象状态保留)
solver.setStepSize(0.01)
const coarse = solver.solve([1.0, 0.0], 10)
第五步:发版符号表归档(线上崩溃可定位)
科学计算 App 涉及大量 C++ 代码,release 包的 .so 是 strip 过的,崩溃堆栈只有地址没有符号——不归档符号表,线上崩溃根本无法定位。
每次发版都要归档符号表:
#!/bin/bash
# release_archive.sh —— 发版时执行
VERSION=$1
COMMIT=$(git rev-parse --short HEAD)
ARCHIVE_DIR="symbols/${VERSION}-${COMMIT}"
if [ -z "$VERSION" ]; then
echo "用法: ./release_archive.sh <version>"
exit 1
fi
mkdir -p $ARCHIVE_DIR
# 归档未 strip 的 .so(含符号信息)
for abi in arm64-v8a armeabi-v7a; do
SO_PATH="entry/build/default/intermediates/libs/default/${abi}/libcalc.so"
if [ -f "$SO_PATH" ]; then
cp "$SO_PATH" "$ARCHIVE_DIR/libcalc.so.${abi}"
echo "已归档: libcalc.so.${abi}"
fi
done
# 记录构建信息
cat > $ARCHIVE_DIR/build-info.txt <<EOF
version=${VERSION}
commit=${COMMIT}
branch=$(git branch --show-current)
date=$(date -Iseconds)
builder=$(whoami)
EOF
echo "✅ 符号表已归档到 $ARCHIVE_DIR"
echo "⚠️ 请将此目录纳入版本管理或上传到制品库,切勿删除"
崩溃后还原堆栈:
# 从崩溃日志拿到 pc 偏移地址,用符号表还原到源码行
llvm-symbolizer \
--obj=symbols/1.0.0-abc1234/libcalc.so.arm64-v8a \
--functions --demangle \
0x1a2b3c
# 输出示例:
# solveODE(std::vector<double>, double, double, int)
# calc.cpp:127
符号表管理建议:
| 做法 | 说明 |
|---|---|
| 纳入制品库 | 每个 release 版本对应一份符号表 |
| 文件名带版本和 commit | symbols/1.0.0-abc1234/ |
| 保留至少一年 | 用户可能长时间不升级 |
| 不要提交到公开仓库 | 符号表含内部实现细节 |
第六步:性能对比(为什么必须用 C++)
// 1000×1000 矩阵乘法性能对比
async function benchmark() {
const N = 1000
const A = new Array(N * N).fill(0).map(() => Math.random())
const B = new Array(N * N).fill(0).map(() => Math.random())
// C++ Eigen(通过 AKI)
const t1 = Date.now()
const cResult = matrixMultiply(A, N, N, B, N, N)
const cppTime = Date.now() - t1
// 纯 ArkTS 实现
const t2 = Date.now()
const jsResult = multiplyInArkTS(A, B, N)
const jsTime = Date.now() - t2
console.info(`C++ Eigen: ${cppTime}ms`)
console.info(`纯 ArkTS: ${jsTime}ms`)
console.info(`性能提升: ${(jsTime / cppTime).toFixed(1)}x`)
}
// 典型结果:C++ Eigen ~800ms,纯 ArkTS ~25000ms,提升 30x+
为什么"算力引擎"选了 AKI?
| 维度 | 裸写 NAPI | @ohos/aki |
|---|---|---|
| 绑定一个函数 | 40+ 行样板 | ✅ 1 行 JSBIND_FUNCTION |
| 绑定一个类 | 极其繁琐 | ✅ JSBIND_CLASS + JSBIND_METHOD |
| 类型转换 | 手写 napi_get/set | ✅ 全自动 |
| 返回 Promise | 手写 AsyncWorker | ✅ aki::Promise |
| 线程安全 | 手写 ThreadSafeFunction | ✅ 内置封装 |
| 代码量 | 100% | ✅ ~40% |
总结
"算力引擎"这个场景里,@ohos/aki 解决了三件事:
- C++ 算法复用——Eigen/FFTW/GSL 等成熟库一行绑定给 ArkTS 调用,性能提升 30x+
- 长任务不卡 UI——
aki::Promise+ 子线程,跨线程只传纯 C++ 数据不传 Value - 线上可定位——符号表归档流程让 release 包崩溃能还原到源码行
科学计算、图像处理、音视频编解码、加密算法、游戏引擎——凡是需要"ArkTS 界面 + C++ 算力"的鸿蒙 App,@ohos/aki 都是那座最省事的桥。
更多推荐


所有评论(0)