本文记录 Koala Plot(koalaplot-core)0.12.1 接入 OpenHarmony 的完整过程,覆盖图表库盘点、鸿蒙 Compose 制品线选型、真实引擎整库编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

Koala Plot 是基于 Compose Multiplatform 的图表与绘图库:饼图/环形图、折线图(线性/对数/分类轴)、堆叠面积图、柱状图、子弹图、雷达/极坐标图、热力图,以及可组合的图表布局与图例,Android、桌面、iOS、Web 单一 API 共享。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:Koala Plot 的 commonMain 全部 79 个 Kotlin 文件都是 androidx.compose.* 代码,没有可剥离的非 Compose 内核;而社区 Maven 上已经出现完整的鸿蒙 Compose klib 制品线(runtime / foundation / ui / material3 / animation 1.12.0-0.1.0-07,全部带 ohosArm64 / ohosX64 变体),因此不需要隔离——整个上游图表库(切片语义、轴模型刻度与缩放/平移状态机、自动定标、二维直方图分箱、色标、手势处理)被原样编译成 OpenHarmony KLIB,再通过 @CName C ABI + CMake N-API 把图表计算引擎暴露给 ArkTS,ArkUI Canvas 重绘;同批源码另有一个 jvm() 目标对上游制品线(Compose Multiplatform 1.11.1)交叉验证"整库编译没有引入漂移"。

项目地址: AtomGit/oh-tpc/koalaplot-core

上游仓库: KoalaPlot/koalaplot-core

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

Koala Plot 为 Compose 应用提供"图表"这一件事的完整答案:PieChart 可组合切片(悬停展开)、可组合标签与连接线、环形中心内容、首帧动画;XYGraph 的线性/对数/分类轴、可组合标题与值标签、数据点符号、单/双轴缩放平移、注记、直线/阶梯/贝塞尔曲线;堆叠面积;VerticalBarPlot 聚类/堆叠/负值/非零起点;BulletGraphs 子弹图;雷达/极坐标;HeatMapPlot 二维直方图与色标;ChartLayout + 图例。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套图表能力,但上游构建(Kotlin 2.4.10 / Gradle 9.4.1 / AGP 9.0.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出图表,却无法证明公共图表引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证图表引擎、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.10 / Gradle 9.4.1 / AGP 9.0.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据。
依赖矩阵全部 79 个 commonMain 文件 import androidx.compose.*——runtime / foundation / ui / material3 / animation 每一件都要有 ohos klib 变体才能解析;coroutines 同理。
没有可提取的内核与 qrose(纯 Kotlin 编码器 + Painter 隔离)相反,Koala Plot 是 100% Compose 库,composeMain 隔离老路等于把仓库清空。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
组合语义跨不了桥@Composable、手势、动画是 Kotlin 侧概念,C ABI 只能同步一来一回;OHOS 侧也没有社区 Compose 线的组合宿主。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:整库 ohos 编译(含唯一的 Modifier.onGestureInput expect/actual 接缝的真实 ohos 实现)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。图表计算语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和重绘。

1.2 库提供的能力

Demo 提供一组真实的图表计算会话,对应桥接的全部入口:

  • 饼图切片语义:切片权重、起始角、扩展角、入场动画进度 β 送入真实的 makePieSliceData(上游 internal 函数,PieChart Composable 的切片引擎),返回每片的起始角/扫过角;切片颜色来自真实的 generateHueColorPalette(HSL 均匀色相);
  • 坐标轴与交互状态机:线性轴(构造参数、主/次刻度、缩放/平移/重置)全部由真实 DoubleLinearAxisModel 执行——zoom() / pan() 带最小/最大视图范围钳制,连点缩放可观察到真实边界;对数轴刻度与偏移来自真实 LogAxisModel;
  • 数据点映射:折线/散点的每个数据点通过 probes 参数在真实模型里 computeOffset,ArkTS 永远不做值↔像素的二次插值;
  • 二维直方图热力图:样本送入真实 generateHistogram2D 分箱,值域由真实 autoScaleRange 计算,格子颜色由真实 linearColorScale(蓝→红)逐格插值;
  • 版本握手:koalaPlotVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / axis回显本次操作与轴会话名(x/y 各自持有真实视图状态)。
major / minor主/次刻度表 [{v, off}],值与偏移都来自真实模型。
viewStart / viewEnd缩放/平移后的当前视图范围(真实钳制后的结果)。
probeOff数据点探测偏移数组(真实 computeOffset)。
slices / rows饼图切片角度与颜色 / 热力图逐格颜色(真实引擎计算)。

引擎侧完整能力(子弹图、极坐标、堆叠面积、注记、图例排版、悬停展开等)全部在上游 commonMain 里原样随编译进入 .so,未单独开桥接入口。

1.3 实现适配

维度要求
代码复用commonMain 全部 79 个文件原样编译,不提取、不隔离、不重写;仅新增 2 个 ohos 源文件(手势 actual + headless 门面)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);android/ios/js/wasmJs 目标、Sonatype 发布、dokka/detekt/ktlint 随根构建剥离(原文保留为 *.upstream)。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致(Compose 1.11.1 / material3 1.9.0 / coroutines 1.11.0),ohos 侧只换仓库到鸿蒙制品线(换线不换版,双线拆分见第 2 阶段)。
UI 完整单应用覆盖全部桥接入口:饼图 / 坐标轴 / 热力图 / 链路四个 Tab,全部 Canvas 重绘。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 侧暂无社区 Compose 线的组合宿主(没有把 @Composable 树变成像素的渲染层),所以页面沿用 cmp_qrose / vetra-ui / compose-shelf 的社区模式:跨 C ABI 只传值,ArkTS Canvas 重绘——但每一个角度、刻度、偏移、分箱计数和颜色都由 .so 内的上游原版实现计算。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认鸿蒙 Compose klib 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、依赖双线拆分
第 3 阶段:整库编译       ── commonMain 79 文件原样编译 ohos KLIB、唯一 expect 的触摸手势 ohos actual
第 4 阶段:桥协议建模     ── 7 个 op 的扁平 JSON 契约、headless 门面进库内(internal 可见性)
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、$ORIGIN rpath、DisposeString
第 6 阶段:ArkUI 宿主     ── 四 Tab 图表 Demo、Canvas 重绘、HAP、真机验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
src/commonMain/                    上游图表引擎(79 文件,进 ohos 构建,jvm + 双 ohos)
src/ohosMain/
  └─ .../util/ModifierExtensions.ohos.kt     唯一 expect 的 ohos actual(触摸手势)
  └─ .../KoalaPlotHeadless.ohos.kt           桥协议门面:7 op + 真实引擎调用
