本文记录 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 / RiveViewModelInstance API。本次适配与社区已有路线有本质区别: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 运行时,用 gitee yp9522/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 / yop=fit 的 RiveFit → rive::Fit 引擎码(源自 include/rive/layout.hpp);op=alignment 的 RiveAlignment → rive::Alignment(x,y)。
kind / url / length / headop=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 上运行期不渲染;动画宿主由 ArkTS RiveAnimationView 承担,参数面与 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(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)
coroutines1.11.0-0.1.0-07(ohos 制品线)
Gradle9.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 NDKDevEco 自带 native(API 26 sysroot),经无空格 junction E:\ohos-ndk 引用
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.rivecmp
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
library9Compose 壳 + expect/actual,平台 actual 消费原生运行时整体编译(jvm + 双 ohos,源码零改动)
runtime-macos-arm643java-library 打包 macOS arm64 dylib留进构建(jvm 线 runtimeOnly)
sample / androidSample40+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 方法覆盖内核
RiveCmpDispatchrivecmpDispatch(req)RiveCompositionSpec.url/byteArray 工厂、RiveFit→rive::Fit、RiveAlignment→(x,y)、API 能力面
RiveCmpVersionrivecmpVersion()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.tomlKotlin 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.kt2 个 C ABI、5 个 op、MiniJson、错误通道
harmonyApp/.../cpp/CMakeLists.txtrive 交叉编译触发、系统库链接、KN IMPORTED、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../cpp/napi_rivecmp_bridge.cpp桥 N-API 导出、参数检查、DisposeString
`harmonyApp/…/cpp/srcinclude/`
harmonyApp/.../ets/rive_runtime/RiveAnimationView 运行时(XComponent + EGL + 帧回调 + 状态机输入)
harmonyApp/.../ets/pages/Index.etsArkUI 三 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)
.gitmodulesrive-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 + EGLGLES 逐帧渲染到 XComponent surface
NativeRiveCmpDispatch / RiveCmpVersionC ABI JSON 导出
N-APIrivecmpDispatch(req) / rivecmpVersion()向 ArkTS 暴露库桥
ArkTSRiveAnimationView({...})动画宿主,参数面镜像 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 功能用例

  1. 启动后标题区出现 RIVE-CMP · OPENHARMONY 与版本行 rive-cmp 0.6.0-ohos.1 · Kotlin/Native ohosArm64/ohosX64 · rive-cpp engine;
  2. 动画 Tab 默认播放 alligator_swipe.riv——C++ rive 引擎经 EGL/GLES 渲染,鳄鱼角色动画连续播放,帧率流畅(见 7.1 效果图);
  3. 切换其余三个 .riv(mode_switch / pull_to_refresh_use_case / data_binding_test_triggers)均正常加载播放;
  4. Fit 从 CONTAIN 切到 FILL,画面即时从"完整可见"变为"铺满裁切",再切 COVER / FIT_WIDTH 等各档语义正确——引擎码来自 Kotlin 桥 op=fit;
  5. Alignment 九宫格切换,画板对齐位置随 (x,y) 变化;
  6. autoPlay 关闭后切换 .riv,动画不再自动播放;
  7. 桥接 Tab 顶部为 rivecmpVersion() 握手 JSON("fork":"0.6.0-ohos.1"、"kotlin":"2.4.20-0.1.0-07"、"targets":["ohosArm64","ohosX64"]),来自真实 Native 调用而非静态文字;
  8. fit 表逐行显示 name / ordinal / engine,alignment 表逐行显示 name / ordinal / x / y,与 include/rive/layout.hpp 的枚举值一致;
  9. 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 踩坑复盘

  1. DevEco SDK 默认装在带空格的路径(…\DevEco Studio\…),premake 生成的 ninja 命令行对 --sysroot= 不加引号,空格直接把参数截断成 Studio\sdk\…——建无空格 junction(E:\ohos-ndk)并让 OHOS_NDK 指过去;
  2. premake 5.0.0-alpha16(2021)没有 sanitize API,rive 构建配置在 asan 过滤块里无条件调用它,脚本加载即报 attempt to call a nil value (global 'sanitize')——升到 5.0.0-beta3;
  3. Windows 没有 GNU make,而 pls 渲染器的 shader 处理(默认目标 minify)靠 Makefile 驱动;看清该目标只做 python3 minify.py -o <out> *.glsl 后,写一页 make shim(bat → python 解析 -C/-j/OUT=)即可替代,Metal/D3D/SPIRV 是 ohos 用不到的额外命名目标;
  4. python3 在 Windows 上可能指向商店存根,统一用 py;minify.py 依赖 ply 包,pip install ply 一次解决;
  5. 上游 rive-runtime pin 在 github,国内不可达——子模块切 gitee ohos_dev 分支(ohos_rive 移植同源),.gitmodules 更新 + gitlink 记 f27a900;
  6. org.jetbrains.compose.material3:material3:1.12.1 在 Maven Central 不存在,material3 自 1.9.0 起独立版本线——jvm 对照线单独钉 1.9.0;
  7. settings.gradle.kts 里 eazytec 双仓必须排在最前,否则配置期直接 Unresolved reference: ohosArm64;
  8. 新版 DevEco SDK 的 ArkTS 编译器对 arkts-no-any-unknown 收紧:ohos_rive 参考代码 10 处 const x = rive.Napi_… 因 d.ts 用大写 BigInt 注解而推断为 any,逐一补显式标注(const x: BigInt = …)即过——参考工程当年在旧 SDK 能编,属版本代差而非逻辑缺陷;
  9. PackageHap 阶段报 spawn java ENOENT:hvigor 打包工具要起 java 进程,PATH 必须能直接解析到 java(JDK 21);git bash 下加 PATH 要用 /c/... 风格,混用 C:\... 反斜杠条目对子进程 spawn 无效;
  10. HAP 缺当前 ABI 的 so 报安装错误 9568347;abiFilters 双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可;
  11. build-profile.json5 的 signingConfigs 仓库内留空数组,DevEco 勾选自动签名后回填 material;提交前 git update-index --skip-worktree 保护,证书绝不入 AtomGit;
  12. KN 返回字符串必须经 libohosrivecmp_symbols()->DisposeString 归还运行时,N-API 层不能 free();
  13. 根 .gitignore 盖不住嵌套目录——harmonyApp 的 .hvigor/、oh_modules/、build/、cpp/out/、cpp/dependencies/ 要单独追加;
  14. rive 静态库首次全量编译约 7 分钟/ABI(约 714 个翻译单元),out/ 已 gitignore,第二次起 ninja 增量。

