开源鸿蒙平台 CMP 三方库「OrbMotion」适配全流程
本文记录
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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(JAVA_HOME 指向 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.6.1(2.4.20 线支持 Gradle 9.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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、OrbAnimationPlayground | androidx.compose.* | 移入 composeMain,留盘不进构建 |
androidMain / iosMain | Platform actual | 平台 API | ohos / 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 方法 | 覆盖内核 |
|---|---|---|
OrbMotionVersion | orbMotionVersion() | 版本握手(真机版本行的数据来源) |
OrbMotionPresets | orbMotionPresets(req) | 九状态预设(信息卡 / Gallery 标题) |
OrbMotionFrame | orbMotionFrame(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.toml | Kotlin 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.kt | Platform actual(引擎侧唯一新增平台源文件) |
shared/src/commonMain/.../OrbFrameDigest.kt | FNV-1a 逐帧摘要(JVM/Native 一致性证据) |
shared/src/jvmTest/.../OrbEngineParityTest.kt | 12 用例交叉验证 + golden digest 表 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 3 个 C ABI、MiniJson、帧扁平化、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkTS Canvas 验收页面(分桶批绘) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | OrbState(9 枚举) | 业务语义状态(检索/排版/解题/聆听/工作/连接/编织/呼吸/塑形) |
| 引擎 | OrbPresets.forState(state) | 状态 → 模式、速度、点预算 |
| 引擎 | FrameDispatcher / FinalizeFrame | 帧步进、z 排序、不可见剔除、半径下限 |
| 引擎 | OrbFrameDigest | FNV-1a 逐帧摘要(16 位十六进制) |
| Native | OrbMotionVersion / Presets / Frame | C ABI JSON 导出 |
| N-API | orbMotionVersion() / 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 |
|---|---|
| SEARCHING | 51e0b3da4fbaca88 |
| COMPOSING | f8190da7523f22ce |
| SOLVING | 0327ca5e7e7743f4 |
| LISTENING | 6736d4ec6b36ad7c |
| WORKING | c9bcb0c4b95730f0 |
| CONNECTING | e8116e23f0014745 |
| WEAVING | a748f53ef70541a7 |
| BREATHING | 1daf2d62df803152 |
| SHAPING | 760967e84f912211 |
真机 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 功能用例
- 启动后标题下出现
v1.0.1-ohos.1 · upstream 1.0.1(来自真实 Native 调用,非静态文字); - Playground 默认 WORKING 状态,画布出现 ORBITS 光球,右上角 FPS 稳定在 57–60;
- 切换九个状态,信息卡
state → mode正确联动(如 WEAVING → BRAID),preset speed 来自 native 预设查询; - 拖动 Diameter 滑块,画布直径 120–420 平滑变化,点预算随尺寸缩放;
- 拖动 Speed 滑块至 3.0,动画加速且相位正确(speed 双重语义同时生效);
- 切换 White / Ice / Amber 配色,点线颜色即时切换;
- 信息卡 live digest 与第 6.2 节 golden 表对应状态逐一对上;
- 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:shared真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 上 KN 预构建发行版可能解压不完整(
konan/lib空但provisioned.ok已写入),linkDebugShared*报ClassNotFoundException: org.jetbrains.kotlin.cli.utilities.MainKt——删除~/.konan对应发行版目录强制重新解压即恢复,不要盲目重装工具链; - 引擎与渲染的耦合靠目录拆分解开:
OrbState里的 Compose 色彩引用必须去掉,枚举才能留在commonMain; - 鸿蒙线没有
String.format,浮点序列化用自写appendNum固定 3 位小数,同时规避科学计数法与-0; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); speed的双重语义(推进累加 + 进入引擎)缺一不可,桥接时照抄上游AnimatedThinkingOrb的原版行为;- DevEco 打开工程后生成的
example/harmonyApp/.idea/、.hvigor/、oh_modules/、entry/libs/、entry/build/要单独追加 gitignore,否则混进提交;*.so/*.hap全局忽略,同步产物绝不进入仓库; - HAP 缺当前 ABI 的 so 会报安装失败,
abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - 全仓链接统一使用 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 三条经验
- 先拆边界再选路线:库的 UI 依赖面决定了适配形态——纯数学引擎走「真编译 + Canvas 重写渲染」,比把整个 Compose 渲染栈拖上鸿蒙更小、更可验证;路线选择必须基于当天的源码依赖盘点;
- 工具链问题先查发行版完整性:
MainKt找不到这类报错先看~/.konan对应目录的konan/lib是否为空、provisioned.ok是否残留,删目录重新解压比重装工具链快得多; - 一致性证明要可机器判定:双编译目标(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。
参考文档
更多推荐


所有评论(0)