本文记录 OrbMotion 接入 OpenHarmony 的完整过程,覆盖动画库盘点、引擎/渲染边界拆分、鸿蒙 Kotlin 工具链钉版、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkTS Canvas 真机页面、HAP 构建和真机验收。

OrbMotion 是一个 Compose Multiplatform 光球(orb)动画库:九个语义状态(SEARCHING / COMPOSING / SOLVING / LISTENING / WORKING / CONNECTING / WEAVING / BREATHING / SHAPING)映射到九种粒子模式(ORBITS / GLOBE / RUBIK / WAVE / WEB / BRAID / RIBBON / RING / MORPH),内部是一个纯 Kotlin 的逐帧几何引擎——给定状态、尺寸和时间,输出一帧点(含 z 排序与不可见剔除)和线。上游把引擎与 Compose 渲染写在一起,因此本次适配与 KStore 那类"零依赖整库真编译"不同,走的是 「引擎跨、渲染重写」 路线:约 600 行纯 Kotlin 动画引擎(帧分发、几何生成、投影、帧定稿)原样编进 OpenHarmony KLIB,Compose 渲染层留在树外不进构建,ArkTS 用 Canvas 按同样的数学把点线画出来,JVM 侧用逐帧 FNV-1a 摘要(digest)钉住两个编译目标的一致性。

项目地址: AtomGit/oh-tpc/OrbMotionKMPLibrary.git

上游仓库: valentineRutto/OrbMotionKMPLibrary

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

OrbMotion 为 KMP 应用提供"思考中"光球动画:ThinkingOrb 是 Compose 入口,AnimatedThinkingOrb 用 remember + 帧时钟驱动内部引擎,OrbState 九状态对应真实业务语义(检索中、排版中、解题中、聆听中、工作中、连接中、编织中、呼吸中、塑形中),每个状态锁定一种粒子模式与预设(点预算、速度、连线规则)。引擎核心是三层流水线:FrameDispatcher 把连续时间离散成帧步进,FrameGenerators / Projectors 按模式生成与投影点集,FinalizeFrame 完成 z 从远到近排序、不可见剔除与半径下限收缩——这些全部是纯 Kotlin 数学,不碰任何 UI API。KMP 项目把引擎放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套动效,但上游构建(Kotlin / AGP / Compose 插件)不会生成 OpenHarmony 变体。

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

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游根构建(AGP + Compose 插件 + Android/iOS 示例工程)与鸿蒙工具链无法共存于一个 Gradle 构建。
引擎与渲染耦合ThinkingOrb / Playground / Gallery 直接 import Compose,拖上 compose.ui 就进不了 ohos 构建。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
帧语义跨不了桥帧时钟、speed 累加是调用侧概念,C ABI 只能同步一来一回。
一致性无法目测引擎被编译两次(JVM 与 Native),必须有一个逐帧可比对的证据链。

因此,本项目把边界放在:引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。帧几何完全由上游原版数学负责,ArkTS 只负责调 Native、解析 JSON 和把点线画到 Canvas 上。

1.2 库提供的能力

Demo 提供一个真实的动画会话,对应桥接的全部入口:

  • 版本握手:OrbMotionVersion() 返回 fork 版本号(1.0.1-ohos.1)、上游版本、目标 ABI、九状态与九模式清单——页面标题下的版本行来自真实 Native 调用而非静态文字;
  • 预设查询:OrbMotionPresets(request) 输入尺寸,返回九个状态的预设(模式、速度、点预算),供页面信息卡与 Gallery 标题使用;
  • 逐帧取数:OrbMotionFrame(request) 输入 {state, size, t, speed},返回该帧的全部几何:dots(x, y, r, alpha 四个一组)与 lines(x1, y1, x2, y2, w, alpha 六个一组),坐标原点在画布左上角,引擎已按 z 从远到近排序,依序绘制即是正确的深度顺序;
  • 一致性证明:响应带 digest 字段——引擎对整帧做 FNV-1a 摘要(坐标量化到 1/1000,与序列化精度一致),真机信息卡显示 live digest,与 JVM 交叉验证产出的 golden 表逐一对上即证明 .so 里的引擎没有漂移。

引擎侧完整能力(九状态 → 九模式映射、点预算、FrameDispatcher 步进、z 排序、不可见剔除、半径下限)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 12 项交叉验证用例,证明 ohos .so 里执行的动画逻辑与 JVM 逐位一致。

