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 把这些压缩到一行。

本文除了基础用法,重点讲两个工程实践:跨线程传数据的正确姿势(科学计算必然要子线程跑长任务)和发版符号表归档(线上崩溃要能定位)。

这篇文章聊什么

  1. 绑定矩阵运算——C++ Eigen 库一行暴露给 ArkTS
  2. 长任务异步化——子线程跑计算,跨线程只传数据
  3. 符号表归档——让 release 包的崩溃可定位

< 100ms

> 100ms

ArkTS: 用户输入矩阵

参数校验

AKI 绑定层

预估耗时

同步调用 C++

aki::Promise 异步

子线程跑 Eigen/FFTW

promise.Resolve 结果

ArkTS await 拿结果

渲染计算结果

线上 crash

符号表还原堆栈

定位到源码行


第一步:依赖配置

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 版本对应一份符号表
文件名带版本和 commitsymbols/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手写 AsyncWorkeraki::Promise
线程安全手写 ThreadSafeFunction✅ 内置封装
代码量100%✅ ~40%

总结

"算力引擎"这个场景里,@ohos/aki 解决了三件事:

  1. C++ 算法复用——Eigen/FFTW/GSL 等成熟库一行绑定给 ArkTS 调用,性能提升 30x+
  2. 长任务不卡 UI——aki::Promise + 子线程,跨线程只传纯 C++ 数据不传 Value
  3. 线上可定位——符号表归档流程让 release 包崩溃能还原到源码行

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

Logo

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

更多推荐