src/jvmMain/ (jvmTest/)            上游 desktopMain/desktopTest 平移(jvm 交叉验证目标)
src/{android,ios,js,wasmJs}Main/   上游其余平台源,留盘不进构建
example/nativeApp/                 Kotlin/Native 动态库(@CName 桥 + 双 ABI 拷贝任务)
example/harmonyApp/                DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md     鸿蒙适配说明(中英双份)
README.upstream.md                 上游原文保存
convention-plugins/ versions.properties detekt.yml .github/   上游工具链,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,根项目即库模块,rootProject.name = "ohos_koalaplot_core"。上游的 refreshVersions 插件、convention-plugins(Sonatype 发布)、dokka / detekt / ktlint、AGP classpath 全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 钉在 F:/cmp/.tools/jdk-21.0.12.1+1)
Compose(ohos 线)1.12.0-0.1.0-07(runtime / foundation / ui / material3 / animation 均有 ohos klib)
Compose(jvm 线)1.11.1 / material3 1.9.0 / coroutines 1.11.0(Maven Central,与上游 0.12.1 同版本体)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.6.1(2.4.20 线支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.koalaplot
export JAVA_HOME="F:/cmp/.tools/jdk-21.0.12.1+1"
java -version
1.3 源码依赖度盘点
源码集文件依赖形态处置
commonMain79全部 androidx.compose.{runtime,foundation,ui,material3,animation} + coroutines,零平台 API整体编译(jvm + 双 ohos,一行不改)
desktopMain / desktopTest1 / 18onGestureInput 鼠标 actual + Compose 桌面测试平移为 jvmMain / jvmTest(模板下 jvm 目标源集名)
androidMain iosMain jsMain wasmJsMain各 1onGestureInput 各平台 actual留盘不进构建(ohos 另写第 6 个 actual)

expect 声明全库只有 1 处:internal expect fun Modifier.onGestureInput(...)。上游给了 5 个平台 actual——android/ios 是触摸同型(pointerInput + 公共手势 handler + 样条衰减),desktop/js/wasmJs 是鼠标滚轮型。ohos 补第 6 个,照 android/ios 抄:

// src/ohosMain/kotlin/io/github/koalaplot/core/util/ModifierExtensions.ohos.kt
internal actual fun Modifier.onGestureInput(
    key1: Any?, key2: Any?, gestureConfig: GestureConfig,
    onZoomChange: (size: IntSize, centroid: Offset, zoom: ZoomFactor) -> Unit,
    onPanChange: (size: IntSize, pan: Offset) -> Boolean,
): Modifier = this then Modifier.pointerInput(key1, key2, gestureConfig) {
    val gesturesHandler = TransformGesturesHandlerImpl(
        panHandler = PanHandlerImpl(),
        panFlingHandler = PanFlingHandlerImpl(
            panFlingBehavior = PanFlingBehavior(
                animationSpec = SplineBasedFloatDecayAnimationSpec(density = this),
            ),
        ),
        zoomHandler = if (gestureConfig.independentZoomEnabled) StickyAxisZoomHandler(/*…*/)
                      else LockedRatioZoomHandler(),
    )
    gesturesHandler.detectTransformGestures(/*…*/)
}

值得单独说明:SplineBasedFloatDecayAnimationSpec 在鸿蒙 Compose 线中存在——android/ios 的惯性衰减在 ohos 上原样可用,不需要降级到 exponentialDecay。加上库内门面,这就是整个 fork 唯二的新增源文件。

1.4 页面边界
Tab 切换        饼图 / 坐标轴 / 热力图 / 链路(底部四 Tab)
饼图 Tab        切片1-6 值滑杆、起始角(-180..180)、扩展角(90..360)、动画β(0..1)、重置
                Canvas 饼图(真实角度+真实色板)、图例(值+角度)、状态行
坐标轴 Tab      线性/对数切换、正弦/散点数据预设
                放大×1.25 / 缩小×0.8 / ←平移 / 平移→ / 重置(驱动真实 zoom()/pan() 状态机)
                Canvas 轴+主/次刻度+曲线(probeOff 映射)、视图范围行
热力图 Tab      采样数 200/500/1000、分箱 6..24、重新采样
                Canvas 逐格颜色(真实 linearColorScale)、状态行
链路 Tab        版本握手、调用链路图、桥接日志(最近 8 次 op/ok/耗时/字节)、真实与复刻对照

页面启动时先调 koalaPlotVersion() 做握手,每个 Tab 的 Canvas onReady 即发起首次桥接调用。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品。用 .module 元数据逐个验证:

org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07   同上
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07                   同上
org.jetbrains.compose.material3:material3:1.12.0-0.1.0-07     同上
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07     同上
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07 同上

三个关键结论:其一,Compose 制品线一应俱全,100% Compose 库整库编译的障碍已经消失——qrose 时代"隔离 Painter"是因为这条线当时不存在;其二,ohos 线不发布 *-desktop 子制品,jvm() 交叉验证目标不能从这条线解析桌面变体,必须双线拆分(jvm 走 Maven Central 的 1.11.1,与上游 0.12.1 构建所用版本体完全一致——仍是换线不换版);其三,material-icons 等周边线没有 ohos 变体,好在 Koala Plot 不用图标(vetra-ui 需要另做 icons-shim)。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 2.2.21 只能读到 2.3.0,编译期直接报:

Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.

工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.4.10 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 用 9.6.1(2.4.20 线支持 Gradle 9.x,上游 wrapper 9.4.1 同代小步升级,不像 2.2.21 旧线必须降 8.14.1)。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
// dependencyResolutionManagement 同序;rootProject.name = "ohos_koalaplot_core"
include(":example:nativeApp")
2.3 编译真实引擎

根 build.gradle.kts 收敛为极简 KMP 库构建(上游原文保留为 build.gradle.kts.upstream):

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)   // 库声明 @Composable,编译器插件必须保留
}

