本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot,已写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 wrapper 8.7 升级;2.2.21 线要求 8.14.1)
第三方依赖零(引擎无 coroutines / serialization / IO 依赖,无制品线对齐问题)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.middleellipsistext
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 内容依赖形态处置
MiddleEllipsisTextcomposable 1 个 + BreakIterator expect/actual100% androidx.compose.*,测量依赖 TextLayoutResult算法移植进 commonMain,Compose 部分归档 composeMain
MiddleEllipsisText3同上(Material 3 包)同上同构处理
sampleAppCMP 示例(依赖已发布 Maven 快照)Compose 1.6 / AGP 8.1留盘不进构建
build-logicAGP 约定插件 + 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 方法覆盖内核
MiddleEllipsisComputemiddleEllipsisCompute(req)前缀宽度 → 贪心双指针截断 → head...tail
MiddleEllipsisVersionmiddleEllipsisVersion()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.tomlKotlin 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.ktUAX #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.kt2 个 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 验收页面(measureText 前缀测量)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
引擎middleEllipsize(text, prefixWidths, maxWidth, …)纯函数截断:贪心双指针 + 字素簇预算(核心)
引擎MiddleEllipsisResult结果串 + truncated + 头尾字符数/宽度 + 省略串宽
分词getBreakIterator()expect/actual 接缝(上游原件)
分词GraphemeClusterBreakIteratorUAX #29 近似(ohos actual 的实现)
上游归档MiddleEllipsisText() composableCompose 侧完整实现,composeMain 静置待工具链对齐
NativeMiddleEllipsisCompute / MiddleEllipsisVersionC ABI JSON 导出
N-APImiddleEllipsisCompute(req) / middleEllipsisVersion()向 ArkTS 暴露能力
ArkTSmeasurePrefixWidths(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 项):

测试类用例数钉住什么
GraphemeClusterBreakIteratorTest10分词近似:ASCII、合成重音、假名、浊点、旗帜对、ZWJ 家庭、肤色修饰、谚文、CRLF、变体选择符
MiddleEllipsizeTest10引擎语义:空串/放得下直通、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. 启动后标题区绿色徽标显示 1.2.0-ohos.1(来自 middleEllipsisVersion() 真实 Native 调用,非静态文字);
  2. 默认文本为仓库 AtomGit 地址,超宽即截断,预览区呈 head...tail 形态,下方对照的原文为尾部省略;
  3. 点"日文混排"预设,信息面板显示头尾各保留 3 字符(约 290px/289px),完整宽度 4585px 远超可用 770px(220vp),truncated=true;
  4. 拖动宽度滑杆 80–340vp,截断位置实时变化;拖到最宽时 truncated=false 且预览等于原文;
  5. 点"Emoji"预设,任何宽度下预览无乱码、无半个字形——ZWJ 家庭/肤色修饰/旗帜对整簇进出;
  6. 省略字符改为 …、个数改为 1 或 6,结果随之变化,信息面板省略串宽同步;
  7. 拖到最窄,预览只剩省略串(头尾 0 字符);
  8. 信息面板测量耗时与计算耗时分行显示(真机实测约 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :MiddleEllipsisText / :MiddleEllipsisText3 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 Compose 源码必须整体移出编译路径:仅去掉 compose 依赖不够,只要文件还在注册的源码集里,KN 编译就会在 @Composable 上炸——归档到不注册的 src/composeMain 是唯一干净解;
  4. 上游 expect getBreakIterator() 在新目标下必须补 actual,否则 ohos 编译失败;actual 的实现(纯 Kotlin 分词器)放 commonMain 才能被 JVM 测试直接覆盖;
  5. 引擎测试的期望值不能凭直觉写:上游"消费整簇后复核宽度、超预算则弃簇"的语义比直觉严——60px 预算下 "abcdefghij" 的结果是 "ab...j"(头 2 尾 1)而不是对称的 "ab...ij";第一版期望值写错 5 处,回读上游源码逐条修正后全绿,这正是语义测试的意义;
  6. ArkTS 不能直接 import KN so(HarmonyOS CAdapter 不导出业务函数),必须经 CMake NAPI 包装层;
  7. 只拷 KN so 不拷 libc++_shared.so 会在 dlopen 时失败,copyKnSo* 任务要把三件套一起回填(从 ~/.konan/dependencies 取);
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可(模拟器是 x86_64,真机是 arm64);
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038 must have required property 'material')——仓库内留空数组,勾选自动签名后由 IDE 回填完整配置;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. 桥响应字段名要与 ArkTS 读取端逐一对齐:本次真机截图发现"省略宽度"显示 ?,排查为 Demo 读 ellipsisWidth 而桥返回 ellipsisTextWidth,字段名一处不一致就让防御性读取回落成 ?——跨语言契约的字段名要有测试或对表把关;
  12. 签名材料由 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 重投影;
  • 分词层:BreakIterator expect/actual 接缝 + UAX #29 近似分词器(JVM 基准为 java.text);
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:前缀宽度测量、JSON 校验、状态刷新、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线跟着"耦合面"走,而不是跟着库类型走:同为 CMP 三方库,KStore 的 commonMain 本来就纯,答案是真编译整库;本库实现全在 composable 里,答案是算法重投影。判断依据只有一个——上游与平台的接触面能不能收敛成数字和纯函数,收敛不了就换路线;
  2. 测量类契约要选"守恒量":逐字符宽度在代理对上会失真,前缀累计宽度的差分在任何簇边界上 telescope 守恒。跨语言传测量事实时,先想清楚哪一种表示在边界情况下仍然自洽,再定协议;
  3. 语义等价要靠测试钉,不靠"逐行"自我声明:逐行移植也可能因为期望值想当然而出错——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。

参考文档

Logo

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

更多推荐