本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(从上游 9.5.1 降级,2.2.21-1.0.0 线的钉法)
第三方依赖零(桥的 JSON 解析手写 MiniJson)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.rollingnumbers、versionName 0.4.1-ohos.1
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
源集文件数依赖形态处置
commonMain10(上游)纯 Kotlin,零 UI、零 AGP、零第三方整体编译(jvm + 双 ohos)
composeMain4androidx.compose.*(TextStyle / Animatable / TextMeasurer / DrawScope)冻结隔离,不接任何 target
androidMain 等 4 个平台源集各 1java.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 项全绿
用例钉住什么
等长数字段全 SAMELevenshtein 对等长段的最优编辑语义
分隔符不滚动不在轨道里的字符(, .)保持原地替换
'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 方法覆盖内核
RollingNumbersRenderrollingNumbersRender(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):

  1. translate 必须在内层内容上。加在外层窗口上,窗口会被拖出布局槽位(最多 -400px,直接飘到屏幕顶部),而且窗口平移后显示的永远是内部第一行——恰好是 EMPTY 空槽,表现为「框外几个无字色块」;
  2. 不要嵌套 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.propertiesJDK 21 钉版 + KN 三开关 + version=0.4.1-ohos.1
gradle/wrapper/gradle-wrapper.propertiesGradle 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/jvmMainohosMain/`
lib/src/jvmTest/.../RollingNumbersModelTest.kt10 项模型契约,gradlew :lib:jvmTest 一键回归
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、MiniJson(方括号感知)、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(四 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)千分位 + 小数位 + 防科学计数法
桥RollingNumbersRenderC ABI JSON 导出(模型进 / 模型出)
N-APIrollingNumbersRender(req)向 ArkTS 暴露能力
ArkTSgenerate(request)调桥、预计算 viewColumns、触发 animateTo
ArkUIColumn(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 功能用例

  1. 启动后自动跑默认数字滚动 12345 → 67890,状态行出现 生成成功 kind=text 列数=5 direction=Any duration=2000ms;
  2. 数字 Tab 快捷目标依次点击:0(全列 DELETE 坍缩)、99999(全列大幅上行)、20260101(INSERT 扩张,新列从空滚入)、随机;
  3. 货币 Tab 1234.5 → 9876.54:8 列滚动,千分位逗号与小数点原地跳变(上游语义:分隔符不在轨道里不滚),状态行 列数=8 direction=Up;
  4. 货币 Tab 输入 -1234.5:符号位出现 −;打开「显示正号」后正数前出现 +;切换 符号在前 符号换边;
  5. 字符集 Tab 切 alphabetical:样本换 HELLO → WORLD,只有字母滚动;alphanumeric 双轨道同屏;custom 十六进制 0F1E2D → 3C4B5A;
  6. 方向与速度 Tab 反转样本 0123456789 → 9876543210 分别切 Any / Up / Down:三种种路径差异肉眼可见,Any 每列取较短方向;
  7. 快速连点「开始滚动」:防重入令牌生效,动画不叠加不互扰;
  8. 每步操作的状态行与灰色响应预览按最新结果刷新,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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :lib 真正编译成 ohosArm64 / ohosX64 动态库;
  2. translate 必须加在内层轨道上,绝不能加在外层裁剪窗口上:窗口被拖出布局槽位(最多 -400px,飘到屏幕顶部),且窗口平移后显示的永远是内部第一行——EMPTY 空槽,表现为「框外几个无字色块」。这是本适配耗时最长的坑,两轮真机截图才定位;
  3. 嵌套 ForEach(列里套字符)在真机渲染不可靠:21 槽长轨道的列整列不出,2 槽回退轨道反而出现。轨道压平成一个多行 Text(\n 连接 + lineHeight 钉行高)后,每列只剩一个组件,问题消失;
  4. 环绕轨道 EMPTY 恒占 0 号槽,字符 d 的索引是 d+1('0'→1、'9'→10)——桥、门面、ArkTS 三侧的槽位索引必须以同一张表为基准,回退轨道(list=-1)也要保留 EMPTY 头槽,否则差一位就是整列空白;
  5. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;上游 :sample / :androidApp 留在 include 里会把 AGP / Compose 插件整链拉回;
  6. 上游 wrapper Gradle 9.5.1 与 2.2.21-1.0.0 线不兼容,必须降 8.14.1——2.4.20 线(KStore)留 9.x 的经验不能互相套用;
  7. 上游 gradle.properties 的 org.gradle.configuration-cache=true 要去掉,KN 插件在配置缓存下不稳定;kotlin.native.ignoreDisabledTargets / downloadFromMaven 两个开关必开;
  8. MiniJson 顶层逗号切分只感知引号不够——characterLists 是字符串数组,必须加方括号深度感知,否则数组被切穿;
  9. 负号走 signed=false 的格式化、由 UI 层画 −(U+2212)——让 - 混进滚动列会被 Levenshtein 判 SAME 原地替换;同理金额分隔符「原地跳变」是上游语义,不是 bug;
  10. 货币方向必须按绝对值比较(currencyDirection),按代数值比较会让负数金额变大时方向反判;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  13. 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 三条经验

  1. 桥载荷的形态由库语义决定,不由路线惯性决定:同样是 CMP 库,qrose 出 PNG、KStore 出引擎操作、RollingNumbers 出动画模型——「滚动是过程量」,把稳定契约(轨道 + 槽位)交出去、把帧序列留在有动画能力的层,Demo 才像原生组件;
  2. 工具链版本跟着依赖走,零依赖库走已审最低成本线:2.2.21-1.0.0 配 Gradle 8.14.1,KStore 的 2.4.20 配 9.x——两条线的钉法不能互相套用,但「eazytec 双仓置前 + 剔除上游示例模块」是共同前置;
  3. 渲染层适配的验收必须上真机:嵌套 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