kotlin {
    explicitApi()
    applyDefaultHierarchyTemplate()

    jvm() { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } }   // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        ohosMain.dependencies {          // ohos 线(1.12.0-0.1.0-07)
            implementation(libs.compose.runtime)
            implementation(libs.compose.foundation)
            implementation(libs.compose.material3)
            api(libs.compose.ui)
            api(libs.compose.animation)
            implementation(libs.kotlinx.coroutines)
        }
        jvmMain.dependencies {           // jvm 线(Maven Central,上游同版本体)
            implementation(libs.compose.runtime.jvm)      // …1.11.1
            implementation(libs.compose.foundation.jvm)
            implementation(libs.compose.material3.jvm)    // …1.9.0
            api(libs.compose.ui.jvm)
            api(libs.compose.animation.jvm)
            implementation(libs.kotlinx.coroutines.jvm)   // …1.11.0
        }
    }
}

Compose 依赖按目标线声明在 ohosMain / jvmMain 上,commonMain 保持线无关——同一批源码、两条解析线(vetra-ui / compose-shelf 已审核的同款拆法)。一个必须处理的源集细节:上游用 kotlin.mpp.applyDefaultHierarchyTemplate=false + jvm("desktop") 手工层级,而鸿蒙构建启用默认层级模板后 jvm("desktop") 不再生成 desktopMain 访问器——处置是目标改回 jvm(),src/desktopMain / src/desktopTest git mv 到 src/jvmMain / src/jvmTest(git 100% rename,内容零改动)。实测:

