开源鸿蒙平台 CMP 三方库「Rive-CMP」适配全流程
本文记录
Rive-CMP接入 OpenHarmony 的完整过程,覆盖 Rive 官方运行时盘点、鸿蒙 Kotlin 工具链钉版、库 API 壳的真编译与 expect/actual 边界、ohosArm64/ohosX64目标、rive-cpp C++ 引擎的 OHOS NDK 交叉编译(premake +ohos_dev分支)、Kotlin/Native 动态库、C ABI/N-API 桥接、EGL/GLES 渲染、ArkUI 真机页面、HAP 构建和真机验收。
Rive-CMP(dev.muazkadan:rive-cmp)是把 Rive 动画运行时包成 Compose Multiplatform 统一 API 的包装库:Android 端嵌app.rive:rive-android的RiveAnimationView,iOS 端经 SPM cinterop 调rive-ios,Web 端用@rive-app/canvas,桌面端走自研 JNI 桥接 rive-cpp 并用 Skia CPU 光栅渲染进ImageBitmap,对外暴露一套CustomRiveAnimation/rememberRiveComposition/RiveViewModelInstanceAPI。本次适配与社区已有路线有本质区别:qrose / haze / PreCompose 走"零依赖内核提取",KStore 走"kotlinx 制品线补齐后的真编译整库",而 Rive-CMP 是社区第一个引擎在 C++ 而非 Kotlin 的 CMP 库适配——它的公共 API 下没有可提取的 Kotlin 引擎,每个平台消费的都是预编译原生运行时。因此本次确立"双轨"形态:Kotlin 轨真编译 API 壳(上游本来就是 Kotlin 2.4.20 源码,与鸿蒙工具链2.4.20-0.1.0-07同代零降级,全库仅 3 处 expect、只补 3 个 actual),引擎轨交叉编译 rive-cpp(Rive 官方没有 OpenHarmony 运行时,用 giteeyp9522/rive-runtime的ohos_dev分支 + OHOS NDK 编出 rive / pls_renderer / harfbuzz / sheenbidi / yoga 五个静态库,EGL + GLESv3 在 XComponent 上渲染),两轨在同一个 NAPI 模块libohos_rive.so汇合,真机上同时完成 Kotlin 桥握手与 C++ 引擎动画播放。项目地址: AtomGit/oh-tpc/Rive-CMP.git
上游仓库: muazkadan/Rive-CMP
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Rive-CMP 为 Compose Multiplatform 应用提供"一套 API 播放 Rive 动画"这件事的完整答案:CustomRiveAnimation 一个组件覆盖 url / byteArray / composition 三种载入,rememberRiveComposition 返回带 loading / error 状态的组合结果,RiveComposition 暴露 setNumberInput / setBooleanInput / setTriggerInput 与 pause / reset / stop,实验性的 RiveViewModelInstance 把数据绑定属性(number / string / boolean / color / enum / trigger)按路径读写。KMP 项目把这些声明放在 commonMain,由 Android、iOS、Web、Desktop 各自的平台运行时兑现。OpenHarmony 应用同样需要 Rive 动画能力,但上游构建(Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明两件事:公共 API 壳能在 OpenHarmony ARM64 设备上完成编译链接,以及 Rive 动画引擎本身能在鸿蒙上跑起来。适配需要同时验证 Kotlin 库、C ABI、N-API、rive-cpp 交叉编译、EGL 渲染链路、ArkUI 页面和 HAP 交付。
| 障碍 | 具体问题 |
|---|---|
| 引擎缺失 | Rive 官方运行时只覆盖 Flutter / iOS / Android / Web / C++,没有 OpenHarmony;rive-flutter 的 issue #518 明确记录鸿蒙缺 librive_native.so。必须自己把 rive-cpp 交叉编译出来。 |
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.1(com.android.kotlin.multiplatform.library + iOS SPM + wasm)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | commonMain 依赖 Compose(runtime / foundation / material3 / ui)与 kotlinx-coroutines,每一项都要有 ohos 变体才能解析;jvm 对照线的 material3 在 Maven Central 上是独立版本线。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON;Kotlin 侧的 RiveComposition 语义要跨到引擎侧。 |
| 交付链路复杂 | rive 静态库、premake 工具链、shader 生成、.so 三件套、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:API 壳的真编译(commonMain 零改动 + 3 个 ohos actual)、rive-cpp 的 OHOS 交叉编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 双桥接层。动画语义由 C++ 引擎原版实现负责,Kotlin 侧持有 spec 模型与枚举契约,ArkTS 只负责调用 Native、驱动渲染和展示结果。
1.2 库提供的能力
Demo 提供一个真实的 Rive 动画会话,对应双轨的全部入口:
- 动画播放:
RiveAnimationView组件加载 rawfile 里的 4 个上游示例.riv(alligator_swipe / mode_switch / pull_to_refresh_use_case / data_binding_test_triggers),C++ rive 引擎经 EGL/GLES 的 pls 渲染器在 XComponent surface 上逐帧渲染——不是 ArkTS 近似绘图,是 rive-cpp 原版引擎; - 参数面镜像:fit(六档)/ alignment(九宫格)/ autoPlay 实时切换,可选值列表来自 Kotlin 库
RiveFit/RiveAlignment枚举经RiveCmpDispatch下发的引擎映射,不是静态文案; - 版本握手:
rivecmpVersion()返回{"ok":true,"fork":"0.6.0-ohos.1",...},来自真实 Kotlin/Native 调用; - spec 模型:
op=spec走库的RiveCompositionSpec.url / byteArray真工厂并回显。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
fork / upstream / kotlin / compose / targets | 版本握手:fork 版本、上游版本、工具链线、ohos 目标。 |
values[].name / ordinal / engine / x / y | op=fit 的 RiveFit → rive::Fit 引擎码(源自 include/rive/layout.hpp);op=alignment 的 RiveAlignment → rive::Alignment(x,y)。 |
kind / url / length / head | op=spec 的载入类型、URL 回显或字节数与文件头十六进制。 |
引擎侧完整能力(多画板、线性动画 play/pause/loop/direction/speed、状态机 bool/number/trigger 输入、嵌套输入、ViewModel 数据绑定属性、文本运行替换、事件回调)全部随 NAPI 绑定层进入 libohos_rive.so,ArkTS 侧由 RiveAnimationView 封装调用。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | library 的全部 commonMain 文件原样编译,不提取、不重写;全库 expect 只有 3 处,ohos 补第 4~6 个 actual(dispatcher / headless RiveComposition / 占位 CustomRiveAnimation,共 3 个文件)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample / androidSample 与 iOS SPM / AGP / 发布插件留盘不进构建。 |
| 引擎线 | rive-cpp 走 gitee yp9522/rive-runtime 的 ohos_dev 分支(oh-tpc ohos_rive 移植同源),premake --for_ohos + OHOS NDK 交叉编译五个静态库,HAP 构建期由 CMake 自动触发。 |
| 桥接稳定 | 2 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI;KN 字符串经 DisposeString 归还运行时。 |
| 依赖纪律 | Kotlin 版本主体与上游完全一致(2.4.20),只把仓库换到 ohos 制品线(换线不换版);上游源码零改动。 |
| UI 完整 | 单页覆盖全部入口:4 个 .riv 渲染、fit / alignment / autoPlay 切换、握手、映射表、spec 回显、分层说明。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙社区 Compose 线(
1.12.0-0.1.0-07)能把@Composable编进 klib 但没有组合宿主,CustomRiveAnimation在 ohos 上运行期不渲染;动画宿主由 ArkTSRiveAnimationView承担,参数面与 Kotlin API 一一对应。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 expect 边界、确认引擎不可达、确定双轨交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双线 Compose 依赖
第 3 阶段:桥协议建模 ── fit/alignment/spec/version JSON 契约、错误通道
第 4 阶段:原生桥接 ── rive-cpp ohos_dev 交叉编译、@CName C ABI、C++ N-API、CMake 集成
第 5 阶段:ArkUI 宿主 ── RiveAnimationView 接入、桥握手注入、三 Tab 验收页
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
library/ 上游 API 壳(进 ohos 构建,jvm + ohosArm64 + ohosX64)
library/src/commonMain/ 9 个文件零改动:CustomRiveAnimation(expect×3) / RiveComposition(expect) /
RiveCompositionSpec / RiveCompositionResult / RiveViewModelInstance /
RiveFit / RiveAlignment / defaultAsyncDispatcher(expect)
library/src/ohosMain/ 新增:3 个 actual(全仓仅有的新 Kotlin 源)
runtime-macos-arm64/ 上游 dylib jar 模块(jvm target 的 runtimeOnly,去掉发布插件)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API、rive 引擎绑定和 ArkTS 页面
submodules/rive-runtime 引擎源:gitee ohos_dev 分支(pin f27a900)
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.md / README.upstream.md fork 主页 / 上游原文保存
sample/ androidSample/ 上游 Compose 示例,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library、:runtime-macos-arm64 与 :example:nativeApp,rootProject.name = "ohos_rivecmp"。上游的 AGP / iOS SPM(spmForKmp)/ vanniktech 发布 / dokka 插件全部随根构建替换剥离——本 fork 不发 Maven Central,上游构建文件原样保存为 *.upstream。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 钉死 Adoptium) |
| Compose(ohos 线) | 社区鸿蒙线 1.12.0-0.1.0-07(klib,无 desktop 子制品) |
| Compose(jvm 线) | 上游原线 1.12.1 + material3 1.9.0(Maven Central) |
| coroutines | 1.11.0-0.1.0-07(ohos 制品线) |
| Gradle | 9.8.0(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作对照编译) |
| rive 引擎 | rive-cpp,gitee yp9522/rive-runtime 分支 ohos_dev @ f27a900 |
| 渲染 | rive_pls_renderer,EGL + GLESv3,XComponent surface |
| OHOS NDK | DevEco 自带 native(API 26 sysroot),经无空格 junction E:\ohos-ndk 引用 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.rivecmp |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
library | 9 | Compose 壳 + expect/actual,平台 actual 消费原生运行时 | 整体编译(jvm + 双 ohos,源码零改动) |
runtime-macos-arm64 | 3 | java-library 打包 macOS arm64 dylib | 留进构建(jvm 线 runtimeOnly) |
sample / androidSample | 40+ | AGP / iOS Xcode / js / wasm 宿主工程 | 留盘不进构建 |
expect 声明全库只有 3 处:CustomRiveAnimation(三个重载的 @Composable expect fun)、RiveComposition(expect class,输入与播放控制)、defaultAsyncDispatcher(internal expect val)。上游给了 Android / iOS / js / wasmJs / jvm 五套 actual,每一套都站在平台运行时上——Android 嵌 RiveAnimationView(AndroidView + rive-android AAR),iOS 走 Swift RiveAnimationController(SPM cinterop),js/wasmJs 渲染进 HTML canvas,jvm 用 35 个 JNI 包装文件持裸指针驱动 rive-cpp。ohos 补第六套 actual,这就是整个 fork 在 Kotlin 侧的全部新增源码:
// library/src/ohosMain/kotlin/dev/muazkadan/rivecmp/utils/DefaultAsyncDispatcher.ohos.kt
// Dispatchers.IO is internal in the HarmonyOS coroutines rebuild (no dedicated
// IO pool under the FFRT scheduler), so this mirrors the js/wasmJs actuals.
internal actual val defaultAsyncDispatcher: CoroutineDispatcher = Dispatchers.Default
RiveComposition 的 ohos actual 是 headless 模型:持有 spec、记录 setNumberInput / setBooleanInput / setTriggerInput 的最后一次写入与 pause / stop 状态,reset 清空——ArkTS 宿主把这份状态回放到 C++ 引擎。CustomRiveAnimation 三个 actual 为占位空实现(无组合宿主,编译即验收),文档注释明确渲染路径。上游 jvmMain 的 35 个 JNI 文件与 commonMain 一起进 jvm 对照编译,一个字节不动。
值得单独说明的是引擎盘点结论:这个库没有可以提取的 Kotlin 引擎。qrose 抽编码器、KStore 编译存储引擎的前提都是"引擎在 commonMain";Rive-CMP 的引擎是 C++(submodules/rive-runtime,上游 pin 在 github 073942e,国内构建机不可达)。所以适配路线必须分两轨,引擎轨的源换成 gitee yp9522/rive-runtime 的 ohos_dev 分支——这是 oh-tpc 已过审 ohos_rive 移植(华为 rive-android → OHOS,Apache-2.0)构建所用的同源分支,premake 配置内置 --for_ohos,依赖(harfbuzz / sheenbidi / yoga / premake-ninja)全部走 dependency.gitee() 拉取,整条链路为无 GitHub 环境设计。
1.4 页面边界
标题区 RIVE-CMP · OPENHARMONY / 版本行(rive-cmp 0.6.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64 · rive-cpp engine)
Tab 切换 动画 / 桥接 / 关于
动画 Tab RiveAnimationView(约 360vp)渲染当前 .riv
文件区 4 个 .riv 切换按钮(alligator_swipe / mode_switch / pull_to_refresh_use_case / data_binding_test_triggers)
Fit 区 RiveFit 六值切换(FILL/CONTAIN/COVER/FIT_WIDTH/FIT_HEIGHT/NONE,来自 op=fit)
Alignment 区 RiveAlignment 九值切换(九宫格,来自 op=alignment)
播放区 autoPlay Toggle
桥接 Tab 版本握手 JSON、fit→rive::Fit 引擎码表、alignment→(x,y) 表、op=spec 回显
关于区 双轨分层图、AtomGit / 上游 / 社区链接
页面启动时先调 rivecmpVersion() 做握手,再经 op=fit / op=alignment 拉取枚举映射初始化两个切换器。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手之一)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose / coroutines 制品。用 .module 元数据逐个验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
variants: ohosArm64Api-published / ohosX64Api-published / …(runtime/foundation/material3/ui 齐全)
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
variants: 同上(jvm + ohos 双线齐全)
三个关键结论:其一,Compose 鸿蒙线版本主体 1.12.0 与上游 1.12.1 同代,commonMain 里一行依赖坐标都不用改;其二,该线只发 klib 不发 *-desktop 子制品,jvm 对照线必须从 Maven Central 的上游线解析;其三,org.jetbrains.compose.material3:material3:1.12.1 在 Maven Central 上不存在——JB material3 从 1.9.0 起走独立版本线,1.9.0 才是与 compose 1.12.x 桌面线配对的稳定版(koalaplot 的 ohos fork 用同一配对)。jvm 线第一次解析就卡在 material3,钉 1.9.0 即解。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区此前批准的钉法有两代:qrose / haze 用的 2.2.21-1.0.0(必须降 Gradle 8.14.1),KStore 确立的 2.4.20-0.1.0-07(Gradle 可留 9.x)。Rive-CMP 上游源码本来就是 Kotlin 2.4.20 编的,与鸿蒙工具链 2.4.20-0.1.0-07 同代,源码级兼容成本为零;Gradle 方面 2.4.20 线支持 9.x(FileKit 0.16.0 先例),上游 wrapper 9.8.0 原样沿用——两条工具链线的 Gradle 经验不能互相套用,只有 2.2.21 旧线才必须降 8.14.1。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin toolchain must resolve first, otherwise ohosArm64()
// fails with "Unresolved reference" at configuration time.
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_rivecmp"
include(":library")
include(":runtime-macos-arm64")
include(":example:nativeApp")
2.3 编译真实引擎(Kotlin 轨)
library/build.gradle.kts 收敛为纯 KMP 模块(上游原文存 build.gradle.kts.upstream)。关键手法是依赖按源码集分线声明——commonMain 不声明任何外部依赖,ohosMain 挂鸿蒙线、jvmMain 挂上游线,“换线不换版”:
kotlin {
applyDefaultHierarchyTemplate()
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi")
}
jvm {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs.add("-Xjdk-release=11")
}
}
ohosArm64()
ohosX64()
sourceSets {
commonTest.dependencies { implementation(kotlin("test")) }
ohosMain.dependencies {
implementation(libs.compose.runtime) // 1.12.0-0.1.0-07
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.kotlinx.coroutines.core) // 1.11.0-0.1.0-07
}
jvmMain.dependencies {
implementation(libs.compose.runtime.jvm) // 1.12.1 (Maven Central)
implementation(libs.compose.foundation.jvm)
implementation(libs.compose.material3.jvm) // 1.9.0 独立版本线
implementation(libs.compose.ui.jvm)
implementation(libs.compose.desktop.jvm)
runtimeOnly(project(":runtime-macos-arm64"))
}
jvmTest.dependencies { implementation(libs.compose.ui.test) }
}
}
实测:
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :library:compileKotlinJvm # BUILD SUCCESSFUL
commonMain 的 9 个文件一个没动,jvmMain 的 35 个 JNI 包装文件随 jvm 目标对照编译,只有那 3 个 ohos actual 是新的。
2.4 测试依赖的处置:jvmTest 需要 macOS dylib
上游 jvmTest(6 个测试类 + 3 个 .riv 资源)执行时经 NativeLoader.loadLibraryFromJar 加载 runtime-macos-arm64 里的 dylib——只能在 macOS arm64 上运行(上游 CI 亦然),Windows/Linux 主机会在 dylib 加载处失败。处置是两条纪律的组合:测试统一在 macOS 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件零改动保留,Windows 构建机上的对照验证落在 compileKotlinJvm(编译级),验收时以 macOS 侧 desktopTest 为准。
第 3 阶段:桥协议建模
3.1 JSON 契约
dev.muazkadan.rivecmp/**(上游 API 壳,原样编译)
↓ RiveCompositionSpec 工厂 / RiveFit / RiveAlignment
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API rivecmpDispatch() / rivecmpVersion()(挂在 libohos_rive.so)
↓
ArkUI Index.ets(三 Tab)
请求与响应:
请求 : {"op":"version"}
{"op":"fit"} / {"op":"alignment"} / {"op":"capabilities"}
{"op":"spec","kind":"url","value":"https://…"}
{"op":"spec","kind":"bytes","bytesBase64":"…"}
响应 : {"ok":true,"fork":"0.6.0-ohos.1","upstream":"0.6.0","kotlin":"2.4.20-0.1.0-07",…}
{"ok":true,"values":[{"name":"CONTAIN","ordinal":1,"engine":1},…]}
{"ok":true,"values":[{"name":"CENTER","ordinal":4,"x":0.0,"y":0.0},…]}
{"ok":false,"error":"…"}
两个不对称设计:其一,fit / alignment 的引擎码不是在 ArkTS 侧手写魔法数,而是把库的枚举与 include/rive/layout.hpp 的 rive::Fit(fill=0…none=5)和 rive::Alignment(x,y)(x/y ∈ [-1,1],y=-1 为顶边)的映射关系固化在 Kotlin 桥里下发——枚举契约的唯一事实源是库本身;其二,spec 走库的公开工厂 RiveCompositionSpec.url / byteArray,字节路径回显长度与文件头十六进制,证明载入模型在真机上可用。
3.2 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("RiveCmpDispatch")
fun riveCmpDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、不支持的 op、空 kind,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 引擎线:rive-cpp 的 OHOS 交叉编译
本阶段是整个适配最重的一段。rive 用 premake5 生成 ninja 工程,ohos_dev 分支的 build/rive_build_config.lua 内置鸿蒙支持:--for_ohos 参数注册自定义 premake.tools.ohos_ndk 工具集,从 OHOS_NDK 环境变量定位 llvm / sysroot,--target=aarch64-unknown-linux-ohos,定义 RIVE_OHOS;harfbuzz / sheenbidi / yoga / premake-ninja 依赖全部经 dependency.gitee() 从 gitee 拉取。实际命令(HAP 构建期由 CMake 自动执行):
export OHOS_NDK=E:/ohos-ndk # 无空格路径(见 8.1 踩坑 1)
export PATH=<premake5>:<NDK>/build-tools/cmake/bin:$PATH
sh submodules/rive-runtime/build/build_rive.sh \
--file=./premake5_cpp_runtime.lua ohos ninja release arm64 \
--no-rive-decoders --with_rive_audio=disabled -- rive_cpp_runtime
产物落在 entry/src/main/cpp/out/arm64_release/:librive.a(约 17 MB)、librive_pls_renderer.a、librive_harfbuzz.a、librive_sheenbidi.a、librive_yoga.a;x86_64 把 arm64 换 x64 再跑一遍。Windows 主机的四个工具链坑(路径空格截断 sysroot、premake 需要 beta3 的 sanitize API、make 缺失需 shim、minify.py 依赖 ply 包)全部记录在 8.1。
4.2 三层桥接
ArkTS(RiveAnimationView / Index.ets)
│ JSON string / 组件调用
▼
C++ N-API 层(libohos_rive.so)
│ ① rive 引擎绑定(38 个 .cpp,适配自 oh-tpc ohos_rive,Apache-2.0)
│ ② rivecmpDispatch / rivecmpVersion 再导出 → const char*
▼
Kotlin/Native C ABI(libohosrivecmp.so)
│ RiveCmpDispatch / RiveCmpVersion → :library 公共 API
▼
rive-cpp 静态库(rive / pls_renderer / harfbuzz / sheenbidi / yoga)
↓
EGL + GLESv3(XComponent surface)
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制库语义,所以选择 C ABI + JSON。ArkTS 只 import libohos_rive.so 一个 so——HarmonyOS CAdapter 不会把 @CName 业务符号挂到 ArkTS,libohosrivecmp.so 必须经 NAPI 层转一手。
4.3 Kotlin/Native 导出函数
@CName("RiveCmpDispatch")
fun riveCmpDispatch(request: String): String // version / spec / fit / alignment / capabilities 一口进
@CName("RiveCmpVersion")
fun riveCmpVersion(): String // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
RiveCmpDispatch | rivecmpDispatch(req) | RiveCompositionSpec.url/byteArray 工厂、RiveFit→rive::Fit、RiveAlignment→(x,y)、API 能力面 |
RiveCmpVersion | rivecmpVersion() | fork 版本握手(真机版本行的数据来源) |
llvm-nm -D 实测 RiveCmpDispatch / RiveCmpVersion / libohosrivecmp_symbols 均为全局 T 符号。
4.4 CMake 和 N-API 集成
# rive 静态库(CMake configure 期触发交叉编译)
execute_process(COMMAND ${RIVE_RUNTIME_DIR}/build/build_rive.bat
--file=./premake5_cpp_runtime.lua ohos ninja ${CONFIG} ${RIVE_ARCH}
--no-rive-decoders --with_rive_audio=disabled -- rive_cpp_runtime ...)
add_library(ohosrivecmp SHARED IMPORTED)
set_target_properties(ohosrivecmp PROPERTIES
IMPORTED_LOCATION ${KN_SO} # entry/libs/<abi>/libohosrivecmp.so
IMPORTED_NO_SONAME TRUE)
target_link_libraries(ohos_rive PUBLIC
libace_napi.z.so libEGL.so libGLESv3.so libhilog_ndk.z.so
libnative_drawing.so libimage_source.so libpixelmap.so libnative_window.so
ohosrivecmp)
OHOS_ARCH 为空或 libohosrivecmp.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++_shared.so 三件套同目录,dlopen 时互相可见。N-API 回调统一三步:napi_typeof 校验必须是 string、调 KN 函数并包装返回;KN 返回的字符串必须经 libohosrivecmp_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。类型声明追加进 types/libohos_rive/index.d.ts:
export const rivecmpDispatch: (request: string) => string;
export const rivecmpVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 引擎视图接入与握手注入
import { RiveAnimationView } from '../rive_runtime/runtime/RiveAnimationView';
import { rivecmpDispatch, rivecmpVersion } from 'libohos_rive.so';
aboutToAppear(): void {
this.versionRaw = rivecmpVersion(); // 版本握手:真实 Native 调用
this.fits = (JSON.parse(rivecmpDispatch('{"op":"fit"}')) as FitResponse).values;
this.alignments = (JSON.parse(rivecmpDispatch('{"op":"alignment"}')) as AlignmentResponse).values;
}
动画区用参数面镜像库 API:riveResource: '@rawfile/…'、riveFit: fitString、alignmentIndex、riveAutoPlay——RiveAnimationView 内部创建 XComponent(SURFACE 型),经 EGL context + pls 渲染器逐帧驱动 rive 引擎。
5.2 页面交互
动画 Tab 自上而下:RiveAnimationView(360vp)、4 个 .riv 切换按钮、fit 六值按钮组、alignment 九值按钮组、autoPlay Toggle,每行都有小字标注当前值与来源(“Fit (RiveFit from the Kotlin library)”)。桥接 Tab 展示握手 JSON、fit→引擎码表、alignment→(x,y) 表与 op=spec 回显,全部等宽字体块。关于 Tab 是双轨分层图与 AtomGit / 上游 / 社区链接。
5.3 对比度和错误边界
主色深青绿 #0A5F59,选中态按钮白字、未选中浅灰底深灰字;ok:false 时错误原文进对应区块;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进 fitError 展示。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosrivecmp)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 2(spec/fit/alignment/version/capabilities)
│ └── MiniJson.kt 扁平 JSON(继承 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.rivecmp、versionName 0.6.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的 KN so + libc++(提交入库)
├── src/main/cpp/
│ ├── CMakeLists.txt rive 交叉编译 + 绑定层 + KN IMPORTED + $ORIGIN rpath
│ ├── napi_init.cpp ohos_rive 注册表 + 本仓桥导出挂载
│ ├── napi_rivecmp_bridge.{h,cpp} 桥 NAPI 包装(DisposeString 纪律)
│ ├── include/libohosrivecmp_api.h KN 导出符号手写头
│ ├── src/ + include/ 引擎绑定层(38 .cpp + 40 .h,版权保留)
│ ├── premake5_cpp_runtime.lua rive premake 聚合构建
│ ├── out/<arch>_release/ rive 静态库(gitignore,按需重建)
│ └── types/libohos_rive/ 引擎类型声明 + 桥声明
├── src/main/ets/rive_runtime/ RiveAnimationView ArkTS 运行时(66 个 .ets)
├── src/main/ets/pages/Index.ets 三 Tab 验收页面
└── src/main/resources/rawfile/ 上游 sample 的 4 个 .riv
6.2 构建、签名和安装
./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 中勾选自动生成,证书写入本机不进入仓库(仓库内 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.rivecmp -m entry
四、完整代码对照
4.1 整体架构
dev.muazkadan.rivecmp/**(上游 API 壳原样编译)
│ CustomRiveAnimation / RiveComposition / Spec / RiveViewModelInstance 模型
Bridge.kt + MiniJson(ohos 轨)
│ RiveCmpDispatch / RiveCmpVersion
libohosrivecmp.so(Kotlin/Native,双 ABI)
↕ N-API 再导出
libohos_rive.so(引擎轨)
│ ohos_rive 绑定层 + rive-cpp 静态库 + EGL/GLES pls 渲染
RiveAnimationView.ets / Index.ets
│ 参数面镜像 + JSON parse + @State 更新
▼
ArkUI 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_rivecmp、sample/androidSample 移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + Compose 双线(ohos 1.12.0-0.1.0-07 / jvm 1.12.1+material3 1.9.0)+ coroutines 钉版 |
build.gradle.kts(根) | 版本 0.6.0-ohos.1、全局禁用 Ohos*Test* 任务 |
library/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,依赖按源码集分线 |
library/src/ohosMain/** | 3 个 actual(dispatcher / headless RiveComposition / 占位 composable,全仓唯一新增 Kotlin 源) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、5 个 op、MiniJson、错误通道 |
harmonyApp/.../cpp/CMakeLists.txt | rive 交叉编译触发、系统库链接、KN IMPORTED、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../cpp/napi_rivecmp_bridge.cpp | 桥 N-API 导出、参数检查、DisposeString |
| `harmonyApp/…/cpp/src | include/` |
harmonyApp/.../ets/rive_runtime/ | RiveAnimationView 运行时(XComponent + EGL + 帧回调 + 状态机输入) |
harmonyApp/.../ets/pages/Index.ets | ArkUI 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(17 节) |
.gitmodules | rive-runtime 指向 gitee ohos_dev 分支(pin f27a900) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 库 | RiveCompositionSpec.url / byteArray | 载入模型工厂(桥 op=spec 真实调用) |
| 库 | RiveFit / RiveAlignment | 枚举契约(桥下发引擎码,ArkTS 切换器消费) |
| 库 | RiveComposition(ohos actual) | headless:记录输入写入与播放命令,供宿主回放 |
| 引擎 | rive::Imported / Artboard / StateMachineInstance | .riv 导入、画板、状态机与 bool/number/trigger 输入 |
| 引擎 | rive_pls_renderer + EGL | GLES 逐帧渲染到 XComponent surface |
| Native | RiveCmpDispatch / RiveCmpVersion | C ABI JSON 导出 |
| N-API | rivecmpDispatch(req) / rivecmpVersion() | 向 ArkTS 暴露库桥 |
| ArkTS | RiveAnimationView({...}) | 动画宿主,参数面镜像 CustomRiveAnimation |
4.4 ArkTS 与 Kotlin 的边界
const version = rivecmpVersion();
// {"ok":true,"fork":"0.6.0-ohos.1","upstream":"0.6.0",...}
const fits = (JSON.parse(rivecmpDispatch('{"op":"fit"}')) as FitResponse).values;
// [{name:'CONTAIN', ordinal:1, engine:1}, ...] —— engine 即 rive::Fit 枚举值
RiveAnimationView({
riveResource: '@rawfile/alligator_swipe.riv',
riveFit: 'contain', // 由上面的 name 映射而来
alignmentIndex: 4, // CENTER,来自 op=alignment 的 ordinal
riveAutoPlay: true,
})
private fun fitOp(): String {
val engineCode = mapOf(
RiveFit.FILL to 0, RiveFit.CONTAIN to 1, RiveFit.COVER to 2,
RiveFit.FIT_WIDTH to 3, RiveFit.FIT_HEIGHT to 4, RiveFit.NONE to 5,
)
val items = RiveFit.entries.joinToString(",") { fit ->
"""{"name":"${fit.name}","ordinal":${fit.ordinal},"engine":${engineCode[fit] ?: -1}}"""
}
return """{"ok":true,"values":[$items]}"""
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;fit / alignment 的数值含义由 Kotlin 侧一次定义、两侧共用。
五、关键决策说明
决策 1:双轨拆分而不是等待 Compose 宿主或提取"内核"
社区 Compose 鸿蒙线能把 @Composable 编进 klib 但没有组合宿主;同时这个库没有可提取的 Kotlin 引擎(引擎是 C++)。硬等宿主 = 交付无限期;硬提内核 = 无物可提。双轨把两个不可解拆成两个可解:Kotlin 轨真编译 API 壳证明库的可移植性,引擎轨交叉编译 rive-cpp 证明动画能力,ArkTS 宿主用与 CustomRiveAnimation 一一对应的参数面承接渲染。
决策 2:工具链钉 2.4.20-0.1.0-07,Gradle 9.8.0 原样沿用
上游源码本来就是 Kotlin 2.4.20,与鸿蒙工具链同代,零降级零改动;Gradle 经验按工具链线区分(2.4.20 线留 9.x,FileKit 先例),不必像 2.2.21 旧线那样降到 8.14.1。jvm 对照线沿用 Maven Central 上游原线(compose 1.12.1),material3 单独钉 1.9.0——“换线不换版”。
决策 3:引擎 pin 从 github 切到 gitee ohos_dev
上游 pin 的 073942e 在 github,国内构建机不可达;gitee yp9522/rive-runtime 的 ohos_dev 分支是 oh-tpc ohos_rive 移植的同源构建分支(premake 内置 --for_ohos,依赖全走 gitee),gitlink 记录 f27a900。.riv 文件格式向前兼容,上游 sample 的 4 个文件真机全部可播;版本差(ohos_dev 为 rive-android 10.x 世代 vs 上游 11.x)作为已知问题记录。
决策 4:NAPI 绑定适配 ohos_rive 而不是自写引擎层
oh-tpc 的 ohos_rive 是华为已过审的 rive-android → OHOS 移植(Apache-2.0):38 个绑定 .cpp、EGL/PLS 双线程态、完整 RiveAnimationView ArkTS 组件。自写引擎层意味着重做一年前别人做过的 NAPI 全套。适配保留版权声明,只在 napi_init.cpp 注册表末尾挂本仓桥导出——引擎与库桥同住 libohos_rive.so,ArkTS 一站 import。
决策 5:编译级 JVM 对照 + 上游源码零改动
引擎逻辑无法在 JVM 复跑(引擎在 HAP 里),但 API 壳可以::library:compileKotlinJvm 把 commonMain + 35 个 jvmMain JNI 文件对上游原线编译,证明"源码零改动 + 换线"成立;运行级 desktopTest 留给 macOS 侧。适配期间上游 Kotlin 源文件一个字节未动,这是"没漂"的先决条件。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)构建机、JDK 21(Adoptium)、DevEco Studio 5.1.0 Release(自带 node / hvigor / ohpm / OHOS NDK)、premake5 5.0.0-beta3、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :library:compileKotlinJvm
上游 commonMain(9 文件)+ jvmMain(35 个 JNI 包装)对 Maven Central 上游线(compose 1.12.1 / material3 1.9.0)编译通过,源码零改动。运行级 :library:jvmTest(6 测试类 + 3 个 .riv 资源)需要 macOS arm64 dylib,按 §2.4 处置留给 macOS 侧执行。
6.3 HAP 验证
:library:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:library:compileKotlinJvm: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: RiveCmpDispatch / RiveCmpVersion / libohosrivecmp_symbols 均为全局 T 符号
rive premake + ninja(双 ABI): [714/714] librive_cpp_runtime.a 等五静态库编出
llvm-nm -C libohos_rive.so: 3212 个 rive:: 引擎符号 + 5400+ 绑定符号 + RiveCmpBridge* 桥函数
CompileArkTS: finished(10 处 arkts-no-any-unknown 显式标注后)
BuildNativeWithNinja / ProcessLibs / DoNativeStrip: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 70 MB,内含双 ABI 各三件套(libohos_rive.so 约 14 MB / libohosrivecmp.so 约 19 MB / libc++_shared.so 约 1.3 MB,均 strip 后)与 rawfile 的 4 个 .riv。
6.4 功能用例
- 启动后标题区出现
RIVE-CMP · OPENHARMONY与版本行rive-cmp 0.6.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64 · rive-cpp engine; - 动画 Tab 默认播放
alligator_swipe.riv——C++ rive 引擎经 EGL/GLES 渲染,鳄鱼角色动画连续播放,帧率流畅(见 7.1 效果图); - 切换其余三个
.riv(mode_switch / pull_to_refresh_use_case / data_binding_test_triggers)均正常加载播放; - Fit 从
CONTAIN切到FILL,画面即时从"完整可见"变为"铺满裁切",再切COVER/FIT_WIDTH等各档语义正确——引擎码来自 Kotlin 桥op=fit; - Alignment 九宫格切换,画板对齐位置随 (x,y) 变化;
- autoPlay 关闭后切换
.riv,动画不再自动播放; - 桥接 Tab 顶部为
rivecmpVersion()握手 JSON("fork":"0.6.0-ohos.1"、"kotlin":"2.4.20-0.1.0-07"、"targets":["ohosArm64","ohosX64"]),来自真实 Native 调用而非静态文字; - fit 表逐行显示
name / ordinal / engine,alignment 表逐行显示name / ordinal / x / y,与include/rive/layout.hpp的枚举值一致; op=spec的 URL 回显 AtomGit 仓库地址,kind=bytes路径回显字节数与文件头十六进制。
6.5 验证结论
API 壳、C ABI、N-API、rive-cpp 交叉编译、EGL 渲染链路、ArkUI 页面和 HAP 已完成全链路验证。应用在鸿蒙真机上成功安装并进入前台,动画 Tab 展示了 C++ rive 引擎的原版渲染(非近似绘图),桥接 Tab 展示了 Kotlin/Native 库桥的真实握手与枚举契约——双轨各自的可信度都有真机证据(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Rive-CMP.git
cd Rive-CMP
export JAVA_HOME="/path/to/jdk-21"
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # klib
./gradlew :library:compileKotlinJvm # jvm 对照编译
./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.rivecmp -m entry
hdc shell hilog | grep -E "RiveCmp|RiveAnim"
八、遗留问题与改进方向
8.1 踩坑复盘
- DevEco SDK 默认装在带空格的路径(
…\DevEco Studio\…),premake 生成的 ninja 命令行对--sysroot=不加引号,空格直接把参数截断成Studio\sdk\…——建无空格 junction(E:\ohos-ndk)并让OHOS_NDK指过去; - premake 5.0.0-alpha16(2021)没有
sanitizeAPI,rive 构建配置在 asan 过滤块里无条件调用它,脚本加载即报attempt to call a nil value (global 'sanitize')——升到 5.0.0-beta3; - Windows 没有 GNU make,而 pls 渲染器的 shader 处理(默认目标
minify)靠 Makefile 驱动;看清该目标只做python3 minify.py -o <out> *.glsl后,写一页 make shim(bat → python 解析-C/-j/OUT=)即可替代,Metal/D3D/SPIRV 是 ohos 用不到的额外命名目标; python3在 Windows 上可能指向商店存根,统一用py;minify.py 依赖ply包,pip install ply一次解决;- 上游 rive-runtime pin 在 github,国内不可达——子模块切 gitee
ohos_dev分支(ohos_rive移植同源),.gitmodules更新 + gitlink 记f27a900; org.jetbrains.compose.material3:material3:1.12.1在 Maven Central 不存在,material3 自 1.9.0 起独立版本线——jvm 对照线单独钉1.9.0;settings.gradle.kts里 eazytec 双仓必须排在最前,否则配置期直接Unresolved reference: ohosArm64;- 新版 DevEco SDK 的 ArkTS 编译器对
arkts-no-any-unknown收紧:ohos_rive 参考代码 10 处const x = rive.Napi_…因 d.ts 用大写BigInt注解而推断为 any,逐一补显式标注(const x: BigInt = …)即过——参考工程当年在旧 SDK 能编,属版本代差而非逻辑缺陷; PackageHap阶段报spawn java ENOENT:hvigor 打包工具要起 java 进程,PATH 必须能直接解析到java(JDK 21);git bash 下加 PATH 要用/c/...风格,混用C:\...反斜杠条目对子进程 spawn 无效;- HAP 缺当前 ABI 的 so 报安装错误
9568347;abiFilters双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可; build-profile.json5的signingConfigs仓库内留空数组,DevEco 勾选自动签名后回填material;提交前git update-index --skip-worktree保护,证书绝不入 AtomGit;- KN 返回字符串必须经
libohosrivecmp_symbols()->DisposeString归还运行时,N-API 层不能free(); - 根
.gitignore盖不住嵌套目录——harmonyApp 的.hvigor/、oh_modules/、build/、cpp/out/、cpp/dependencies/要单独追加; - rive 静态库首次全量编译约 7 分钟/ABI(约 714 个翻译单元),
out/已 gitignore,第二次起 ninja 增量。
8.2 已知问题
- 鸿蒙无 Compose 组合宿主:
CustomRiveAnimation的 ohos actual 编译通过但运行期不渲染,动画宿主由 ArkTSRiveAnimationView承担(参数面一一对应);社区出现组合宿主后可把渲染接回 Kotlin 侧; - ohos 的
RiveCompositionactual 是 headless 模型(记录输入与播放命令),与引擎之间没有直接 FFI 绑定; RiveViewModelInstance的数据绑定属性当前只在引擎绑定层可用(ArkTS 直调),未跨 Kotlin 桥——桥的capabilitiesop 已声明该 API 面,后续补op=dataBinding;- 音频关闭(
--with_rive_audio=disabled);jvmTest仅 macOS arm64 可运行; - 引擎为
ohos_dev分支(rive-android 10.x 世代),与上游 0.6.0 依赖的 rive-android 11.x 存在版本差,.riv格式向前兼容、示例实测可加载,新格式特性以真机验收为准; - android / js / wasmJs / apple 目标剥离(源文件留盘),发布、dokka 一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
RiveCmpDispatch补op=dataBinding,把 ViewModel 属性读写跨到 Kotlin 桥;- 评估 cinterop:给 ohosMain
RiveCompositionactual 直连引擎 C ABI,替代"记录—回放"; - 在 CI 中加入 ohos 编译、rive 交叉编译、ELF 符号检查(
llvm-nm验RiveCmpDispatch为全局 T)和 HAP 构建; - macOS 侧补跑
:library:jvmTest全量对照; - 模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 rive-cmp(Kotlin 2.4.20 / Compose 1.12.1)
→ 盘点结论:引擎在 C++、官方无 OHOS 运行时(rive-flutter #518)
→ 双轨定型:Kotlin 轨真编译 API 壳 + 引擎轨交叉编译 rive-cpp
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.8.0 沿用,零降级)
→ Compose 双线依赖(ohos 1.12.0-0.1.0-07 / jvm 1.12.1 + material3 1.9.0)
→ ohosArm64 / ohosX64 KLIB + 仅 3 个 actual
→ rive ohos_dev premake + OHOS NDK(junction / premake beta3 / make shim / ply 四坑)
→ C ABI(@CName × 2)+ NAPI 绑定层适配 ohos_rive
→ EGL/GLES pls 渲染 + ArkTS 参数面镜像
→ ArkUI 真机三 Tab 验收(引擎动画播放 + 库桥握手)
9.2 封装层次
- API 层:上游原版
CustomRiveAnimation/RiveCompositionSpec/RiveViewModelInstance模型与RiveFit/RiveAlignment枚举契约; - Kotlin/Native 层:双 ABI 动态库经 2 个 C ABI 输出 spec / 映射 / 版本 JSON;
- 引擎层:rive-cpp 静态库(rive / pls_renderer / harfbuzz / sheenbidi / yoga)+ ohos_rive 绑定层 + EGL/GLES 渲染;
- N-API 层:参数检查、字符串转换、Native 内存释放与引擎/库桥同模块导出;
- ArkTS 层:页面状态、握手注入、参数切换、日志与错误文本;
- DevEco 层:CMake 触发引擎交叉编译、HAP、签名、安装和真机运行。
9.3 三条经验
- 引擎型库的适配路线要先回答"引擎在哪":引擎在 commonMain 就真编译整库(KStore 路线),引擎在 C++ 就双轨拆分——Rive-CMP 证明了第二条路可行,且不必自写引擎绑定层,oh-tpc 的原生移植先例(ohos_rive)就是引擎轨的起点;
- 工具链版本跟着制品线走,Gradle 经验按线区分:上游 Kotlin 2.4.20 与鸿蒙
2.4.20-0.1.0-07同代即零降级、Gradle 9.8.0 原样沿用;2.2.21 旧线必须降 8.14.1 的结论不能平移——同一个仓库换一条工具链线,构建行为完全不同; - 双轨验收要各有真机证据:只有动画播放证明不了 Kotlin 库在跑(可能是纯 ArkTS 方案),只有握手 JSON 证明不了引擎在跑(可能是空壳)——动画 Tab 的 rive 原版渲染与桥接 Tab 的库桥握手同时成立,"Kotlin 持有 API 模型、C++ 重宿主编译引擎"才从口号变成可验收的事实。
9.4 适配成果
当前适配已完成:
- rive-cmp
0.6.0API 壳的双 ABI Kotlin/Native 编译(上游 commonMain 零改动,仅 3 个 ohos actual)与 jvm 对照编译(含 35 个 JNI 文件); - Compose 双线依赖在动画类库上的落地(ohos 线
1.12.0-0.1.0-07klib / jvm 线1.12.1+ material31.9.0); - rive-cpp(
ohos_dev@f27a900)的 OHOS NDK 双 ABI 交叉编译:librive.a(17 MB)+ pls_renderer + harfbuzz + sheenbidi + yoga; - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),引擎绑定层适配自 oh-tpcohos_rive(版权保留); - DevEco Stage ArkUI 三 Tab 验收页面(4 个
.riv引擎渲染、fit/alignment/autoPlay 切换、版本握手、映射表、spec 回显); - HAP 构建(约 70 MB,双 ABI 六件套 so + rawfile)、签名、设备安装和真机验证(见 7.1 效果图);
- 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)