8.2 已知问题

  • 鸿蒙无 Compose 组合宿主:CustomRiveAnimation 的 ohos actual 编译通过但运行期不渲染,动画宿主由 ArkTS RiveAnimationView 承担(参数面一一对应);社区出现组合宿主后可把渲染接回 Kotlin 侧;
  • ohos 的 RiveComposition actual 是 headless 模型(记录输入与播放命令),与引擎之间没有直接 FFI 绑定;
  • RiveViewModelInstance 的数据绑定属性当前只在引擎绑定层可用(ArkTS 直调),未跨 Kotlin 桥——桥的 capabilities op 已声明该 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 RiveComposition actual 直连引擎 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 三条经验

  1. 引擎型库的适配路线要先回答"引擎在哪":引擎在 commonMain 就真编译整库(KStore 路线),引擎在 C++ 就双轨拆分——Rive-CMP 证明了第二条路可行,且不必自写引擎绑定层,oh-tpc 的原生移植先例(ohos_rive)就是引擎轨的起点;
  2. 工具链版本跟着制品线走,Gradle 经验按线区分:上游 Kotlin 2.4.20 与鸿蒙 2.4.20-0.1.0-07 同代即零降级、Gradle 9.8.0 原样沿用;2.2.21 旧线必须降 8.14.1 的结论不能平移——同一个仓库换一条工具链线,构建行为完全不同;
  3. 双轨验收要各有真机证据:只有动画播放证明不了 Kotlin 库在跑(可能是纯 ArkTS 方案),只有握手 JSON 证明不了引擎在跑(可能是空壳)——动画 Tab 的 rive 原版渲染与桥接 Tab 的库桥握手同时成立,"Kotlin 持有 API 模型、C++ 重宿主编译引擎"才从口号变成可验收的事实。

9.4 适配成果

当前适配已完成:

  • rive-cmp 0.6.0 API 壳的双 ABI Kotlin/Native 编译(上游 commonMain 零改动,仅 3 个 ohos actual)与 jvm 对照编译(含 35 个 JNI 文件);
  • Compose 双线依赖在动画类库上的落地(ohos 线 1.12.0-0.1.0-07 klib / jvm 线 1.12.1 + material3 1.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-tpc ohos_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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