./gradlew :compileKotlinJvm            # BUILD SUCCESSFUL(79 文件,上游制品线)
./gradlew :compileKotlinOhosArm64 :compileKotlinOhosX64   # BUILD SUCCESSFUL(同批文件,ohos 线)

引擎的全部 commonMain 文件一个没动,新增的只有 ohos actual 与门面两个文件。

2.4 测试依赖的处置:kotlin-test 的实验性标记

上游 18 个 desktopTest 文件基于 Kotlin 2.4.10 的 kotlin-test 编写;2.4.20-0.1.0-07 线把其中使用的断言 API 标为 @ExperimentalKotlinTestApi,直接编译报 opt-in 错误。处置是给 jvmTest 编译加一个 opt-in(测试文件零改动),并沿用社区纪律——测试统一在 JVM 验证,根构建全局禁用 ohos 测试任务:

jvmTest { compilerOptions { optIn.add("kotlin.test.ExperimentalKotlinTestApi") } }

allprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

实测 ./gradlew compileTestKotlinJvm 通过——上游测试文件一个字节不动。

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.koalaplot.core/**(上游图表引擎,原样编译)
        ↓ makePieSliceData / 轴模型状态机 / autoScaleRange / generateHistogram2D
KoalaPlotHeadless.ohos.kt(库内门面,7 op 扁平 JSON 进出)
        ↓ KoalaPlotDispatch / KoalaPlotVersion(@CName)
N-API koalaPlotDispatch() / koalaPlotVersion()
        ↓
ArkUI Index.ets(四 Tab,Canvas 重绘)

请求与响应(节选):

请求 : {"op":"pie","values":"30,20,45","start":0,"extend":360,"beta":1}
       {"op":"axis.linear","axis":"x","min":-0.6,"max":6.9,"len":320,
        "zoom":1.25,"pivot":0.5,"probes":"0,0.5,1"}
       {"op":"axis.log","expStart":-1,"expEnd":3}
       {"op":"axis.category","count":5,"offset":"full"}
       {"op":"autoscale","values":"..."}
       {"op":"histogram2d","xs":"...","ys":"...","bins":16}
响应 : {"ok":"true","op":"pie","total":95,"slices":[{"i":0,"value":30,
        "startAngle":0,"angle":113.68,"color":"#FF800080"},…]}
       {"ok":"true","op":"axis.linear","viewStart":1.26,"viewEnd":6.9,
        "major":[{"v":1.5,"off":0.045},…],"minor":[…],"probeOff":[0.06,0.31,0.56]}
       {"ok":false,"op":"…","error":"…"}

两个关键设计:axis.linear 会话有状态——同一 axis 名 + 同一构造键的调用复用缓存的 DoubleLinearAxisModel 实例,zoom/pan 增量作用于当前视图,zoom()/pan() 里的最小/最大视图范围钳制因此被真实执行(Demo 连点"放大"能观察到钳制);probes 让数据点映射也发生在真实模型里——ArkTS 拿到的 probeOff 就是 computeOffset 的原值,渲染端永不做值↔像素的二次插值。

3.2 门面为什么放在库内

桥接需要调用 makePieSliceData(internal——上游把切片语义封装在 PieChart Composable 内部)。internal 对库模块外不可见,所以门面放在库自己的 src/ohosMain(compose-shelf 的 ShelfHeadless.ohos.kt 同法):

// src/ohosMain/kotlin/io/github/koalaplot/core/KoalaPlotHeadless.ohos.kt(节选)
public object KoalaPlotHeadless {
    private val linearAxes = HashMap<String, Pair<String, DoubleLinearAxisModel>>()

    private fun pieResponse(json: MiniJson): String {
        val slices = makePieSliceData(              // 上游 internal 引擎,同模块可见
            data = values, beta = beta,
            pieStartAngle = start.deg, pieExtendAngle = extend.deg,
        )
        val colors = generateHueColorPalette(values.size)   // 上游真实色板
        /* 拼装 slices[{i,value,startAngle,angle,color}] */
    }
}

