开源鸿蒙平台 CMP 三方库「RollingNumbers」适配全流程
本文记录
RollingNumbers接入 OpenHarmony 的完整过程,覆盖 Compose Multiplatform 字符滚动库盘点、鸿蒙工具链钉版、真实算法编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
RollingNumbers是发布在 Maven Central(com.dontsaybojio:rollingnumbers)的 Kotlin Multiplatform Compose 动画库:文本变化时逐字符沿「字符轨道」纵向滚动,像里程表、老式收银机、机场翻页牌。对外入口是RollingNumbers/CurrencyRollingNumbers两个@Composable,内核是三块纯 Kotlin——Levenshtein 列动作(源自 Robinhood ticker 的移植)、EMPTY + 列表 + 列表环绕字符轨道索引数学、带分组分隔符的金额格式化。本次适配与社区两条既有路线都有对话:qrose 的桥输出 PNG(渲染结果),KStore 的桥直接暴露完整引擎;而「滚动」是过程量,本仓的桥输出的是动画模型 JSON(每列沿哪条轨道、从哪个槽滚到哪个槽、走几步),ArkUI 只负责把位移重放出来——这是 CMP 动画类库在鸿蒙上的第三种桥形态。Compose 层(
@Composable、Animatable、TextMeasurer、DrawScope)依赖社区 CMP 1.9.2 对不上的 Compose 1.11,原样冻结在src/composeMain不参与 ohos 编译;算法层零改动编进 KLIB——「能滚不算适配,滚得和 JVM 逐位一致才算」,仓库里带 10 项 JVM 模型契约测试钉死这条底线。
项目地址: AtomGit/oh-tpc/rollingnumbers.git
上游仓库: delacrixmorgan/rollingnumbers
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
RollingNumbers 为 Compose Multiplatform 应用提供「数字在动」这一件事的完整答案:RollingNumbers(text, characterLists, animationDuration, scrollingDirection) 让任意文本逐字符滚动,CurrencyRollingNumbers(amount, decimals, currencySymbol, decimalSeparator, groupingSeparator, isCurrencySymbolInFront) 在此之上给出本地化货币组合(正负号、货币符号位置、小数位),方向由数值升降按绝对值比较自动推断。Levenshtein 算法算出「每一列是保留 / 插入 / 删除」,环绕轨道数学把「从字符 A 滚到字符 B」折算成 startIndex → endIndex,Double.format 负责防科学计数法的金额格式化。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、WASM 共享。OpenHarmony 应用的行情页、余额页、支付成功页同样需要这套动画,但上游构建(Kotlin 2.3.21 / CMP 1.11.1 / Gradle 9.5.1 / AGP 9.2.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以自己滚,却无法证明公共算法在 OpenHarmony ARM64 设备上完成编译、链接和执行——那只是用 ArkTS 手抄一份 Levenshtein。适配需要同时验证算法内核、动画模型、Native 动态库、C ABI、N-API、ArkUI 重放和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Gradle 9.5.1 / AGP 9.2.1 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21-1.0.0 线必须降 Gradle 8.14.1。 |
| Compose UI 边界 | @Composable 依赖 Compose 1.11(社区 CMP 是 1.9.2),整个渲染层编不进 ohos,必须隔离且不能假装「已在鸿蒙跑 Compose」。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 动画语义跨不了桥 | 「滚动」是过程量:帧序列耦合时长、曲线、帧率,过桥即失真;能过桥的只有起点 / 终点 / 路径。 |
| internal 可见性 | 算法类全是 internal,跨模块的桥碰不到,需要公共门面。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:算法内核的 ohos 编译、根构建替换(cmp_qrose 模式)、公共门面 RollingNumbersModel 和 C ABI/N-API 桥接层。Levenshtein、环绕轨道、格式化完全由上游原版实现负责,ArkTS 只负责调桥、解析模型和重放滚动。
1.2 库提供的能力
Demo 提供四个真实滚动会话(单页四 Tab),覆盖桥接的全部入口:
- 数字(kind=text):
source → target逐字符滚动,快捷目标0/99999/20260101/ 随机五位数,覆盖进位、借位、位数扩张与坍缩; - 货币(kind=currency):前后金额(支持负数)、小数 0-4 位、符号 $ / ¥ / € / £、符号在前 / 在后、正号开关——负号恒显
−(U+2212),组合规则与上游CurrencyRollingNumbers的 Row 结构逐项对齐; - 字符集:
number/alphabetical/alphanumeric/custom四预设,custom 默认十六进制0123456789ABCDEF,切换自动换合法样本; - 方向与速度:
Any/Up/Down× Slow 3500 / Medium 2000 / Fast 1000 ms,反转样本0123456789 → 9876543210让三种方向的路径差异肉眼可见。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文,Kotlin 异常永不跨 FFI。 |
kind / source / target | 回显类型与格式化后的前后文本(货币模式已含千分位与小数)。 |
direction / duration | 实际生效的方向与时长(钳制 200-10000 ms)。 |
sign / currencySymbol / symbolInFront / decimals | 货币符号信息,正号是否显示由 ArkTS 决定。 |
lists | 实际生效的字符轨道(桥侧解析预设后的结果,ArkTS 不手写字符集)。 |
columns[] | 逐列动画模型:index / action / current / target / list / start / end / steps / animate。 |
内核侧完整能力(Levenshtein 矩阵回溯、多列表按序匹配、环绕最短路径、格式化防科学计数法)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 10 项模型契约测试交叉验证,证明 ohos .so 里执行的算法没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | lib 全部 commonMain 文件原样编译,不提取、不重写 Levenshtein;仅新增公共门面 RollingNumbersModel 与 jvm / ohos 两份 locale actual。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample / androidApp / iosApp 留盘不进 settings,Compose 实现冻结 composeMain。 |
| 桥接稳定 | 单一 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 全仓零第三方依赖(连 kotlinx.serialization 都不用,手写 MiniJson),工具链是唯一钉版面。 |
| UI 完整 | 单页四 Tab 覆盖全部桥接入口:数字、货币、字符集、方向与速度,底部状态行 + 原始响应预览。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Compose 渲染层没有 ohos 制品,ArkTS Demo 是把桥暴露的动画模型用系统
animateTo重放出来——曲线、打断、手势都是原生的。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认算法/渲染分层、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模 ── 动画模型载荷、text/currency JSON 契约、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 裁剪窗口 + 滑动轨道重放、四 Tab 交互、状态行与响应预览
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
lib/ 上游算法库(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
lib/src/commonMain/ 算法 11 文件:levenshtein×2、model×5、ui×2、expect.kt + 新增门面
lib/src/composeMain/ 上游 Compose 实现 4 文件(冻结,不参与任何 target 编译)
lib/src/jvmMain/ 新增:expect.jvm.kt(java.text,交叉验证用)
lib/src/ohosMain/ 新增:expect.ohos.kt(Intl 默认值)
lib/src/jvmTest/ 新增:RollingNumbersModelTest(10 项模型契约)
lib/src/androidMain|desktopMain|iosMain|wasmJsMain/ 上游 actual 留盘(目标已移除,作对照)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
sample/ androidApp/ iosApp/ 上游示例,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_rollingnumbers"。上游的 AGP / Compose Multiplatform / compose-compiler / hot-reload / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central,libs.versions.toml 只剩 kotlin-multiplatform 一行。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(从上游 9.5.1 降级,2.2.21-1.0.0 线的钉法) |
| 第三方依赖 | 零(桥的 JSON 解析手写 MiniJson) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.rollingnumbers、versionName 0.4.1-ohos.1 |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 源集 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
commonMain | 10(上游) | 纯 Kotlin,零 UI、零 AGP、零第三方 | 整体编译(jvm + 双 ohos) |
composeMain | 4 | androidx.compose.*(TextStyle / Animatable / TextMeasurer / DrawScope) | 冻结隔离,不接任何 target |
androidMain 等 4 个平台源集 | 各 1 | java.text / platform.Foundation / js() 互操作 | 留盘(目标已移除) |
expect 声明全库只有 3 个 locale 函数:getDecimalSeparator / getGroupingSeparator / isCurrencySymbolInFront。上游给了 4 份平台 actual,目标换成 jvm() + ohosArm64 + ohosX64 后补 2 份新的——jvmMain 照抄上游 android 的 java.text.NumberFormat 实现(JVM 同源,交叉验证用);ohosMain 是整个 fork 唯一有「实现取舍」的源文件:
// lib/src/ohosMain/kotlin/com/dontsaybojio/rollingnumbers/expect.ohos.kt
// KN 上没有 java.text,社区工具链也没有预置系统 locale 的 cinterop,
// 返回 Intl 默认值('.' / ',' / 货币符号在前)。
// 三项都是 CurrencyRollingNumbers 的默认参数,业务可逐项覆盖;
// 真实 locale 检测放 ArkTS 层(i18n Kit),读出来传进桥即可。
actual fun getDecimalSeparator(): Char = '.'
actual fun getGroupingSeparator(): Char = ','
actual fun isCurrencySymbolInFront(): Boolean = true
真正挡住跨模块桥接的不是 expect,而是 internal 可见性:LevenshteinUtils、LevenshteinAction、CharacterListData 全是 internal,Kotlin 的 internal 是模块级可见性,example/nativeApp 碰不到。处置是新增公共门面 RollingNumbersModel(对标 cmp_qrose 的 Qrose.encodeQr):ACTION_SAME/INSERT/DELETE 常量、computeColumnActions、resolveColumn(复刻 AnimatedCharacterColumn 的 INSERT→空到目标、DELETE→当前到空、SAME 语义与环绕索引计算)、currencyDirection(按绝对值比较,保证负数金额方向正确)、ColumnAnimation 数据类(currentChar / targetChar / listIndex / startIndex / endIndex / animate / steps)。其中四个环绕索引私有函数从 AnimatedCharacterColumn.kt 逐字抽出——composeMain 里的上游文件保持原样不回改,门面文件 KDoc 注明来源,构成一份受控重复。
1.4 页面边界
标题区 RollingNumbers · OpenHarmony Demo(Kotlin/Native 动画模型 + ArkUI 渲染)
Tab 切换 数字 / 货币 / 字符集 / 方向与速度(Chip 单页切换)
回显行 source → target(桥格式化后的前后文本)
显示框 逐列裁剪窗口 + 滑动轨道(滚动动画本体)
输入区 各 Tab 的参数输入与快捷按钮
状态行 最近一次操作结果(如"生成成功 kind=currency 列数=8 direction=Up duration=2000ms")
响应预览 桥返回 JSON 的前 220 字符(灰字,真机排障用)
页面 aboutToAppear 会立刻跑一次默认数字滚动(12345 → 67890),这是功能层验收的第一段动画。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件,settings.gradle.kts 必须把 eazytec 双仓写在 pluginManagement 与 dependencyResolutionManagement 最前。工具链版本跟着依赖走——本库零第三方依赖,没有 KStore 那种「coroutines 制品线反推 2.4.20」的约束,直接采用社区已审的最低成本线 2.2.21-1.0.0;这条线的配套钉法是 Gradle 8.14.1(上游 wrapper 9.5.1 与该线不兼容,必须降),kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true 随 gradle.properties 落盘。两条工具链线的经验不能互相套用:2.2.21-1.0.0 降 8.14.1,2.4.20 线(KStore 先例)留 9.x。
// settings.gradle.kts
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_rollingnumbers"
include(":lib")
include(":example:nativeApp")
上游 settings.gradle.kts 里的 :sample、:androidApp 必须从 include 剔除(目录留盘):它们挂着 AGP 与 Compose 插件,留在构建里会顺着 version catalog 把整套 Android/Compose 工具链拉回来,配置期就炸。
2.2 编译真实算法
lib/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零第三方依赖
jvmTest.dependencies {
implementation(kotlin("test")) // 10 项模型契约测试
}
}
}
实测:
./gradlew :lib:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64 # BUILD SUCCESSFUL
算法的全部 commonMain 文件一个没动——没有改 Levenshtein 回溯、没有改环绕数学、没有改 Double.format 输出。新增的只有门面、两份 actual 和测试。
2.3 交叉验证纪律:模型契约测试
上游没有测试,无「上游测试零改动全绿」可用,所以本仓自带契约测试钉住「两端一致」:
./gradlew :lib:jvmTest # 10 项全绿
| 用例 | 钉住什么 |
|---|---|
| 等长数字段全 SAME | Levenshtein 对等长段的最优编辑语义 |
| 分隔符不滚动 | 不在轨道里的字符(, .)保持原地替换 |
'1'→'6' 走 (2,7) | 环绕轨道索引(EMPTY 占 0 号槽,字符 d 在 d+1) |
'9'→'0' 走 (10,11) | Any 方向的最短环绕路径 |
'0'→'9' Down 走 (11,10) | Down 方向的环绕方向 |
| INSERT 从 EMPTY 槽出发 / DELETE 滚向头部 EMPTY | 插入删除的槽位语义 |
currencyDirection 绝对值比较 | 负数金额方向正确 |
Double.format 输出契约 | 1234567.891 → "1,234,567.89" |
| 轨道外字符回退列表 | listIndex = -1 与 EMPTY+双字符回退 |
改算法前先跑它——这是「桥返回的模型 = Compose 端行为」的唯一自动化闸门。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.dontsaybojio.rollingnumbers/**(上游算法,原样编译)
↓ LevenshteinUtils / RollingNumbersModel.resolveColumn / Double.format
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON(动画模型,不是像素)
N-API rollingNumbersRender()
↓
ArkUI Index.ets(裁剪窗口 + 滑动轨道重放)
请求与响应:
请求 : {"kind":"text","source":"12345","target":"67890",
"characters":"number","scrollingDirection":"Any","animationDuration":2000}
{"kind":"currency","sourceAmount":1234.5,"targetAmount":9876.54,
"decimals":2,"currencySymbol":"$","symbolInFront":true,...}
响应 : {"ok":true,"kind":"...","source":"...","target":"...","direction":"Any",
"duration":2000,"lists":["0123456789"],
"columns":[{"index":0,"action":"SAME","current":"1","target":"6",
"list":0,"start":2,"end":7,"steps":5,"animate":true}, ...]}
{"ok":false,"error":"..."}
三条关键语义:其一,columns 是渲染指令而不是渲染结果——每列一条轨道引用和一对槽位索引,帧序列不跨桥;其二,current / target 里的 "" 表示上游 EMPTY_CHAR(INSERT 的起点、DELETE 的终点),DELETE 列 target 为空时 ArkTS 直接不渲染(对应 Compose 端 charWidth > 0 才画);其三,货币模式的金额格式化在桥内走上游 Double.format(signed=false) 完成,负号不进滚动列——- 不在数字轨道里,混进去会被 Levenshtein 判 SAME 原地替换,整列看起来「不滚」。
3.2 载荷类型为什么是动画模型
桥内计算这样走:
val actions = RollingNumbersModel.computeColumnActions(source, target, lists)
for (i in 0 until maxOf(source.length, target.length)) {
val column = RollingNumbersModel.resolveColumn(
source.getOrNull(i) ?: Utils.EMPTY_CHAR,
target.getOrNull(i) ?: Utils.EMPTY_CHAR,
actions.getOrNull(i) ?: ACTION_SAME,
lists, direction,
)
// column.listIndex / startIndex / endIndex / animate → JSON
}
返回模型意味着 ArkTS 侧不需要理解 Levenshtein,也不需要复刻环绕数学——它只拿到「每列从轨道槽 N 滚到槽 M」。帧率、曲线、打断全部交给系统 animateTo,这正是把「过程量留在有动画能力的那一层」。animationDuration 桥侧钳制 200-10000 ms(默认 Medium 2000),decimals 钳制 0-8——防御性钳制而不是信任调用方。字符轨道的三级解析顺序是 characterLists 显式数组 > characters 预设(映射上游 Utils.provideNumberString / provideAlphabeticalString / provideAlphanumericList,custom 用 customList)> 默认数字列表——ArkTS 不手写字符集,与 JVM 对齐才有交叉验证的意义。
3.3 错误通道
导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("RollingNumbersRender")
fun rollingNumbersRender(request: String): String = try {
/* kind 分发 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、金额 NaN、不支持的 kind,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(librollingnumbers_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosrollingnumbers.so)
│ RollingNumbersRender
▼
UTF-8 JSON(动画模型)+ DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;把 Levenshtein 抄进 ArkTS 又会引入漂移,所以选择 C ABI + JSON 模型。
4.2 Kotlin/Native 导出函数
@CName("RollingNumbersRender")
fun rollingNumbersRender(request: String): String // text / currency 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
RollingNumbersRender | rollingNumbersRender(req) | computeColumnActions + 逐列 resolveColumn + Double.format + currencyDirection |
对比 qrose 的 QroseRender(出 PNG)与 KStore 的 KStoreDispatch/KStoreVersion(出引擎操作):本仓一个符号就够,因为滚动没有持久化语义、也没有多种操作——一次请求一次模型。
4.3 C++ N-API 分发
napi_property_descriptor desc[] = {
{"rollingNumbersRender", nullptr, NapiRollingNumbersRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打请求字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosrollingnumbers_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。CAdapter 生成的 libohosrollingnumbers_api.h 整份入库(含 RollingNumbersRender extern 与 ExportedSymbols 服务结构),与 link 产物严格一致。API 26 的 NAPI 头把 napi_get_value_string_utf8 缓冲区标成非 const——先量长度、再开 std::vector<char>、拷完再构造 std::string。
4.4 CMake 和类型声明
add_library(ohosrollingnumbers SHARED IMPORTED)
set_target_properties(ohosrollingnumbers PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(rollingnumbers_napi SHARED napi_init.cpp)
target_link_libraries(rollingnumbers_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosrollingnumbers)
OHOS_ARCH 为空或 libohosrollingnumbers.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 copyKnSoArm64/X64 任务从 ~/.konan/dependencies/<triple>/lib/ 找到匹配三元组的 libc++,finalizedBy 挂在 linkDebugSharedOhosArm64/X64 后面自动回填 HAP 的 entry/libs/<abi>/。
export const rollingNumbersRender: (request: string) => string;
第 5 阶段:ArkUI 宿主(本适配最大的坑在这里)
5.1 滚动重放的结构
每列最终收敛为两层结构:
Column(外层窗口:26×40,clip(true),固定不动,负责裁剪)
└─ Text(内层轨道:多行字符串,lineHeight=40,translate 滑动)
trackText = " \n0\n1\n…\n9\n0\n…" ← EMPTY + 轨道 + 轨道,\n 连接
第 N 行就是轨道第 N 槽,lineHeight(CHAR_H) 把行高钉死成 40,translate({ y: -offset * 40 }) 把槽 offset 滚进窗口。这与 Compose 端 Box(clipToBounds) + Canvas 内部位移 逐层对应,也是 ArkUI 跑马灯的标准写法。两条铁律(都是真机踩出来的,见 8.1):
- translate 必须在内层内容上。加在外层窗口上,窗口会被拖出布局槽位(最多 -400px,直接飘到屏幕顶部),而且窗口平移后显示的永远是内部第一行——恰好是 EMPTY 空槽,表现为「框外几个无字色块」;
- 不要嵌套 ForEach。逐字符 Text 的嵌套 ForEach 在真机上渲染不可靠(21 槽长轨道整列不出、2 槽回退轨道反而出现);压平成一个多行 Text 后每列只剩一个组件。
每次生成时:viewColumns(每列的 trackText 与可见性)在 generate() 里一次性预计算,UI 层零方法建数组;key 里带代数号(g{genId}-col-{index})强制重建节点,规避「key 相同不更新」;animateTo({ duration, curve: Curve.EaseInOut }) 在 120 ms 后把 offsets 从 starts 推到 ends,带防重入令牌。
5.2 页面交互
数字 Tab 自上而下:当前 / 目标输入框、快捷目标 Chip(0 / 99999 / 20260101 / 随机)、开始滚动按钮;货币 Tab 增加前后金额(可输负数)、小数 0-4、符号四选、符号在前与正号开关,显示结构镜像 CurrencyRollingNumbers(− 恒显 / + 可选 / 符号按位 / 间距 8vp);字符集 Tab 四预设切换自动换合法样本(12345→67890 / HELLO→WORLD / ABC123→XYZ789 / 0F1E2D→3C4B5A);方向与速度 Tab 用反转样本 0123456789→9876543210 展示三方向路径差异。底部状态行显示 生成成功 kind=… 列数=N direction=… duration=…ms,其下灰字给出桥响应前 220 字符——真机排障不用连 hdc 也能看到桥说了什么。
5.3 对比度和错误边界
非法金额(parseFloat NaN)前置校验、不发 Native 调用;ok:false 的错误原文进状态行(红色);Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并写 hilog。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosrollingnumbers)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName + MiniJson(引号+方括号感知,扁平 JSON)
└── harmonyApp/
├── build-profile.json5 "signingConfig": "default"
├── AppScope/app.json5 bundleName org.terminator.ohos.rollingnumbers、versionName 0.4.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/libohosrollingnumbers_api.h
├── src/main/cpp/types/librollingnumbers_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(四 Tab)
6.2 构建、签名和安装
./gradlew :lib:jvmTest # 10 项交叉验证
./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 写 "signingConfig": "default",证书不进入仓库。
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.rollingnumbers -m entry
四、完整代码对照
4.1 整体架构
com.dontsaybojio.rollingnumbers.levenshtein/**(上游算法原样编译)
│ LevenshteinAction / LevenshteinUtils(矩阵 + 回溯)
com.dontsaybojio.rollingnumbers.model/**(上游模型原样编译)
│ CharacterIndices / CharacterListData / AnimationState / ScrollingDirection / DefaultAnimationDuration
com.dontsaybojio.rollingnumbers.ui/**(Utils + DoubleExtensions,原样编译)
│ EMPTY_CHAR / 字符集预设 / 金额格式化
RollingNumbersModel.kt(新增公共门面)
│ computeColumnActions / resolveColumn / currencyDirection
Bridge.kt + MiniJson
│ RollingNumbersRender
librollingnumbers_napi.so
│ rollingNumbersRender
Index.ets
│ JSON parse + viewColumns 预计算 + @State 更新
▼
ArkUI 真机页面(裁剪窗口 + 滑动轨道,四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_rollingnumbers、:sample / :androidApp 移出 |
gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 一行 + fork 版本号 |
gradle.properties | JDK 21 钉版 + KN 三开关 + version=0.4.1-ohos.1 |
gradle/wrapper/gradle-wrapper.properties | Gradle 9.5.1 → 8.14.1 |
build.gradle.kts(根) | 版本 0.4.1-ohos.1、全局禁用 Ohos*Test* 任务 |
lib/build.gradle.kts | 纯 KMP:jvm() + ohosArm64 + ohosX64,零依赖,jvmTest 补 kotlin-test |
lib/.../RollingNumbersModel.kt | 新增公共门面:动作常量、逐列解析、货币方向、环绕索引数学 |
lib/src/composeMain/ | 上游 Compose 实现 4 文件冻结(RollingNumbers / CurrencyRollingNumbers / AnimatedCharacterColumn / DrawScopeExtensions) |
| `lib/src/jvmMain | ohosMain/` |
lib/src/jvmTest/.../RollingNumbersModelTest.kt | 10 项模型契约,gradlew :lib:jvmTest 一键回归 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 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 | ArkUI 验收页面(四 Tab + 窗口/轨道两层重放) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 算法 | LevenshteinUtils.computeColumnActions | 逐列 SAME / INSERT / DELETE(internal,经门面暴露) |
| 门面 | RollingNumbersModel.resolveColumn | 列动作 → 轨道引用 + 起止槽位 + 步数 |
| 门面 | RollingNumbersModel.currencyDirection | 前后金额按绝对值推断方向 |
| 格式化 | Double.format(decimals, …, signed=false) | 千分位 + 小数位 + 防科学计数法 |
| 桥 | RollingNumbersRender | C ABI JSON 导出(模型进 / 模型出) |
| N-API | rollingNumbersRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | generate(request) | 调桥、预计算 viewColumns、触发 animateTo |
| ArkUI | Column(clip) + Text(lineHeight) | 每列一个组件的重放结构 |
4.4 ArkTS 与 Kotlin 的边界
const res: RenderOk = JSON.parse(rollingNumbersRender(JSON.stringify({
kind: 'text', source: '12345', target: '67890',
characters: 'number', scrollingDirection: 'Any', animationDuration: 2000
})));
// res.columns[i]:{ action, list, start, end, steps, animate }
// 外层窗口固定,内层轨道 translate -offset*40,animateTo 2s 滚到 target
val actions = RollingNumbersModel.computeColumnActions(source, target, lists)
val column = RollingNumbersModel.resolveColumn(
currentChar, targetChar, action, lists, scrollingDirection)
// "12345"→"67890":列 0 为 ('1'→'6', list=0, start=2, end=7, steps=5)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或帧序列。
五、关键决策说明
决策 1:算法真编译、Compose 冻结,而不是提取内核
算法层(Levenshtein、环绕轨道、格式化)零 Compose 依赖,原样编进 ohos 没有任何障碍——抽内核等于手抄 Robinhood ticker,反而引入漂移。Compose 渲染层(Animatable / TextMeasurer / clearAndSetSemantics)依赖 Compose 1.11,社区 CMP 是 1.9.2,硬接等于重发一套 Compose 发行版;照搬 cmp_qrose 的结论原样冻结 composeMain,等社区 CMP ohos 制品对齐再接回。分层线画在「第一处 import androidx.compose 的文件」上,一刀切干净。
决策 2:零依赖库走 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 旧线
工具链版本跟着依赖走:KStore 依赖 kotlinx ohos 制品线(stdlib 2.4.20),被迫上 2.4.20 线;本库 commonMain 零第三方依赖,没有版本反推压力,直接采用社区已审的 2.2.21-1.0.0 最低成本线,配套钉法 Gradle 8.14.1。同时把上游 :sample / :androidApp 从 include 剔除——它们挂着的 AGP / Compose 插件是配置期最大的炸点。
决策 3:桥出动画模型,而不是渲染结果
qrose 的桥输出 PNG,因为条码的最终形态就是一张图;RollingNumbers 的最终形态是「一段动画」,图无法表达,逐帧位移序列又会耦合时长 / 曲线 / 帧率。桥输出「每列:哪条轨道、从槽 N 到槽 M、走几步」这组稳定契约,animateTo 的曲线、打断、手势全部原生——这与 Vico(数据交 ArkTS 画)、Reorderable(列表状态交 ArkTS)、KStore(引擎操作交 ArkTS)同属一条社区路线,只是载荷形态按库语义各不相同。
决策 4:RollingNumbersModel 公共门面 + 受控重复
internal 是模块级可见性,桥碰不到上游算法类;对标 Qrose.encodeQr 在库内加公共门面。环绕索引四个私有函数从 AnimatedCharacterColumn(已冻结 composeMain)逐字抽出门面,composeMain 不回改——门面与冻结件存在一份受控重复,来源在 KDoc 注明。不把冻结件改成引用门面,是因为 composeMain 不参与编译,改了也没有编译器兜底;保持逐字原样反而可审计。
决策 5:自带契约测试 + ArkUI 双层重放
上游没有测试,交叉验证的闸门只能自带:10 项 :lib:jvmTest 钉死列模型契约(含 EMPTY 槽位偏移、环绕方向、格式化输出),「两端一致」从口头承诺变成一键回归。ArkUI 侧收敛出「外层裁剪窗口 + 内层滑动轨道」的双层结构,并把它写成两条铁律(translate 在内层、禁止嵌套 ForEach)——这两条都是真机踩出来的,见 8.1。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x64)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :lib:jvmTest
10 项用例全部通过(详见 2.3 节用例表)。上游没有测试文件,本仓的契约测试就是「内核没漂」的闸门:同一输入下,桥返回的 columns 与 JVM 门面输出逐列一致(含 action / list / start / end / steps),货币模式的格式化字符串逐字符一致。
6.3 HAP 验证
:lib:compileKotlinJvm: BUILD SUCCESSFUL
:lib:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:example:nativeApp:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/<abi>/
llvm-nm -D: RollingNumbersRender 为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 librollingnumbers_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosrollingnumbers.so debugShared 双 ABI:arm64 约 3.77 MB、x86_64 约 3.47 MB(含 Kotlin/Native 运行时),随行 libc++_shared.so 按 konan 三元组(aarch64-linux-ohos / x86_64-linux-ohos)匹配拷贝。CAdapter 链接期有一条 clang api.cpp:448 警告,与 cmp_qrose / KStore 同源(生成器已知告警),可忽略。
6.4 功能用例
- 启动后自动跑默认数字滚动
12345 → 67890,状态行出现生成成功 kind=text 列数=5 direction=Any duration=2000ms; - 数字 Tab 快捷目标依次点击:
0(全列 DELETE 坍缩)、99999(全列大幅上行)、20260101(INSERT 扩张,新列从空滚入)、随机; - 货币 Tab
1234.5 → 9876.54:8 列滚动,千分位逗号与小数点原地跳变(上游语义:分隔符不在轨道里不滚),状态行列数=8 direction=Up; - 货币 Tab 输入
-1234.5:符号位出现−;打开「显示正号」后正数前出现+;切换符号在前符号换边; - 字符集 Tab 切
alphabetical:样本换HELLO → WORLD,只有字母滚动;alphanumeric双轨道同屏;custom十六进制0F1E2D → 3C4B5A; - 方向与速度 Tab 反转样本
0123456789 → 9876543210分别切Any / Up / Down:三种种路径差异肉眼可见,Any每列取较短方向; - 快速连点「开始滚动」:防重入令牌生效,动画不叠加不互扰;
- 每步操作的状态行与灰色响应预览按最新结果刷新,hilog 同步留痕。
6.5 验证结论
算法内核、动画模型、Native 动态库、C ABI、N-API、ArkUI 重放和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四个 Tab 的滚动动画与 JVM 交叉验证的列模型一致(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/rollingnumbers.git
cd rollingnumbers
export JAVA_HOME="/path/to/jdk-21"
./gradlew :lib:compileKotlinJvm # JVM 交叉验证目标
./gradlew :lib:jvmTest # 10 项模型契约
./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.rollingnumbers -m entry
hdc shell hilog | findstr RollingNumbers
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:lib真正编译成ohosArm64/ohosX64动态库; translate必须加在内层轨道上,绝不能加在外层裁剪窗口上:窗口被拖出布局槽位(最多 -400px,飘到屏幕顶部),且窗口平移后显示的永远是内部第一行——EMPTY 空槽,表现为「框外几个无字色块」。这是本适配耗时最长的坑,两轮真机截图才定位;- 嵌套 ForEach(列里套字符)在真机渲染不可靠:21 槽长轨道的列整列不出,2 槽回退轨道反而出现。轨道压平成一个多行
Text(\n连接 +lineHeight钉行高)后,每列只剩一个组件,问题消失; - 环绕轨道 EMPTY 恒占 0 号槽,字符 d 的索引是 d+1(
'0'→1、'9'→10)——桥、门面、ArkTS 三侧的槽位索引必须以同一张表为基准,回退轨道(list=-1)也要保留 EMPTY 头槽,否则差一位就是整列空白; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;上游:sample/:androidApp留在 include 里会把 AGP / Compose 插件整链拉回;- 上游 wrapper Gradle 9.5.1 与
2.2.21-1.0.0线不兼容,必须降 8.14.1——2.4.20 线(KStore)留 9.x 的经验不能互相套用; - 上游
gradle.properties的org.gradle.configuration-cache=true要去掉,KN 插件在配置缓存下不稳定;kotlin.native.ignoreDisabledTargets/downloadFromMaven两个开关必开; - MiniJson 顶层逗号切分只感知引号不够——
characterLists是字符串数组,必须加方括号深度感知,否则数组被切穿; - 负号走
signed=false的格式化、由 UI 层画−(U+2212)——让-混进滚动列会被 Levenshtein 判 SAME 原地替换;同理金额分隔符「原地跳变」是上游语义,不是 bug; - 货币方向必须按绝对值比较(
currencyDirection),按代数值比较会让负数金额变大时方向反判; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - DevEco 打开工程后生成的
entry/oh_modules/不被根oh_modules规则覆盖,要单独追加 gitignore,否则依赖目录混进提交。
8.2 已知问题
- HarmonyOS 上没有 Compose 渲染:
composeMain冻结,Demo 是 ArkUI 重放,等社区 CMP ohos 制品对齐后接回、桥降级 fallback; - 列宽固定 26vp:Compose 端按
TextMeasurer逐字符实测(EMPTY 列宽 0),ArkTS 侧固定值在W/€等宽字符上有观感差异,可改measureTextSize动态测宽; - locale 三函数是 Intl 默认值:
./,/ 符号在前——KN 无系统 locale API,真实 locale 检测放 ArkTS(i18n Kit)经桥参数覆盖; - 动画曲线与上游 tween 的默认 easing 不逐帧一致:Compose
tween默认 FastOutSlowIn,Demo 用Curve.EaseInOut,观感同为先快后慢; - 无障碍语义未移植:上游
clearAndSetSemantics把完整目标值作为整体朗读,ArkUI 侧可补accessibilityText; - CAdapter clang
api.cpp:448警告与 cmp_qrose / KStore 同源,等 HarmonyOS Kotlin 修生成器; - HAP 安装必须使用与 bundle name
org.terminator.ohos.rollingnumbers匹配的签名。
8.3 未来优化方向
- 用
measureTextSize动态测列宽,对齐 Compose 端TextMeasurer行为; - ArkTS 侧接入 i18n Kit 读真实 locale,自动填充
decimalSeparator/groupingSeparator/symbolInFront; - 补
accessibilityText无障碍语义(整体朗读目标值); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查(
llvm-nm -D)和 HAP 构建; - 补充真机(x86_64)验收矩阵与 hdc uitest 自动化截图脚本(数字 / 货币 / 字符集 / 方向逐 Tab 留档)。
九、总结
9.1 核心难点回顾
上游 lib(commonMain 算法零 Compose 依赖)
→ 根构建替换(cmp_qrose 模式,:sample / :androidApp 移出)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(零依赖库的最低成本线)
→ ohosArm64 / ohosX64 KLIB + composeMain 冻结 + 门面/actual 三个新增源文件
→ C ABI(@CName)+ 动画模型载荷(不是 PNG,也不是帧序列)
→ C++ N-API(DisposeString 纪律)
→ ArkTS 预计算 viewColumns + 双层重放(外层裁剪窗口 / 内层滑动轨道)
→ ArkUI 真机四 Tab 验收(数字 / 货币 / 字符集 / 方向与速度)
→ 10 项 JVM 契约测试钉住「两端一致」
9.2 封装层次
- 算法层:上游原版 Levenshtein 列动作、环绕轨道索引、金额格式化;
- 门面层:新增
RollingNumbersModel公共 API(动作常量、逐列解析、货币方向),internal 与桥之间的唯一通道; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出动画模型 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、预计算轨道视图、animateTo 重放、状态行与响应预览;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 桥载荷的形态由库语义决定,不由路线惯性决定:同样是 CMP 库,qrose 出 PNG、KStore 出引擎操作、RollingNumbers 出动画模型——「滚动是过程量」,把稳定契约(轨道 + 槽位)交出去、把帧序列留在有动画能力的层,Demo 才像原生组件;
- 工具链版本跟着依赖走,零依赖库走已审最低成本线:2.2.21-1.0.0 配 Gradle 8.14.1,KStore 的 2.4.20 配 9.x——两条线的钉法不能互相套用,但「eazytec 双仓置前 + 剔除上游示例模块」是共同前置;
- 渲染层适配的验收必须上真机:嵌套 ForEach 失灵、translate 加错层,真机与代码审查都发现不了;「框外无字色块」这类现象只有真机截图能定位,Demo 里预留状态行与响应预览(不连 hdc 也能看到桥说了什么)让每轮真机反馈都信息满格。
9.4 适配成果
当前适配已完成:
- RollingNumbers
0.4.1(0.4.1-ohos.1)算法内核的双 ABI Kotlin/Native 编译(上游commonMain文件零重写,新增门面 + 2 份 actual + 1 个测试文件); - Compose 渲染层
composeMain冻结隔离,等社区 CMP ohos 制品对齐后接回; - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(数字、货币、字符集、方向与速度,双层窗口轨道重放);
- 10 项 JVM 模型契约测试全绿 + HAP 构建(双 ABI 三件套 so,arm64 约 3.77 MB)、签名、设备安装和真机验证;
- 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)