开源鸿蒙平台 CMP 三方库「MiddleEllipsisText」适配全流程
本文记录
MiddleEllipsisText接入 OpenHarmony 的完整过程,覆盖文本截断库盘点、算法与 Compose 的解耦、纯 Kotlin 引擎提炼、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、前缀宽度测量契约、ArkUI 真机页面、HAP 构建和真机验收。
MiddleEllipsisText是 Compose Multiplatform 的"中间省略号"Text 组件库:把超宽文本截成head...tail而不是尾部省略,省略字符与个数可自定义,内建 BreakIterator 字素分词,不切开多字节文字和 emoji,以io.github.mataku:middle-ellipsis-text(Material 2)与io.github.mataku:middle-ellipsis-text3(Material 3)双制品发布。本次适配与社区此前两条路线都不同:KStore 那类零依赖内核库走"真编译整库",而本库的全部上游实现都活在@Composable里(SubcomposeLayout+TextLayoutResult)——因此走 texty 开创的"算法重投影"路线:把上游贪心截断算法逐行移植为纯 Kotlin 引擎,测量与渲染交给 ArkTS,中间用"前缀宽度数组"这条数字契约缝合。40 项 JVM 测试锚定语义没漂,双 ABI 动态库在真机上完成验收。
项目地址: AtomGit/oh-tpc/MiddleEllipsisText.git
上游仓库: mataku/MiddleEllipsisText
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
MiddleEllipsisText 为 Compose 应用提供"长文本中间省略"这一件事的完整答案:MiddleEllipsisText(text = soLongText) 一行替换 Text,文件名、URL、邮箱、哈希这类长文本就能同时保留头尾有效信息;ellipsisChar / ellipsisCharCount 让省略形态完全可控(Compose 1.8 起官方的 TextOverflow.MiddleEllipsis 恰恰不支持这两项自定义,这正是上游的存活空间);实现上先用无约束 SubcomposeLayout 拿到全文 TextLayoutResult,再用双指针从两端按字素簇贪心取字,直到装满 maxWidth - 省略符宽度——多码点 emoji(旗帜、ZWJ 家庭、肤色修饰)整簇进出,绝不切半个字形。
OpenHarmony 的 ArkUI Text 只有尾部省略,同样需要这个能力。但上游构建(Kotlin 1.9.23 / Compose 1.6.10 / AGP 8.1.4)既不生成 OpenHarmony 变体,它的实现又整体站在 androidx.compose.* 上——这是比 KStore 更硬的边界。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 1.9.23 / Compose 1.6.10 / AGP 8.1.4 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)无法共存于一个 Gradle 构建。 |
| 实现全在 Compose | 与 KStore"内核纯 Kotlin"不同,上游零平台无关代码可直接编译——算法嵌在 composable 的 remember {} 里,测量依赖 TextLayoutResult.getBoundingBox。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 测量依赖宿主文本引擎 | 截断决策需要每个字素的像素宽度,而 KN 侧没有文本引擎,宽度事实只能来自 ArkTS 的 measure.measureText。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:上游算法的纯 Kotlin 重投影(逐行移植,语义等价可测)、根构建替换(cmp_qrose 模式)和"前缀宽度契约"C ABI/N-API 桥接层。截断语义完全由移植引擎负责,ArkTS 只负责测量、调用 Native 和渲染结果。
1.2 库提供的能力
Demo 提供一个真实的中间省略会话,对应桥接的全部入口:
- 实时截断(compute):文本、可用宽度、省略符、个数任一变化,立即返回
head + 省略串 + tail; - 四组预设:长 URL / 日文混排 / Emoji(含 ZWJ 家庭、肤色修饰、旗帜对)/ 中文长文件名,一键验证多字节与多码点安全;
- 宽度滑杆:80–340vp 连续拖动,预览区宽度即约束宽度,截断位置实时变化;
- 省略符自定义:省略字符与个数(1–6)可改,对应上游
ellipsisChar/ellipsisCharCount两个差异化参数; - 版本握手:
middleEllipsisVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
result | 截断结果串;放得下时等于原文。 |
truncated | 是否发生截断。 |
headLength / tailLength | 头尾保留的 UTF-16 字符数。 |
headWidth / tailWidth / ellipsisTextWidth / fullWidth | 头宽 / 尾宽 / 省略串宽 / 原文全宽(px),验收宽度的数字证据。 |
version | 引擎版本号。 |
引擎侧完整语义(两遍布局思想、双指针左右交替、字素簇预算、消费后复核弃簇、多码点整簇消费)全部在 commonMain 的 middleEllipsize() 里,与上游 composable 同构;JVM 侧 40 项测试(两模块各 20 项)交叉验证,证明 ohos .so 里执行的截断逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游算法逐行移植为 engine/middleEllipsize()(条件结构、消费顺序、弃簇逻辑与上游一致);上游 composable、平台 actual、测试零改动归档 src/composeMain;internal/BreakIterator expect/actual 上游原件保留。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,两个库模块(Material 2 / Material 3)出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sampleApp(依赖已发布快照)与 build-logic(AGP 约定插件)留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 测量纪律 | 宽度事实全部来自 ArkTS measureText 的前缀累计宽度数组;任意子串宽度按前缀差望远镜式守恒,代理对永不半截计量。 |
| UI 完整 | 双 Tab 覆盖全部桥接入口:文本输入、四预设、宽度滑杆、省略符/个数、实时预览、信息面板、百条日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。库的价值是截断算法而非控件皮肤;ArkTS Demo 把桥暴露的截断闭环画出来,并承担测量职责。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认算法与 Compose 的耦合度、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模 ── 前缀宽度契约、compute JSON 协议、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── measureText 前缀测量、实时重算、信息面板和版本握手
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
MiddleEllipsisText/ 上游 Material 2 库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
MiddleEllipsisText/src/commonMain/ 上游 BreakIterator expect(原件)+ 新增纯省略引擎 + 新增纯 Kotlin 字素分词器
MiddleEllipsisText/src/jvmMain/ 新增:java.text BreakIterator actual(交叉验证基准)
MiddleEllipsisText/src/ohosMain/ 新增:纯 Kotlin 分词器 actual(8 行)
MiddleEllipsisText/src/composeMain/ 上游 composable + 三平台 actual + 上游测试(零改动,不编译)
MiddleEllipsisText3/ Material 3 同构模块,目录完全相同
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
build-logic/ sampleApp/ screenshot/ 上游构建插件/示例/截图,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :MiddleEllipsisText、:MiddleEllipsisText3 与 :example:nativeApp,rootProject.name = "ohos_middleellipsistext"。上游根构建的 Sonatype nexus 发布、签名密钥注入、AGP/compose 插件别名全部随根构建替换剥离——本 fork 不发 Maven Central。
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,已写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 8.7 升级;2.2.21 线要求 8.14.1) |
| 第三方依赖 | 零(引擎无 coroutines / serialization / IO 依赖,无制品线对齐问题) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.middleellipsistext |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 内容 | 依赖形态 | 处置 |
|---|---|---|---|
MiddleEllipsisText | composable 1 个 + BreakIterator expect/actual | 100% androidx.compose.*,测量依赖 TextLayoutResult | 算法移植进 commonMain,Compose 部分归档 composeMain |
MiddleEllipsisText3 | 同上(Material 3 包) | 同上 | 同构处理 |
sampleApp | CMP 示例(依赖已发布 Maven 快照) | Compose 1.6 / AGP 8.1 | 留盘不进构建 |
build-logic | AGP 约定插件 + Sonatype 发布 | AGP 8.1.4 | 留盘不进构建 |
expect 声明全库只有 1 处:internal expect fun getBreakIterator(): BreakIterator。上游给了 3 个平台 actual(android 用 android.icu、desktop/iOS 用 Skia)。本 fork 补 2 个:jvmMain 用 java.text.BreakIterator.getCharacterInstance()(与上游 desktop 走 ICU 的行为最接近,作交叉验证基准),ohosMain 用新增的纯 Kotlin 字素分词器:
// MiddleEllipsisText/src/ohosMain/kotlin/.../internal/BreakIterator.ohos.kt
// Kotlin/Native on ohos has no ICU binding, so grapheme segmentation falls
// back to the pure-Kotlin approximation that lives in commonMain.
actual fun getBreakIterator(): BreakIterator = GraphemeClusterBreakIterator()
GraphemeClusterBreakIterator 是 UAX #29 精选范围近似:CRLF、主要文字(拉丁/希腊/西里尔/希伯来/阿拉伯/天城文/泰文/假名)合成符号、变体选择符(FE00–FE0F / E0100–E01EF)、ZWJ emoji、肤色修饰(1F3FB–1F3FF)、旗帜对(1F1E6–1F1FF 配对)、谚文音节组合,共约 60 行规则表。它放在 commonMain 而不是 ohosMain,是为了让 JVM 测试能直接实例化覆盖(见第 6 阶段)。
1.4 页面边界
标题区 MiddleEllipsisText · OpenHarmony / 版本徽标(绿色,握手值)
Tab 切换 演示 / 说明
文本区 TextArea(默认为仓库 AtomGit 地址)+ 四组预设 chip(长URL/日文混排/Emoji/文件名)
控制区 显示宽度滑杆(80–340vp,显示 vp 与 px 双值)、省略字符输入、省略个数滑杆(1–6)
预览区 定宽容器(宽度 = 滑杆值)内 Text(maxLines 1)展示截断结果;下方灰色小字为尾部省略的原文对照
信息面板 版本 / 是否截断 / 保留头尾字符数与宽度 / 完整宽度 vs 可用宽度 / 省略串宽 / 测量与计算耗时
日志区 带时间戳的操作日志(最新在前,上限 100 条)
说明区 分层链、工作原理、仓库与社区链接、已知限制
页面启动时先调 middleEllipsisVersion() 做握手,任何输入变化即触发重算。
第 2 阶段:目标与依赖打通
2.1 耦合度验证(本适配的成败手)
KStore 的成败手是"社区 Maven 上有没有 ohos 制品线";本库零第三方依赖,这个问题不存在——成败手换成"算法能不能从 Compose 测量里解耦"。逐行读上游 composable 的 remember {} 块,结论是三件事:
其一,算法与 Compose 的接触面只有两个数字通道:getBoundingBox(offset).width(单码元字形宽)和 getBoundingBox(text.lastIndex).right(全文右缘)——全部可以换成入参数组;其二,字素边界来自 getBreakIterator(),这是一条已有的 expect/actual 接缝,ohos 补 actual 即可;其三,其余逻辑(双指针、左右交替条件、簇内消费、消费后复核)是纯控制流,一行都不用改。
于是定下桥的核心契约:宿主测前缀,引擎做决策。ArkTS 只需要提供 text.length + 1 个累计宽度,引擎内部按 unit[i] = prefix[i+1] - prefix[i] 还原逐码元宽度,上游算法原样跑。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
引擎零依赖,没有 KStore 那种"coroutines 元数据反推 Kotlin 版本"的强制力,钉版跟着社区验证充分度走:cmp_qrose 与 texty 两个已过审仓库都钉 HarmonyOS Kotlin 2.2.21-1.0.0,配套 Gradle 8.14.1、JDK 21——本库直接复用这条线。上游 wrapper 是 8.7,不满足 2.2.21 线的最低要求,升级到 8.14.1。KStore 选 2.4.20 线是被依赖元数据逼的(coroutines 1.11.0 需要 stdlib 2.4.20),两条工具链线的经验不能互相套用——零依赖库没有理由离开验证最充分的线。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_middleellipsistext"
include(":MiddleEllipsisText")
include(":MiddleEllipsisText3")
include(":example:nativeApp")
版本目录同步收敛为最简形态(上游 44 行目录裁到 6 行):
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.3 编译库模块
两个库模块收敛为同构的纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用,commonTest 在这里跑
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游的 android 块、iOS framework、Roborazzi、compose 依赖全部随重写剥离(compose 实现已归档 composeMain,不注册源码集、不编译)。实测:
.\gradlew.bat :MiddleEllipsisText:compileKotlinOhosArm64 :MiddleEllipsisText:compileKotlinOhosX64 # BUILD SUCCESSFUL
.\gradlew.bat :MiddleEllipsisText3:compileKotlinOhosArm64 :MiddleEllipsisText3:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.4 测试的处置:上游测试不可复用,新建 40 项锚定
与 KStore"上游测试零改动全绿"不同,本库上游的 commonTest 本来就被作者整体注释掉(截图测试走 Roborazzi/androidTest),没有可直接复用的用例。处置是新建语义等价测试集:把上游算法的每个边界行为翻译成断言——引擎 10 项(空串直通、放得下直通、ASCII 贪心、左右交替左优先、自定义省略符/个数、emoji 簇不切开、混合宽度平衡、预算耗尽只剩省略串、入参校验 ×2)+ 分词器 10 项(ASCII、合成重音、假名、浊点、旗帜对、ZWJ 家庭、肤色修饰、谚文、CRLF、变体选择符),两模块各 20 项共 40 项,统一在 JVM 跑;根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
写测试的第一轮就抓到 5 个失败——全部是期望值凭直觉写错(详见 8.1 第 5 条),这反过来证明测试集真的钉住了语义。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.mataku.middleellipsistext.engine/**(纯 Kotlin 引擎 + 分词器,commonMain)
↓ middleEllipsize(贪心双指针 + 字素簇预算)
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API middleEllipsisVersion() / middleEllipsisCompute()
↓
ArkUI Index.ets(measureText 前缀测量 + 渲染)
请求与响应:
请求 : {"text":"https://atomgit.com/oh-tpc/MiddleEllipsisText.git",
"maxWidth":770, // px,可用宽度
"ellipsisChar":".","ellipsisCharCount":3,
"prefixWidths":[0,8.5,17.2,...], // px,text.length + 1 个累计宽度
"ellipsisWidth":4.2} // px,单个省略符宽度
响应 : {"ok":true,"result":"head...tail","truncated":true,
"headLength":12,"tailLength":8,"headWidth":208.5,"tailWidth":199.2,
"ellipsisTextWidth":12.6,"fullWidth":934.5,"version":"1.2.0-ohos.1"}
{"ok":false,"error":"..."}
一个关键设计:宽度以"前缀累计数组"进场,而不是逐字符宽度。unit[i] = prefix[i+1] - prefix[i] 望远镜式相加——簇内怎么拆,加起来都是整簇宽度;代理对的两个半截各自的前缀测量可能古怪,但引擎永远整簇消费,簇内误差相互抵消,半个字形永远不会被单独计量。这也是上游"逐字符 bbox 累加"在数字域的等价物。
3.2 测量为什么固定在 ArkTS 侧
截断宽度必须与最终渲染自洽:ArkTS 用 measure.measureText({ textContent: text.substring(0, i), fontSize: fp2px(18) }) 逐前缀测量,测与画走同一个 ArkUI 文本引擎,不存在"Kotlin 量的宽度、ArkTS 画的宽度"两套标准。省略符宽度由 Demo 用 8 次重复测量取均值(单字符太窄时抹平舍入误差),引擎按"单字符宽 × 个数"计总宽——与上游把省略符拼在文末测一次的做法一致。
Kotlin 侧引擎是纯函数、无副作用、无 IO,任何能提供前缀宽度的宿主(Android/iOS/桌面/JVM/服务端)都能直接调用 middleEllipsize(...)——这也是未来 Compose 工具链对齐后,把测量回调接回 TextMeasurer 的口子。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("MiddleEllipsisCompute")
fun middleEllipsisCompute(request: String): String = try {
/* 解析请求 → middleEllipsize(...) → 拼 ok 响应 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、prefixWidths 长度不符、负省略个数,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libmiddleellipsistext_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosmiddleellipsistext.so)
│ MiddleEllipsisCompute / MiddleEllipsisVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制截断语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("MiddleEllipsisCompute")
fun middleEllipsisCompute(request: String): String // 截断一口进
@CName("MiddleEllipsisVersion")
fun middleEllipsisVersion(): String = "1.2.0-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
MiddleEllipsisCompute | middleEllipsisCompute(req) | 前缀宽度 → 贪心双指针截断 → head...tail |
MiddleEllipsisVersion | middleEllipsisVersion() | fork 版本握手(真机版本徽标的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"middleEllipsisCompute", nullptr, NapiMiddleEllipsisCompute, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"middleEllipsisVersion", nullptr, NapiMiddleEllipsisVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回(字符串读取用两段式 napi_get_value_string_utf8,API 26 上单段式有边界问题)。KN 返回的字符串必须经 libohosmiddleellipsistext_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosmiddleellipsistext_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosmiddleellipsistext SHARED IMPORTED)
set_target_properties(ohosmiddleellipsistext PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(middleellipsistext_napi SHARED napi_init.cpp)
target_link_libraries(middleellipsistext_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosmiddleellipsistext)
OHOS_ARCH 为空或 libohosmiddleellipsistext.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const middleEllipsisCompute: (request: string) => string;
export const middleEllipsisVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 前缀测量和触发计算
aboutToAppear(): void {
this.version = middleEllipsisVersion(); // 版本握手:来自真实 Native 调用
this.compute();
}
private measurePrefixWidths(text: string): number[] {
const widths: number[] = [0];
const fontSizePx = fp2px(FONT_SIZE_FP);
for (let i = 1; i <= text.length; i++) {
widths.push(measure.measureText({
textContent: text.substring(0, i), fontSize: fontSizePx
}));
}
return widths; // text.length + 1 个累计宽度
}
private compute(): void {
const prefixWidths = this.measurePrefixWidths(this.inputText);
const res = JSON.parse(middleEllipsisCompute(JSON.stringify({
text: this.inputText,
maxWidth: Math.round(vp2px(this.maxWidthVp)),
ellipsisChar: this.ellipsisChar,
ellipsisCharCount: this.ellipsisCount,
prefixWidths: prefixWidths,
ellipsisWidth: this.measureOneEllipsisChar(this.ellipsisChar),
}))) as ComputeResp;
this.apply(res); // @State 刷新预览 + 信息面板 + 时间戳日志
}
文本、滑杆、省略符任一变化都走同一条 compute();空文本跳过 Native 调用;响应字段用 asNum / asStr 防御性读取,缺失数字渲染为 ?。
5.2 页面交互
演示 Tab 自上而下:标题区(绿色版本徽标 = 握手值)、TextArea + 四组预设 chip(长 URL / 日文混排 / Emoji / 文件名)、显示宽度滑杆(vp 与 px 双值显示)、省略字符与个数控制、预览卡片(定宽容器内 maxLines: 1 的截断结果,下方灰色小字为尾部省略的原文对照)、信息面板(版本 / 截断 / 头尾字符数与宽度 / 全宽 vs 可用宽 / 省略串宽 / 测量与计算耗时分行显示)、操作日志([HH:mm:ss.SSS] 时间戳 + ok=true head=… tail=… 摘要,最新在前,上限 100 条)。说明 Tab 展示 ArkTS → NAPI → C ABI → Kotlin 引擎 分层、三条工作原理、仓库与社区 AtomGit 链接和已知限制。
5.3 对比度和错误边界
预览容器带边框、宽度精确等于约束宽度,让"截断在中间发生"一眼可见;ok:false 时错误原文进信息面板与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosmiddleellipsistext)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.middleellipsistext、versionName 1.2.0-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/libohosmiddleellipsistext_api.h
├── src/main/cpp/types/libmiddleellipsistext_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
.\gradlew.bat :MiddleEllipsisText:jvmTest :MiddleEllipsisText3:jvmTest # 40 项交叉验证
.\gradlew.bat :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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.middleellipsistext -m entry
四、完整代码对照
4.1 整体架构
io.github.mataku.middleellipsistext.engine/**(上游算法逐行移植)
│ middleEllipsize(贪心双指针 / 字素簇预算 / 复核弃簇)
io.github.mataku.middleellipsistext.internal/**(BreakIterator expect + 纯 Kotlin 分词器)
│ getBreakIterator()
MiddleEllipsisText3(同构 Material 3 包)
Bridge.kt + MiniJson
│ MiddleEllipsisCompute / MiddleEllipsisVersion
libmiddleellipsistext_napi.so
│ middleEllipsisCompute / middleEllipsisVersion
Index.ets(measureText 前缀测量 + JSON parse + @State 更新)
▼
ArkUI 真机页面(双 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_middleellipsistext、sampleApp/build-logic 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
MiddleEllipsisText*/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,无 compose 依赖 |
*/src/commonMain/.../engine/MiddleEllipsize.kt | 上游算法逐行移植(核心新增) |
*/src/commonMain/.../internal/GraphemeClusterBreakIterator.kt | UAX #29 近似字素分词器(核心新增) |
*/src/jvmMain / ohosMain/.../BreakIterator.*.kt | 平台 actual(java.text / 纯 Kotlin) |
*/src/composeMain/ | 上游 composable + 三平台 actual + 测试归档(零改动,不编译) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 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 验收页面(measureText 前缀测量) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | middleEllipsize(text, prefixWidths, maxWidth, …) | 纯函数截断:贪心双指针 + 字素簇预算(核心) |
| 引擎 | MiddleEllipsisResult | 结果串 + truncated + 头尾字符数/宽度 + 省略串宽 |
| 分词 | getBreakIterator() | expect/actual 接缝(上游原件) |
| 分词 | GraphemeClusterBreakIterator | UAX #29 近似(ohos actual 的实现) |
| 上游归档 | MiddleEllipsisText() composable | Compose 侧完整实现,composeMain 静置待工具链对齐 |
| Native | MiddleEllipsisCompute / MiddleEllipsisVersion | C ABI JSON 导出 |
| N-API | middleEllipsisCompute(req) / middleEllipsisVersion() | 向 ArkTS 暴露能力 |
| ArkTS | measurePrefixWidths(text) / compute() | 前缀测量与触发重算 |
4.4 ArkTS 与 Kotlin 的边界
const prefixWidths: number[] = [0];
for (let i = 1; i <= text.length; i++) {
prefixWidths.push(measure.measureText({
textContent: text.substring(0, i), fontSize: fp2px(18)
}));
}
const res = JSON.parse(middleEllipsisCompute(JSON.stringify({
text: text, maxWidth: vp2px(220), prefixWidths: prefixWidths,
ellipsisChar: '.', ellipsisCharCount: 3, ellipsisWidth: px('.')
})));
// res.ok === true; res.result === 'head...tail'; res.truncated === true
val result = middleEllipsize(
text = text,
prefixWidths = prefix, // text.length + 1 个累计宽度
maxWidth = json.dbl("maxWidth", 0.0).toFloat(),
ellipsisChar = json.str("ellipsisChar").ifBlank { "." }.first(),
ellipsisCharCount = json.int("ellipsisCharCount", 3),
ellipsisWidth = json.dbl("ellipsisWidth", 0.0).toFloat(),
)
两者之间只传输扁平 JSON 字符串与数字数组,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:算法留 Kotlin,测量与渲染留 ArkTS
备选有三:整库 ArkTS 重写、等 Compose-ohos 工具链、算法移植 + 宿主测量。重写无法证明与上游语义一致;等待则永远交付不了。库的价值就是那 100 行贪心算法——把它留在 Kotlin,JVM 可测、ohos 可编;宽度和像素本是宿主的领地,ArkUI measureText 与最终渲染同引擎,测量/渲染天然自洽。这与 texty(时序引擎留 Kotlin,ArkTS 测 textWidth)一脉相承,本库更进一步:把全部测量事实(前缀宽度数组)交给宿主。
决策 2:工具钉 2.2.21-1.0.0 + Gradle 8.14.1
引擎零依赖,钉版没有 KStore 那种元数据强制力,跟着社区验证充分度走:qrose / texty 两个已过审仓库都在这条线上,配套 Gradle 8.14.1、JDK 21。上游 wrapper 8.7 升到 8.14.1 是本线的要求;KStore 的 2.4.20/9.x 经验是依赖倒逼的产物,不适用于零依赖库——两条工具链线的经验不能互相套用。
决策 3:引擎重投影而非整库编译
KStore 能"真编译整库"是因为它的 commonMain 本来就是纯 Kotlin。本库上游实现 100% 站在 androidx.compose.* 上,唯一可行的忠实路线是把算法逐行抽成纯函数:TextLayoutResult.getBoundingBox(...) 换成前缀宽度入参,其余控制流(双指针、交替条件、簇消费、复核弃簇)原样保留。上游 composable 零改动归档 composeMain,等社区 CMP-ohos 工具链对齐后可整体接回——届时 middleEllipsize() 直接成为 composable 的测量内核,一行不用改。
决策 4:前缀宽度契约
宽度跨桥有三种写法:传字号让 Kotlin 自己算(没有文本引擎,不可行)、逐字符宽度(代理对半截失真)、前缀累计宽度数组(本方案)。前缀差的望远镜性质保证簇宽守恒:簇内怎么拆,加起来都是整簇宽度;n + 1 次 measureText 对演示级文本完全可负担,长文本场景由宿主缓存解决。省略符宽度按"单字符宽 × 个数"计,与上游一致。
决策 5:双模块进构建 + 40 项新测试锚定
上游 Material 2 / Material 3 双制品共享同一算法,两个模块都进 ohos 构建(成本近零,审核面与上游目录一致)。上游 commonTest 本就被作者注释掉,没有"零改动复用"的选项——新建 40 项语义等价测试是唯一能把"逐行移植没漂"变成可验证命题的方式;测试第一轮抓出的 5 个期望值错误(见 8.1 第 5 条)恰好证明了它的价值。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12.101)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :MiddleEllipsisText:jvmTest :MiddleEllipsisText3:jvmTest
40 项用例全部通过(两模块同构,各 20 项):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
GraphemeClusterBreakIteratorTest | 10 | 分词近似:ASCII、合成重音、假名、浊点、旗帜对、ZWJ 家庭、肤色修饰、谚文、CRLF、变体选择符 |
MiddleEllipsizeTest | 10 | 引擎语义:空串/放得下直通、ASCII 贪心、左右交替左优先、自定义省略符/个数、emoji 簇永不切开、混合宽度平衡、预算耗尽只剩省略串、入参校验 ×2 |
引擎在 commonMain,JVM 与 ohos 跑同一份代码——JVM 全绿就是"编进 libohosmiddleellipsistext.so 的那串机器码的源码"语义没漂。唯一双平台差异点是 getBreakIterator() 的 actual(JVM 用 java.text 全量 ICU 规则,ohos 用近似分词器),而分词器被直接实例化测试,差异被圈定在已知限制内。
6.3 HAP 验证
:MiddleEllipsisText:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:MiddleEllipsisText3:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: MiddleEllipsisCompute / MiddleEllipsisVersion / libohosmiddleellipsistext_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libmiddleellipsistext_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
debugShared 未 strip 的 KN so arm64 约 3.6MB、x86_64 约 3.3MB(引擎零依赖,不带 coroutines 运行时,明显小于 KStore 的 14.8MB/13.2MB);每个 ABI 另配 libc++_shared.so(约 1.8MB / 1.7MB),由 copyKnSo* 任务自动回填。
6.4 功能用例
- 启动后标题区绿色徽标显示
1.2.0-ohos.1(来自middleEllipsisVersion()真实 Native 调用,非静态文字); - 默认文本为仓库 AtomGit 地址,超宽即截断,预览区呈
head...tail形态,下方对照的原文为尾部省略; - 点"日文混排"预设,信息面板显示头尾各保留 3 字符(约 290px/289px),完整宽度 4585px 远超可用 770px(220vp),
truncated=true; - 拖动宽度滑杆 80–340vp,截断位置实时变化;拖到最宽时
truncated=false且预览等于原文; - 点"Emoji"预设,任何宽度下预览无乱码、无半个字形——ZWJ 家庭/肤色修饰/旗帜对整簇进出;
- 省略字符改为
…、个数改为 1 或 6,结果随之变化,信息面板省略串宽同步; - 拖到最窄,预览只剩省略串(头尾 0 字符);
- 信息面板测量耗时与计算耗时分行显示(真机实测约 3ms / 0ms 级),每步操作日志按时间戳滚动留痕。
6.5 验证结论
截断引擎、字素分词器、Native 动态库、C ABI、N-API、ArkUI 测量/渲染页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的中间省略会话:版本握手、多字节安全截断、宽度/省略符实时联动与完整宽度证据链(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/MiddleEllipsisText.git
cd MiddleEllipsisText
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :MiddleEllipsisText:compileKotlinOhosArm64 :MiddleEllipsisText3:compileKotlinOhosArm64 # klib
.\gradlew.bat :MiddleEllipsisText:jvmTest :MiddleEllipsisText3:jvmTest # 40 项交叉验证
.\gradlew.bat :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.middleellipsistext -m entry
hdc shell hilog | grep MiddleEllipsis
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:MiddleEllipsisText/:MiddleEllipsisText3真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游 Compose 源码必须整体移出编译路径:仅去掉 compose 依赖不够,只要文件还在注册的源码集里,KN 编译就会在
@Composable上炸——归档到不注册的src/composeMain是唯一干净解; - 上游
expect getBreakIterator()在新目标下必须补 actual,否则 ohos 编译失败;actual 的实现(纯 Kotlin 分词器)放 commonMain 才能被 JVM 测试直接覆盖; - 引擎测试的期望值不能凭直觉写:上游"消费整簇后复核宽度、超预算则弃簇"的语义比直觉严——60px 预算下
"abcdefghij"的结果是"ab...j"(头 2 尾 1)而不是对称的"ab...ij";第一版期望值写错 5 处,回读上游源码逐条修正后全绿,这正是语义测试的意义; - ArkTS 不能直接
importKN so(HarmonyOS CAdapter 不导出业务函数),必须经 CMake NAPI 包装层; - 只拷 KN so 不拷
libc++_shared.so会在dlopen时失败,copyKnSo*任务要把三件套一起回填(从~/.konan/dependencies取); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可(模拟器是 x86_64,真机是 arm64); build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(报00303038 must have required property 'material')——仓库内留空数组,勾选自动签名后由 IDE 回填完整配置;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 桥响应字段名要与 ArkTS 读取端逐一对齐:本次真机截图发现"省略宽度"显示
?,排查为 Demo 读ellipsisWidth而桥返回ellipsisTextWidth,字段名一处不一致就让防御性读取回落成?——跨语言契约的字段名要有测试或对表把关; - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- HarmonyOS 上没有 Compose:
MiddleEllipsisText()composable 静置在composeMain,等社区 CMP-ohos 工具链对齐后整体接回(引擎届时直接成为测量内核); - 省略符宽度按"单字符宽 × 个数"近似(与上游一致),宽字形省略符(如
…)由宿主实测单字符宽度传入; GraphemeClusterBreakIterator是 UAX #29 精选范围近似,冷门脚本可能切得比 ICU 细,后果只是省略位置早一簇,不会切开代理对;- ArkTS
measureText与 Compose Skia 测量存在亚像素差异;桥的测量与渲染同为 ArkUI 引擎,自洽; - Compose 版的
onTextLayout回调、softWrap、多行语义不在桥协议里(中间省略以单行为前提,maxLines = 1); - 每次重算需
n + 1次measureText,千字级长文本建议宿主缓存前缀宽度或改二分测量; - android / iOS 目标剥离(composeMain 留原件),AGP / Roborazzi / Sonatype 发布链一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.middleellipsistext匹配的签名。
8.3 未来优化方向
- 跟进社区 CMP-ohos 工具链,对齐后把
composeMain的 composable 接回,middleEllipsize()作为共享内核同时服务 Compose 与 ArkTS 宿主; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
- 长文本二分测量与前缀宽度缓存,替代全量
n + 1次测量。
九、总结
9.1 核心难点回顾
上游 MiddleEllipsisText + MiddleEllipsisText3(实现 100% 在 Compose)
→ 耦合度验证(算法只依赖 bbox 宽度数字,可解耦)
→ 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1,JDK 21)
→ 根构建替换(cmp_qrose 模式,composeMain 隔离)
→ 上游算法逐行移植 + 纯 Kotlin 字素分词器
→ ohosArm64 / ohosX64 KLIB(双模块)
→ 前缀宽度契约 + C ABI(@CName × 2)
→ C++ N-API(DisposeString 纪律)
→ ArkTS measureText 前缀测量
→ ArkUI 真机双 Tab 验收(截断 / 预设 / 宽度联动 / 版本握手)
9.2 封装层次
- 引擎层:上游贪心双指针、字素簇预算、复核弃簇语义的纯 Kotlin 重投影;
- 分词层:
BreakIteratorexpect/actual 接缝 + UAX #29 近似分词器(JVM 基准为 java.text); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:前缀宽度测量、JSON 校验、状态刷新、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线跟着"耦合面"走,而不是跟着库类型走:同为 CMP 三方库,KStore 的 commonMain 本来就纯,答案是真编译整库;本库实现全在 composable 里,答案是算法重投影。判断依据只有一个——上游与平台的接触面能不能收敛成数字和纯函数,收敛不了就换路线;
- 测量类契约要选"守恒量":逐字符宽度在代理对上会失真,前缀累计宽度的差分在任何簇边界上 telescope 守恒。跨语言传测量事实时,先想清楚哪一种表示在边界情况下仍然自洽,再定协议;
- 语义等价要靠测试钉,不靠"逐行"自我声明:逐行移植也可能因为期望值想当然而出错——40 项测试第一轮抓出 5 个错误期望,修正的依据是回读上游源码的复核弃簇语义。"移植 + 语义测试 + 交叉验证"三件套缺一不可。
9.4 适配成果
当前适配已完成:
- MiddleEllipsisText
v1.2.0截断算法的双 ABI Kotlin/Native 编译(上游 composable 零改动归档,引擎逐行移植 + 40 项新测试锚定); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,双 ABI); - 前缀宽度测量契约(ArkTS
measureText↔ Kotlin 贪心引擎,代理对安全); - DevEco Stage ArkUI 双 Tab 验收页面(实时截断、四组预设、宽度/省略符联动、信息面板、操作日志、版本握手);
- 40 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- 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)