example/nativeApp 的 Bridge.kt 只做 @CName 导出,且不应用 compose 编译器插件——桥只调用非 Composable 门面,没有需要插件变换的 lambda 要链接(vetra-ui / compose-shelf 同款说明)。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("KoalaPlotDispatch")
fun koalaPlotDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":"false","op":"dispatch","error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺 op、空 values、max<=min、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libkoalaplot_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoskoalaplot.so,28.1 MB / 26.9 MB)
  │ KoalaPlotDispatch / KoalaPlotVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制图表语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("KoalaPlotDispatch")
fun koalaPlotDispatch(request: String): String   // 7 op 一口进

@CName("KoalaPlotVersion")
fun koalaPlotVersion(): String                   // 版本握手
C ABI 符号ArkTS 方法覆盖内核
KoalaPlotDispatchkoalaPlotDispatch(req)makePieSliceData + DoubleLinearAxisModel / LogAxisModel / CategoryAxisModel / autoScaleRange / generateHistogram2D / linearColorScale
KoalaPlotVersionkoalaPlotVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"koalaPlotDispatch", nullptr, NapiKoalaPlotDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"koalaPlotVersion", nullptr, NapiKoalaPlotVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskoalaplot_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskoalaplot_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoskoalaplot SHARED IMPORTED)
set_target_properties(ohoskoalaplot PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(koalaplot_napi SHARED napi_init.cpp)
target_link_libraries(koalaplot_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskoalaplot)

OHOS_ARCH 为空或 libohoskoalaplot.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const koalaPlotDispatch: (request: string) => string;
export const koalaPlotVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 触发调用与状态刷新
private callBridge(op: string, req: string): string {
  const t0 = new Date().getTime();
  const reply = koalaPlotDispatch(req);
  const ms = new Date().getTime() - t0;
  const parsed = JSON.parse(reply) as VersionResp;
  this.logs = [{ op: op, ok: parsed.ok, ms: ms, bytes: reply.length }]
    .concat(this.logs).slice(0, 8);        // 链路 Tab 桥接日志
  return reply;
}

private dispatchLinearAxis(name: string, zoom: number, pan: number, reset: boolean): void {
  const req = '{"op":"axis.linear","axis":"' + name + '","min":-0.6,"max":6.9,' +
    '"len":320,"probes":"' + this.xs.join(',') + '"' +
    (zoom !== 1 ? ',"zoom":' + zoom + ',"pivot":0.5' : '') +
    (pan !== 0 ? ',"pan":' + pan : '') + (reset ? ',"reset":"true"' : '') + '}';
  this.lastX = JSON.parse(this.callBridge('axis.linear(' + name + ')', req)) as AxisResp;
}

每次桥接调用的 op、ok、耗时与响应字节数进链路 Tab 的日志区;Canvas 在响应到达后重绘。

5.2 页面交互

四个 Tab 自上而下:饼图 Tab 用 6 个切片值滑杆 + 起始角/扩展角/动画 β 滑杆驱动真实 makePieSliceData,图例显示每片值与扫过角;坐标轴 Tab 的放大/缩小/平移/重置按钮把参数发给真实 zoom()/pan(),画布下方实时显示钳制后的视图范围,曲线与散点全部经 probeOff 映射;热力图 Tab 控制采样数与分箱数,重新采样按钮换 LCG 种子;链路 Tab 展示版本握手行、ArkTS → NAPI → C ABI → headless 门面 → 真实引擎 分层说明、桥接日志与"真实与复刻"对照。

5.3 对比度和错误边界

操作按钮深色底白字,数据用蓝色主线(#FF1E88E5)与灰色刻度分层;ok:false 时错误原文进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskoalaplot)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/io/github/koalaplot/nativeapp/
│       └── Bridge.kt                         @CName × 2(门面薄封装)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.koalaplot、versionName 0.12.1-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoskoalaplot_api.h
        ├── src/main/cpp/types/libkoalaplot_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :compileKotlinJvm compileTestKotlinJvm                  # 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.koalaplot -m entry

四、完整代码对照

4.1 整体架构

io.github.koalaplot.core/**(上游图表引擎原样编译,79 文件)
    │ PieChart / XYGraph / VerticalBarPlot / BulletGraphs / HeatMapPlot / 手势 / 图例
io.github.koalaplot.core.KoalaPlotHeadless(库内 ohosMain 门面)
    │ 7 op 扁平 JSON:pie / axis.linear / axis.log / axis.category / autoscale / histogram2d / version
Bridge.kt(@CName × 2)
    │ KoalaPlotDispatch / KoalaPlotVersion
libkoalaplot_napi.so
    │ koalaPlotDispatch / koalaPlotVersion
Index.ets
    │ JSON parse + Canvas 重绘
    ▼
ArkUI 真机页面(四 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_koalaplot_core、上游工具链移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + Compose 双线(ohos 1.12.0-0.1.0-07 / jvm 1.11.1)钉版
build.gradle.kts(根)版本 0.12.1-ohos.1、jvm()+双 ohos 目标、按线依赖、全局禁用 Ohos*Test* 任务
src/ohosMain/.../ModifierExtensions.ohos.kt唯一 expect 的 ohos actual(触摸手势 + 样条衰减)
src/ohosMain/.../KoalaPlotHeadless.ohos.kt桥协议门面(7 op、轴会话缓存、MiniJson、错误通道)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI(门面薄封装,无 compose 编译器插件)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(21 节)

4.3 关键 API 对照

层次API作用
引擎makePieSliceData(values, beta, start, extend)饼图切片语义(internal,门面同模块调用)
引擎generateHueColorPalette(count)HSL 均匀色相色板(Demo 切片颜色来源)
引擎DoubleLinearAxisModel线性轴:构造校验、视图状态机
引擎AxisModel.zoom(factor, pivot) / pan(amount)视图缩放/平移,带最小/最大范围钳制
引擎computeTickValues(axisLength: Dp)主/次刻度值生成
引擎computeOffset(value)值→[0,1] 偏移(Demo 曲线与刻度位置来源)
引擎LogAxisModel(range)对数轴刻度与偏移
引擎CategoryAxisModel / autoScaleRange()分类轴偏移 / 量级取整自动定标
引擎generateHistogram2D(...) / linearColorScale(...)二维分箱 / 逐格颜色插值
NativeKoalaPlotDispatch / KoalaPlotVersionC ABI JSON 导出
N-APIkoalaPlotDispatch(req) / koalaPlotVersion()向 ArkTS 暴露能力
ArkTScallBridge(op, req)触发调用、计时、刷新日志与画布

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(koalaPlotDispatch(
  '{"op":"pie","values":"30,20,45","start":0,"extend":360,"beta":1}')) as PieResp;
// res.slices[0] = { i:0, value:30, startAngle:0, angle:113.68, color:'#FF800080' }
// Canvas 侧 arc(cx, cy, r, startAngle*PI/180, (startAngle+angle)*PI/180) 直接使用
val slices = makePieSliceData(values, beta, start.deg, extend.deg)   // 上游原件
val colors = generateHueColorPalette(values.size)                    // 上游原件

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:整库编译而不是 composeMain 隔离

qrose 时代的"隔离 Painter"路线,前提是库里存在不依赖 Compose 的内核(编码器),以及当时 ohos 上没有 Compose 制品。本次适配先验证制品线(runtime / foundation / ui / material3 / animation 的 ohos klib 全部存在),障碍已消失——再隔离等于把图表库清空。整库编译让切片语义、轴状态机、刻度生成、手势处理全部是上游原件(vetra-ui / compose-shelf 已审核的同一分支)。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 9.6.1

coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.4.10,2.4.20 同代零降级。Gradle 方面 2.4.20 线支持 9.x,wrapper 用 9.6.1——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:Compose 依赖双线拆分(换线不换版)

ohos 线不发布 *-desktop 子制品,jvm 交叉验证目标必须走 Maven Central。拆分的代价是依赖声明从 commonMain 挪到 ohosMain / jvmMain 两个源集;收益是 jvm 线用与上游 0.12.1 完全相同的版本体(Compose 1.11.1 / material3 1.9.0 / coroutines 1.11.0)——同一批源码对两条线编译通过,本身就是"换线不换版、源码未漂移"的证明。

决策 4:桥只传计算值,门面放在库内

OHOS 没有社区 Compose 线的组合宿主,@Composable 上不了屏;桥传"计算结果"(角度/刻度/偏移/分箱/颜色)而不是"绘制指令",让 ArkTS 重绘始终以下游身份消费上游数学。makePieSliceData 是 internal,门面必须进库内 src/ohosMain(compose-shelf 的 ShelfHeadless 同法);桥上不引入 kotlinx.serialization,手写 MiniJson(cmp_qrose 继承),不增加 KN 依赖、保持 C 签名稳定。

决策 5:jvm 交叉验证 + 测试零改动

引擎逻辑在 commonMain,JVM 与 ohos 编译同一份代码;上游 18 个测试文件平移到 jvmTest 后在鸿蒙工具链下编译通过(唯一追加一个 kotlin.test.ExperimentalKotlinTestApi opt-in,纯工具链版本差异),测试文件零改动——交叉验证的有效性才成立。运行需要桌面 Compose 测试环境(AWT),不在本 fork 验收范围,如实记录。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(F:/cmp/.tools/jdk-21.0.12.1+1)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :compileKotlinJvm :compileKotlinOhosArm64 :compileKotlinOhosX64
./gradlew compileTestKotlinJvm

同一批 79 个 commonMain 文件对两条制品线(上游 CMP 1.11.1 / 社区 1.12.0-0.1.0-07)编译全部通过;18 个上游测试文件(饼图 3、坐标轴 7、柱状 2、热力图 2、图例 1、手势 1、动画 1、工具 2 等)在鸿蒙工具链下编译通过、零改动。桌面 UI 测试的运行需要 AWT 环境,属上游 CI 职责,本 fork 以编译验证为准并如实记录。

6.3 链接与 HAP 验证

:compileKotlinJvm / compileTestKotlinJvm: BUILD SUCCESSFUL
:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: KoalaPlotDispatch / KoalaPlotVersion / libohoskoalaplot_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
BuildNativeWithCmake: finished(双 ABI 编出 libkoalaplot_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

libohoskoalaplot.so 因嵌入完整 Compose 栈,arm64 约 28.1 MB、x86_64 约 26.9 MB(debugShared 未 strip;vetra-ui 同路线 28.9 MB 属同量级),HAP 由 DevEco 签名构建生成后安装真机。

6.4 功能用例

  1. 启动后链路 Tab 握手显示 upstream 0.12.1 / fork 0.12.1-ohos.1 / kotlin 2.4.20-0.1.0-07 / compose 1.12.0-0.1.0-07(来自真实 Native 调用,非静态文字);
  2. 饼图 Tab 拖动切片值滑杆,扇区角度实时变化,状态行出现 生成成功 pie N 片 total …;值拖到 0 的切片消失;
  3. 起始角滑杆整体旋转饼图;扩展角 90…360 变出扇形盘;动画 β 滑杆按真实 makePieSliceData 的 β 语义收缩;
  4. 坐标轴 Tab 正弦/散点切换,曲线与散点全部经 probeOff 映射绘制,主/次刻度与标签随视图变化;
  5. 连点"放大 ×1.25",视图收窄、刻度变密,到 minViewExtent 后状态行 view 区间不再变化(真实钳制);缩小/平移同理受 maxViewExtent 与范围边界约束;"重置"回到全量程;
  6. 对数轴模式刻度为 0.1/1/10/100/1000,位置来自真实 LogAxisModel.computeOffset;
  7. 热力图 Tab 调采样数与分箱数,高斯团清晰,状态行出现 生成成功 histogram2d N×N samples … max/bin …;重新采样更换 LCG 种子;
  8. 链路 Tab 日志按时间倒序记录每次调用的 op、ok、耗时与响应字节数。

6.5 验证结论

图表引擎、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的图表计算会话:饼图切片语义、轴刻度生成、缩放/平移钳制、数据点映射与二维直方图分箱(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/koalaplot-core.git
cd koalaplot-core
export JAVA_HOME="F:/cmp/.tools/jdk-21.0.12.1+1"
./gradlew :compileKotlinJvm compileTestKotlinJvm                    # 交叉验证
./gradlew :compileKotlinOhosArm64 :compileKotlinOhosX64            # KLIB
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.koalaplot -m entry
hdc shell hilog | grep KoalaPlot

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让库模块真正编译成 ohosArm64 / ohosX64 KLIB 并链出动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 kotlin.mpp.applyDefaultHierarchyTemplate=false + jvm("desktop") 的组合在默认层级模板下失效——desktopMain 访问器不生成;处置是目标改 jvm() 并把源集目录 git mv 到 jvmMain / jvmTest,而不是去手工拼层级;
  4. Kotlin 2.4.20 线的 kotlin-test 把上游测试所用断言 API 标为实验,编译期 opt-in 错误;处置是 jvmTest 编译加 kotlin.test.ExperimentalKotlinTestApi,测试文件零改动;
  5. 链接完整 Compose 栈时 konanc 默认 3g 堆不够,编译器 JVM 直接原生 malloc 失败(Windows 报"页面文件太小");kotlin.native.jvmArgs=-Xmx5g(注意大写 A),16 GB 机器上 Gradle 守护进程压到 -Xmx1g、workers.max=1,一次链一个 ABI;
  6. 并发构建下偶发 Gradle build daemon has been stopped,gradlew --stop 后用 --no-daemon 重跑即恢复,不是工程问题;
  7. ohos Compose 线不发 *-desktop 子制品,jvm 目标必须双线拆分;把 ohos 坐标写到 commonMain 会让 jvm 解析失败;
  8. SplineBasedFloatDecayAnimationSpec 在 ohos 线可用(android/ios 触摸 actual 可原样照抄),这一点先验证再动手,能省掉降级 exponentialDecay 的语义偏差;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();napi 字符串先取长度再取内容(长度优先缓冲);
  11. CMake 对 KN so 用 IMPORTED_NO_SONAME TRUE + $ORIGIN rpath,避免 Windows 风格绝对路径写进 DT_NEEDED;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持空 signingConfigs,证书绝不进入 AtomGit。

8.2 已知问题

  • OHOS 无社区 Compose 线的组合宿主:@Composable API(PieChart / XYGraph / VerticalBarPlot / BulletGraphs…)编译进 KLIB 但不上屏,屏幕呈现由 ArkTS 重绘;hoverable 悬停展开、鼠标跟踪等指针特性随宿主缺失一并不可用;
  • 桥协议只覆盖计算面:布局测量(SubcomposeLayout、标签放置)、动画时序、图例排版属于组合宿主职责,不在桥内;
  • 上游 desktopTest 在鸿蒙工具链下可编译、未运行(需要桌面 Compose 测试环境与 AWT);
  • axis.category 与 autoscale 已在协议层,Demo 未单独出 Tab,验收走链路 Tab 日志;
  • android / ios / js / wasmJs 目标剥离(源文件留在树里),发布、dokka、detekt 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.koalaplot 匹配的签名。

8.3 未来优化方向

  • 跟进社区组合宿主线(@cpf-kmp-cmp/compose + ComposeArkUIViewController,lazytable 已趟通):版本体追平后把 Demo 从 ArkTS 重绘切到真实 Compose 渲染,KoalaPlotHeadless 桥保留为无 UI 场景的计算面;
  • 补齐 axis.category / autoscale 的独立 Demo Tab,桥接极坐标(PolarGraph)与子弹图计算面;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 koalaplot-core(79 文件,100% Compose)
    → 验证 ohos Compose klib 制品线(runtime/foundation/ui/material3/animation)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式)+ 依赖双线拆分(换线不换版)
    → ohosArm64 / ohosX64 KLIB + 唯一 expect 的触摸手势 actual + 库内 headless 门面
    → C ABI(@CName × 2)+ 7 op 扁平 JSON
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + Canvas 重绘
    → ArkUI 真机四 Tab 验收(饼图 / 坐标轴缩放平移 / 热力图 / 链路)

9.2 封装层次

  • 引擎层:上游原版 PieChart / XYGraph / VerticalBarPlot / BulletGraphs / HeatMapPlot / 手势 / 图例(整库编译);
  • 门面层:库内 KoalaPlotHeadless——7 op 扁平 JSON、轴会话状态、探测点映射、错误通道;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 解析、Canvas 重绘、桥接日志;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:Compose ohos klib 线补齐后,100% Compose 库的正确答案就是"整库编译 + 补一个手势 actual + 一个 headless 门面";qrose 的隔离路线是制品缺失时代的产物,路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走,源集结构跟着模板走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线留 9.x);启用默认层级模板就要接受 jvm() 的源集命名,平移目录好过手工拼层级;
  3. 没有组合宿主时,"计算真实、渲染复刻"是可验收的中间态:桥只传计算值,让每一个角度、刻度、偏移、分箱和颜色都出自 .so 内的上游原件,ArkTS 重绘只是下游消费;jvm 双线交叉验证补上"整库编译没有引入漂移"的证明,剩下的真实渲染等组合宿主线合流。

9.4 适配成果

当前适配已完成:

  • Koala Plot 0.12.1 真实图表引擎的双 ABI Kotlin/Native 编译(上游 commonMain 79 文件零重写,仅 2 个 ohos 新增源文件:手势 actual + headless 门面);
  • 鸿蒙 Compose 制品线(1.12.0-0.1.0-07)在纯 Compose 图表库上的完整落地,jvm() 交叉验证线与上游版本体完全一致(换线不换版);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号),7 op 扁平 JSON 协议含带状态的轴缩放/平移会话与数据点探测映射;
  • DevEco Stage ArkUI 四 Tab 验收页面(饼图切片语义、坐标轴刻度与缩放钳制、热力图分箱配色、版本握手与桥接日志);
  • 上游 18 个测试文件在鸿蒙工具链下编译验证(零改动)+ 双 ABI 链接(arm64 ≈28.1 MB / x86_64 ≈26.9 MB)、HAP 构建、签名、设备安装和真机运行;
  • AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

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

更多推荐