1.3 实现适配

维度要求
代码复用引擎(orbs/engine/**、OrbState、OrbFrame、OrbPresets 等)原样编译,不提取、不重写;新增源码集中在 Platform.ohos.kt actual、digest 工具与桥接层。
引擎/渲染拆分4 个 Compose 文件与 OrbSize 移入 shared/src/composeMain(留盘不进构建),commonMain 保持纯 Kotlin。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);androidApp / iosApp / sampleApp 留盘不进 settings.gradle.kts。
桥接稳定使用 3 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
UI 完整ArkTS Canvas 单页覆盖全部桥接入口:版本握手、Playground(状态/尺寸/速度/配色)与 3×3 Gallery。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkTS Canvas 页面不是 Compose Multiplatform UI 实现。上游 Compose 渲染留在 composeMain 不进构建;ArkTS Demo 用 CanvasRenderingContext2D 按 native 返回的几何把同一份数学画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点源码、确认引擎/渲染边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版
第 3 阶段:源码拆分       ── 引擎留 commonMain、Compose 进 composeMain、补 ohos actual
第 4 阶段:C ABI 桥接     ── @CName × 3、MiniJson、帧扁平化、参数校验
第 5 阶段:ArkUI 宿主     ── Canvas 渲染器、分桶批绘、Playground / Gallery
第 6 阶段:示例与验证     ── HAP、签名、真机运行、digest 一致性验收和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
shared/                                上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
shared/src/commonMain/                 纯 Kotlin 动画引擎(零 UI 依赖,原样编译)
shared/src/composeMain/                Compose 渲染层(ThinkingOrb / Gallery / Playground / OrbSize,留盘不进构建)
shared/src/ohosMain/                   新增:Platform actual
shared/src/jvmMain/                    新增:Platform actual(交叉验证用)
shared/src/jvmTest/                    新增:OrbEngineParityTest(12 用例 + golden digest)
androidApp/ iosApp/ sampleApp/         上游平台示例,留盘不进 settings
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :shared 与 :example:nativeApp,rootProject.name = "ohos_orbmotion"。上游的 AGP classpath、Compose 插件、Android/iOS 示例工程、vanniktech 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(JAVA_HOME 指向 JDK 21)
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.orbmotion
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
源码集文件依赖形态处置
commonMain(引擎)OrbState、OrbFrame、ModeOpts、OrbPresets、engine/engine/**纯 Kotlin 数学,零 UI、零 AGP整体编译(jvm + 双 ohos)
commonMain(渲染)ThinkingOrb、ThinkingOrbGallery、OrbAnimationPlaygroundandroidx.compose.*移入 composeMain,留盘不进构建
androidMain / iosMainPlatform actual平台 APIohos / jvm 各补一份 actual

expect 声明只有 1 处:expect fun getPlatform(): String。上游给了 android / ios 两个 actual,ohos 与 jvm 各补第 3、4 个:

// shared/src/ohosMain/kotlin/com/valentinerutto/orbmotion/Platform.ohos.kt
actual fun getPlatform(): String = "OpenHarmony"

引擎值得单独说明:FrameGenerators / FrameDispatcher / FrameMath / Projectors / FinalizeFrame 的全部实现都是纯函数数学——九模式的点预算、网格投影、噪声抖动、z 排序、不可见剔除不依赖任何平台 API——所以引擎侧一行平台代码都不用补。真正挡住适配的是渲染层 import 了 compose.ui,本次用目录拆分解开这个耦合。

1.4 页面边界
标题区          OrbMotion · OpenHarmony / 版本握手行(v1.0.1-ohos.1 · upstream 1.0.1)
状态栏          实时 FPS 与当前帧 dotCount
Tab 切换        Playground / Gallery
画布区          Canvas(直径 120–420 可调),九状态 chip 单选
控制区          Diameter / Speed 滑块,White / Ice / Amber 三档配色
信息卡          state → mode、preset speed、live digest、native 版本
Gallery         3×3 网格,9 个 96vp Canvas 各跑一种状态,共用同一帧循环

页面启动时先调 orbMotionVersion() 做握手,再启动 16ms 帧循环;speed 同时作用在 elapsed 累加与请求字段上(上游 AnimatedThinkingOrb 的原版语义)。

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

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

本库引擎零第三方依赖,钉版不受 kotlinx 制品线反推,直接沿用社区已过审库(blueprint / compose-continuous-rounded-corner-shape)的现行钉法:HarmonyOS Kotlin 2.4.20-0.1.0-07。额外好处:上游源码是 Kotlin 2.x 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 用 9.6.1(2.4.20 线支持 Gradle 9.x),wrapper 原样对齐。

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()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        mavenCentral()
    }
}

rootProject.name = "ohos_orbmotion"

include(":shared")
include(":example:nativeApp")

eazytec 双仓必须排在两个块的最前,否则配置期直接 Unresolved reference: ohosArm64。

2.2 编译真实引擎

shared/build.gradle.kts 收敛为纯 KMP 模块:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()                // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            implementation(composeRuntime)   // 仅编译classpath,引擎源码不 import
        }
    }
}

实测:

./gradlew :shared:compileKotlinJvm        # BUILD SUCCESSFUL
./gradlew :shared:jvmTest                 # 12 用例全绿(golden digest 产出)

引擎的全部 commonMain 文件一个没动,新增源码只有 Platform 两个 actual 与 OrbFrameDigest.kt。

2.3 KN 发行版完整性坑(本次实际踩中)

首次执行 linkDebugSharedOhosArm64 报 ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt。排查发现 ~/.konan/kotlin-native-prebuilt-windows-x86_64-2.4.20-0.1.0-07 的 konan/lib 与 konan/nativelib 为空,但 provisioned.ok 标记已写入——Gradle 认为发行版就绪,不再重新解压。处置:删除该发行版目录,让 Gradle 从缓存中的完整 zip 重新解压,链接即成功。对照 2.2.21 系列发行版(konan/lib/kotlin-native-compiler-embeddable.jar 217MB 完整在位)可确认这是解压中断留下的残缺目录,不是新版本布局变化。

第 3 阶段:源码拆分

3.1 拆分原则
commonMain   = 引擎(可在 JVM 与 Native 上逐位复现)
composeMain  = 渲染(import androidx.compose.*,留盘不进构建)
ohosMain     = Platform actual(1 行)
jvmMain      = Platform actual(1 行)

OrbState 保留在 commonMain,但去掉对 Compose 色彩类型的引用——九状态枚举、每状态的模式/速度/点预算是纯数据,属于引擎契约;OrbSize 等 Dp 相关工具移入 composeMain。git 以 rename 记录这四个文件(commonMain → composeMain,相似度 100%),历史可追溯。

3.2 逐帧一致性工具(本适配的证据链)

引擎被编译两次(JVM 与 Native),需要一个手段证明两者逐位一致。新增 commonMain/.../OrbFrameDigest.kt,对整帧做 FNV-1a 摘要,坐标量化到 1/1000(与桥接序列化精度一致),产出 16 位十六进制 digest。它不碰任何上游引擎代码,纯粹是本 fork 的验证工具。


第 4 阶段:C ABI 桥接

4.1 example/nativeApp(Kotlin/Native 动态库)
kotlin {
    applyDefaultHierarchyTemplate()
    compilerOptions { optIn.add("kotlin.experimental.ExperimentalNativeApi") }
    ohosArm64 { binaries.sharedLib { baseName = "ohosorbmotion" } }
    ohosX64  { binaries.sharedLib { baseName = "ohosorbmotion" } }
    sourceSets { ohosMain.dependencies { implementation(project(":shared")) } }
}

JSON 契约(扁平数组进出,UTF-8):

请求 : {"state":"WEAVING","size":320,"t":1.234,"speed":1.0}
响应 : {"ok":true,"state":"WEAVING","mode":"BRAID","size":320,"t":1.234,
 "presetSpeed":1.625,"dotCount":156,"lineCount":0,"engineUs":212,"digest":"a748f53ef70541a7",
 "dots":[x,y,r,alpha, ...],"lines":[x1,y1,x2,y2,w,alpha, ...]}

state 接受枚举名或序数(0…8);dots 4 个一组、lines 6 个一组;浮点固定 3 位小数;引擎已按 z 从远到近排序,ArkTS 依序绘制即是正确的深度顺序。

4.2 序列化细节

桥接层带一个 271 行自研 MiniJson(从社区已过审的 ComposePhysicsLayout 复制并改包名),遵循 cmp_qrose 政策:鸿蒙线不引入任何第三方序列化依赖。浮点用自写 appendNum 固定 3 位小数输出,规避 Kotlin/Native 没有 String.format 的问题,也避免科学计数法与 -0。

性能上复用跨帧 StringBuilder(frameBuffer),一帧 GLOBE 约 748 点 → 序列化 ~20KB,同步 C 调用毫秒级返回。

4.3 速度语义(上游原样保留)

上游 AnimatedThinkingOrb 的实现是 speed 既推进本地时间累加、又作为参数进入引擎(影响帧内相位)。桥与 ArkTS 侧原样保留这个双重语义:elapsed += dt * speed 推进累加,请求里同时带 speed 字段——两个层面缺一不可,改掉任何一个都会让动画"快了但不对"。

4.4 错误通道

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

@CName("OrbMotionFrame")
fun orbMotionFrame(request: String): String = try {
    /* 状态解析 → 引擎出帧 → 扁平 JSON */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、未知状态、缺字段,全部走同一个 ok:false 通道,ArkTS 侧把 error 原文展示到界面,不静默。

4.5 Kotlin/Native 导出函数
@CName("OrbMotionVersion")  fun orbMotionVersion(): String
@CName("OrbMotionPresets")  fun orbMotionPresets(request: String): String
@CName("OrbMotionFrame")    fun orbMotionFrame(request: String): String
C ABI 符号ArkTS 方法覆盖内核
OrbMotionVersionorbMotionVersion()版本握手(真机版本行的数据来源)
OrbMotionPresetsorbMotionPresets(req)九状态预设(信息卡 / Gallery 标题)
OrbMotionFrameorbMotionFrame(req)逐帧几何(digest + dots + lines)

第 5 阶段:ArkUI 宿主(Canvas 渲染器)

5.1 三层桥接
ArkTS
  │ JSON string(16ms 帧循环)
  ▼
C++ N-API entry(liborbmotion_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosorbmotion.so)
  │ OrbMotionVersion / Presets / Frame
  ▼
UTF-8 JSON + DisposeString
5.2 帧循环
setInterval(() => {
  const now = Date.now();
  const dt = clamp((now - this.last) / 1000, 0.001, 0.05);
  this.last = now;
  this.elapsed += dt * this.speed;
  const res = JSON.parse(orbMotionFrame(JSON.stringify(
    { state: this.state, size: this.size, t: this.elapsed, speed: this.speed }
  ))) as FrameResponse;
  this.draw(res);
}, 16);

切换 Tab、改状态、改直径时重置 elapsed 但保持计时连续;native 调用整体 try/catch,ok:false 的 error 原文上屏。

5.3 分桶批绘(性能关键)

一帧最多数百个点,若逐点拼 rgba() 字符串设置 fillStyle,ArkTS 侧会先于引擎成为瓶颈。渲染器把 alpha 量化成 32 档,同档的点合并进一条 path、一次 fill;线按(alpha 档 × 线宽档)分桶,一次 stroke 一桶。每帧设置 fillStyle / strokeStyle 的次数从 O(点数) 降到 O(32),真机稳定 57–60 FPS。

5.4 页面交互

Playground Tab 自上而下:画布(暗底 #070A12,直径随滑块 120–420 变化)、九状态横向滚动 chip、Diameter / Speed 滑块(0.25–3.0)、White / Ice / Amber 三档配色、信息卡(state → mode、preset speed、live digest、native 版本)。Gallery Tab 为 3×3 网格,9 个 96vp Canvas 各跑一种状态,共用同一个定时器。每帧右上角实时显示 FPS 与 dotCount。

5.5 严格类型与错误边界

ArkTS 严格模式下不用 any、不用无类型对象字面量:为 JSON 结构定义 interface(FrameResponse / PresetItem / VersionInfo),JSON.parse(raw) as FrameResponse 之后逐字段判 undefined 再使用。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosorbmotion)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/orbmotion/
│       ├── Bridge.kt                         @CName × 3 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
│       └── MiniJson.kt                       271 行自研 JSON(复用 ComposePhysicsLayout 实现)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.orbmotion、versionName 1.0.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/libohosorbmotion_api.h
        ├── src/main/cpp/types/liborbmotion_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkTS Canvas 验收页面
6.2 构建、签名和安装
./gradlew :shared:jvmTest                                          # 12 项交叉验证 + golden digest
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default --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.orbmotion -m entry

四、完整代码对照

4.1 整体架构

com.valentinerutto.orbmotion.orbs.engine/**(上游引擎原样编译)
    │ FrameDispatcher / FrameGenerators / Projectors / FinalizeFrame
com.valentinerutto.orbmotion.orbs/**(OrbState / OrbFrame / OrbPresets,原样编译)
    │ 九状态 → 九模式 / 点预算 / 逐帧几何
Bridge.kt + MiniJson
    │ OrbMotionVersion / Presets / Frame
liborbmotion_napi.so
    │ orbMotionVersion / Presets / Frame
Index.ets
    │ JSON parse + 分桶批绘 + @State 更新
    ▼
ArkUI Canvas 真机页面(Playground / Gallery)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_orbmotion、平台示例工程移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 钉版
build.gradle.kts(根)版本 1.0.1-ohos.1、极简插件声明
shared/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,composeMain 不进构建
shared/src/ohosMain/.../Platform.ohos.ktPlatform actual(引擎侧唯一新增平台源文件)
shared/src/commonMain/.../OrbFrameDigest.ktFNV-1a 逐帧摘要(JVM/Native 一致性证据)
shared/src/jvmTest/.../OrbEngineParityTest.kt12 用例交叉验证 + golden digest 表
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt3 个 C ABI、MiniJson、帧扁平化、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkTS Canvas 验收页面(分桶批绘)
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎OrbState(9 枚举)业务语义状态(检索/排版/解题/聆听/工作/连接/编织/呼吸/塑形)
引擎OrbPresets.forState(state)状态 → 模式、速度、点预算
引擎FrameDispatcher / FinalizeFrame帧步进、z 排序、不可见剔除、半径下限
引擎OrbFrameDigestFNV-1a 逐帧摘要(16 位十六进制)
NativeOrbMotionVersion / Presets / FrameC ABI JSON 导出
N-APIorbMotionVersion() / orbMotionPresets(req) / orbMotionFrame(req)向 ArkTS 暴露能力
ArkTS帧循环 + 分桶批绘16ms 取帧、按档合并绘制

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(orbMotionFrame(JSON.stringify(
  { state: 'WEAVING', size: 320, t: elapsed, speed: 1.0 }
)));
// res.ok === true;res.digest === 'a748f53ef70541a7'(与 JVM golden 表一致)
// 依序绘制 res.dots(4 个一组)与 res.lines(6 个一组)即得正确深度顺序
@CName("OrbMotionFrame")
fun orbMotionFrame(request: String): String {
    val req = parseRequest(request)                 // state 按名字或序数解析
    val frame = OrbFrame.generate(req.state, req.size, req.t, req.speed)
    return frame.toJson(frameBuffer)                // 扁平数组 + digest
}

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

五、关键决策说明

决策 1:引擎跨、渲染重写,而不是拖 Compose 进 ohos

引擎是纯 Kotlin 数学(九模式点预算、投影、z 排序、剔除),编译面天然干净;渲染层 import 了 compose.ui,而鸿蒙线的 Compose Multiplatform 制品对这类自绘动画库并非必需。把 4 个 Compose 文件移入 composeMain 留盘,ArkTS 用 Canvas 重写渲染,换来的是:.so 只有引擎(arm64 不足 4MB),帧数据可被 digest 逐位验证,渲染正确性由「同源几何 + 深度序」保证而不是靠移植 Compose。

决策 2:工具链钉 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.6.1

本库零第三方依赖,钉版直接对齐社区已过审库的现行标准(blueprint / compose-continuous-rounded-corner-shape 同款)。2.4.20 线支持 Gradle 9.x,上游 wrapper 对齐即可;只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),平台示例留盘

上游根构建(AGP、Compose 插件、androidApp / iosApp / sampleApp 三个示例工程)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。三个平台示例目录留盘不进 settings.gradle.kts;上游 README 原文存档为 README.upstream.md,仓库首页换为鸿蒙适配说明。

决策 4:3 个 C ABI + 扁平 JSON,异常不跨 FFI

版本握手、预设查询、逐帧取数各自成口,请求/响应都是扁平 JSON:dots 4 个一组、lines 6 个一组,不做嵌套对象——省掉 ArkTS 侧每点的对象分配,也让 digest 能直接对序列化后的数值量化。每个导出整体 try/catch,Kotlin 异常以 ok:false + error 文本过桥。

决策 5:FNV-1a digest 交叉验证

「引擎没漂」不能靠目测:OrbFrameDigest 对量化后的整帧做 FNV-1a,JVM 测试产出九状态 golden 表(size=320, t=1.5),真机信息卡显示同一算法的 live digest。两表逐一对上,即证明手机上的 .so 与 JVM 执行的是同一份数学——这是本适配区别于"只改了页面"的核心证据。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21、DevEco Studio 5.1.0 Release(5.1.0(18) SDK)、OpenHarmony 命令行工具(hvigor / llvm-nm / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :shared:jvmTest

12 项用例全部通过,覆盖:九状态 → 九模式映射、点预算等于预设网格、WEB 唯一出线、z 排序、不可见剔除与半径下限、确定性、时间/尺寸/速度对帧的影响、golden digest 稳定。九状态 digest 表(size=320, t=1.5):

状态digest
SEARCHING51e0b3da4fbaca88
COMPOSINGf8190da7523f22ce
SOLVING0327ca5e7e7743f4
LISTENING6736d4ec6b36ad7c
WORKINGc9bcb0c4b95730f0
CONNECTINGe8116e23f0014745
WEAVINGa748f53ef70541a7
BREATHING1daf2d62df803152
SHAPING760967e84f912211

真机 Demo 的信息卡展示 live digest,与上表逐一对上即证明 .so 里的引擎与 JVM 一致。

6.3 HAP 验证

:shared:compileKotlinJvm: BUILD SUCCESSFUL
:shared:jvmTest: 12 tests, 0 failed
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: OrbMotionFrame / OrbMotionPresets / OrbMotionVersion /
            libohosorbmotion_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 liborbmotion_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 7 MB,内含双 ABI 各三件套(liborbmotion_napi.so / libohosorbmotion.so / libc++_shared.so);KN so arm64 约 3.9MB、x86_64 约 3.5MB(未 strip 的 debugShared,含 Kotlin/Native 运行时)。SONAME / NEEDED 检查通过,无宿主路径泄漏。

6.4 功能用例

  1. 启动后标题下出现 v1.0.1-ohos.1 · upstream 1.0.1(来自真实 Native 调用,非静态文字);
  2. Playground 默认 WORKING 状态,画布出现 ORBITS 光球,右上角 FPS 稳定在 57–60;
  3. 切换九个状态,信息卡 state → mode 正确联动(如 WEAVING → BRAID),preset speed 来自 native 预设查询;
  4. 拖动 Diameter 滑块,画布直径 120–420 平滑变化,点预算随尺寸缩放;
  5. 拖动 Speed 滑块至 3.0,动画加速且相位正确(speed 双重语义同时生效);
  6. 切换 White / Ice / Amber 配色,点线颜色即时切换;
  7. 信息卡 live digest 与第 6.2 节 golden 表对应状态逐一对上;
  8. Gallery 页 3×3 网格九个光球同时动画,共用一个定时器不掉帧。

6.5 验证结论

动画引擎、Native 动态库、C ABI、N-API、Canvas 渲染和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的逐帧引擎会话:版本握手、九状态切换、速度/尺寸控制、配色切换与 digest 一致性闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/OrbMotionKMPLibrary.git
cd OrbMotionKMPLibrary
export JAVA_HOME="/path/to/jdk-21"
./gradlew :shared:compileKotlinOhosArm64 :shared:compileKotlinOhosX64      # klib
./gradlew :shared:jvmTest                                                  # 12 项交叉验证 + golden digest
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.orbmotion -m entry
hdc shell hilog | grep OrbMotion

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :shared 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Windows 上 KN 预构建发行版可能解压不完整(konan/lib 空但 provisioned.ok 已写入),linkDebugShared* 报 ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt——删除 ~/.konan 对应发行版目录强制重新解压即恢复,不要盲目重装工具链;
  4. 引擎与渲染的耦合靠目录拆分解开:OrbState 里的 Compose 色彩引用必须去掉,枚举才能留在 commonMain;
  5. 鸿蒙线没有 String.format,浮点序列化用自写 appendNum 固定 3 位小数,同时规避科学计数法与 -0;
  6. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  7. speed 的双重语义(推进累加 + 进入引擎)缺一不可,桥接时照抄上游 AnimatedThinkingOrb 的原版行为;
  8. DevEco 打开工程后生成的 example/harmonyApp/.idea/、.hvigor/、oh_modules/、entry/libs/、entry/build/ 要单独追加 gitignore,否则混进提交;*.so / *.hap 全局忽略,同步产物绝不进入仓库;
  9. HAP 缺当前 ABI 的 so 会报安装失败,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. 全仓链接统一使用 AtomGit,上游作者归属(GitHub 原仓库)仅以文字形式保留在 README 存档中。

8.2 已知问题

  • Compose 渲染层(composeMain)不随本 fork 发布:鸿蒙侧渲染由 ArkTS Canvas 承担,上游 Compose API 的视觉细节(如 graphicsLayer 缩放)以 Canvas 参数等价实现;
  • 帧时钟不跨 C ABI:桥是同步一次一调用,ArkTS 侧以 16ms 定时器驱动、elapsed 累加在调用侧维护;
  • android / ios 目标剥离(源文件留在树里),发布、AGP、Compose 插件一并剥离,本 fork 不发 Maven Central;
  • ArkTS 页面在部分机型上 Gallery 九画布同帧的 FPS 会低于 Playground 单画布(约 45–55),可按需把 Gallery 改为按需绘制;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.orbmotion 匹配的签名。

8.3 未来优化方向

  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 补充线段批绘的空间分桶,进一步降低 RIBBON / WEB 等多线模式的绘制开销;
  • 在 CI 中加入 ohos 编译、ELF 符号检查、digest 回归和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 把 Playground 的配色与预设持久化到本地,方便验收时快速复现场景。

九、总结

9.1 核心难点回顾

上游 OrbMotion 引擎(全部纯 Kotlin 数学)
    → 根构建替换(cmp_qrose 模式,eazytec 置前 + Kotlin 2.4.20-0.1.0-07 钉版)
    → 引擎/渲染目录拆分(commonMain 引擎 + composeMain 渲染留盘)
    → ohosArm64 / ohosX64 KLIB + Platform actual
    → C ABI(@CName × 3)+ 扁平 JSON + MiniJson
    → C++ N-API(DisposeString 纪律)
    → ArkTS Canvas 分桶批绘(16ms 帧循环)
    → FNV-1a digest 交叉验证(JVM golden 表 ↔ 真机 live digest)
    → ArkUI 真机双 Tab 验收(Playground / Gallery)

9.2 封装层次

  • 引擎层:上游原版九状态 → 九模式、点预算、帧步进、z 排序、不可见剔除;
  • Native 层:生成双 ABI 动态库并通过 3 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理帧循环、JSON 解析、分桶批绘、页面状态与错误文本;
  • 验证层:FNV-1a digest 让 JVM 与 Native 的逐帧一致性可机器判定;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先拆边界再选路线:库的 UI 依赖面决定了适配形态——纯数学引擎走「真编译 + Canvas 重写渲染」,比把整个 Compose 渲染栈拖上鸿蒙更小、更可验证;路线选择必须基于当天的源码依赖盘点;
  2. 工具链问题先查发行版完整性:MainKt 找不到这类报错先看 ~/.konan 对应目录的 konan/lib 是否为空、provisioned.ok 是否残留,删目录重新解压比重装工具链快得多;
  3. 一致性证明要可机器判定:双编译目标(JVM + Native)的适配必须留一个逐帧可比对的证据(本例是 FNV-1a digest),真机验收从"看起来在动"升级为"两边的数学逐位相同"。

9.4 适配成果

当前适配已完成:

  • OrbMotion 1.0.1-ohos.1 动画引擎的双 ABI Kotlin/Native 编译(上游引擎源码零重写,仅 Platform actual 与 digest 工具为新增);
  • 引擎/渲染目录拆分(composeMain 留盘),commonMain 保持零 UI 依赖;
  • 3 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkTS Canvas 双 Tab 验收页面(Playground / Gallery、九状态切换、速度/尺寸/配色控制、FPS 统计、live digest);
  • 12 项 JVM 交叉验证全绿(九状态 golden digest 表)+ HAP 构建(双 ABI 六件套 so,约 7MB)、真机安装与运行验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