开源鸿蒙平台 CMP 三方库「Highlight」适配全流程
本文记录
Highlight接入 OpenHarmony 的完整过程,覆盖正则高亮库盘点、xregex依赖的 vendor 处置、匹配引擎真编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、addOrMerge富文本合并算法的移植、ArkUI 真机页面、HAP 构建和真机验收。
Highlight是基于正则表达式的跨平台文本 / 语法高亮库:给定一组Scheme(每个 Scheme 是「一个XRegex+ 一个Matcher」),它把命中结果变成平台富文本——Compose 侧是AnnotatedString+SpanStyle,View 侧是SpannedString。本次适配与 KStore 的"制品线换线"路线形成互补:上游依赖com.neoutils.xregex:xregex:1.0.0-rc1只发布了 jvm / js 变体,ohos 上永远解析不到——因此把它的 90 行 commonMain 源码按 MIT 许可原样 vendor 进仓库、补 jvm / ohos 两套 actual;同时上游的富文本层(toAnnotatedString/addOrMerge碰撞切分合并)站在 ComposeSpanStyle上编不进 ohos,算法被移植成纯颜色 Style 的桥内实现,ArkTS 拿到的是切分合并完毕的 segments,用Span渲染。匹配引擎(Highlight/Scheme/Matcher/UiColor)一行未改,运行在真机上。
项目地址: AtomGit/oh-tpc/Highlight.git
上游仓库: NeoUtils/Highlight
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Highlight 为 Kotlin Multiplatform 应用提供"正则命中 → 富文本"这一件事的完整答案:Highlight { textColor { ... } } 一行建高亮器,TextColorScheme / BackgroundColorScheme / SpanStyleScheme 三种着色语义,Matcher.fully(...) 整体命中一色、Matcher.groups(...) 每个捕获组一色,Scheme.range 还能把匹配限定在文本区间内。上游在 Compose 侧用 toAnnotatedString / rememberAnnotatedString / rememberTextFieldValue 把结果接进 Text 与 BasicTextField,其中的 addOrMerge 负责处理多 scheme 命中区域重叠的碰撞切分与样式合并。KMP 项目把匹配引擎放在 commonMain,由 Android、桌面、Web 共享。OpenHarmony 应用同样需要这套能力(搜索词高亮、代码编辑器、日志着色),但上游构建(Kotlin 2.1.21 / CMP 1.8.2 / AGP 8.11.2)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示彩色文字,却无法证明匹配引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证引擎、正则抽象层、富文本合并算法的移植、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 依赖无 ohos 制品 | 引擎 API 直接暴露 com.neoutils.xregex.XRegex 类型(Scheme.regex),而 xregex 1.0.0-rc1 在 Maven Central 只有 jvm / js 变体——不是版本后缀问题,是根本不存在。 |
| Android 注解混入 | UiColor.Integer 上的 @ColorInt 把 androidx.annotation 拖进 commonMain 编译。 |
| 富文本层跨不了桥 | toAnnotatedString 站在 Compose AnnotatedString / SpanStyle 上,社区 CMP 1.9.2 线编不进上游 Compose 1.12 的 UI 层;SpanStyle 也过不了 C ABI。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:匹配引擎的 ohos 真编译、xregex 的 vendor 内联、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。匹配与合并语义完全由上游原版实现负责(合并算法为逐行移植),ArkTS 只负责调用 Native、解析 JSON 和渲染 Span。
1.2 库提供的能力
Demo 提供五个 Tab,对应桥接的全部入口:
- 文本颜色(text):
TextColorScheme+Matcher.fully,color一词整体着色——对应上游 README 的 Quick Start; - 背景色(background):
BackgroundColorScheme(蓝)与TextColorScheme(白)两个 scheme 命中同一个词,addOrMerge把它们合并成"蓝底白字"一段——对应上游 View 示例的双 scheme 叠加; - 捕获组(groups):
("\w+")\s*=\s*("\w+")+Matcher.groups(Blue, Green),两个捕获组两种颜色; - 代码高亮(code):
fun关键字、函数调用(两组捕获)、@注解、字符串字面量四条 scheme 组合,复刻上游CodeHighlightExample; - 自定义(custom):任意正则 +
textColor/backgroundColor+ 单色或分组色串,实时验证引擎的通用性。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文(非法正则不崩溃)。 |
kind / count | 回显样例类型与着色段数。 |
segments | 切分合并完毕的 [{text, color, background}] 数组,ArkTS ForEach 直接渲染 Span。 |
matches | [{text, start, end}] 命中明细,状态行"匹配 N 处"的数据来源。 |
引擎侧完整能力(Scheme.range 区间限定、捕获组索引着色、UiColor 三态、DSL builder)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 7 项 jvmTest 交叉验证用例(上游本无测试,本仓新增),证明 ohos .so 里执行的匹配逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | highlight-core 的 15 个 commonMain 文件原样编译(仅剥一个 @ColorInt 注解);xregex 90 行 common 源码按 MIT 原样 vendor,3 个 expect 在 jvm / ohos 各补 actual。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);highlight-compose、highlight-view-based、build-logic 与四个上游 example 留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI(HighlightRender)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 算法移植 | addOrMerge 碰撞切分合并逐行移植成纯颜色 Style(color, background)(非空覆盖合并语义对齐 SpanStyle.merge),只在桥内,不进库 public API。 |
| UI 完整 | 五 Tab 覆盖全部桥接入口:文本色 / 背景色 / 捕获组 / 代码高亮 / 自定义正则,非法输入返回 ok:false 不崩溃。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的价值一半在引擎、一半在 Compose 富文本扩展;ohos 侧把引擎真编译、把合并算法移植进桥,ArkTS
Span只做最后一步渲染。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点三模块依赖边界、验证 xregex 制品线(结论:无 ohos 变体)、确定 vendor 路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1、vendor xregex + 剥注解
第 3 阶段:桥协议建模 ── kind/schemes JSON 契约、UiColor → #RRGGBB、addOrMerge 移植、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 五 Tab、TextArea 实时重高亮、ForEach Span 渲染、状态行
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
highlight/core/ 上游匹配引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
highlight/core/.../xregex/ vendor:xregex 1.0.0-rc1 的 9 个 common 文件 + jvm/ohos actual
highlight/compose/ 上游 Compose 富文本扩展,留盘不进 settings
highlight/view-based/ 上游 Android View/Spannable 扩展,留盘不进 settings
build-logic/ 上游 convention plugin(AGP + 发布),留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + addOrMerge 移植)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
example/{compose,view-based,desktop,web}/ 上游示例,留盘不进构建
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/img/ 真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :highlight:core 与 :example:nativeApp,rootProject.name = "ohos_highlight"。上游的 AGP / CMP / vanniktech 发布插件、build-logic 组合构建、js/js-store 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze 同款已审线) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium 21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.13 升一格;2.2.21 旧线的要求,与 KStore 的 2.4.20 线留 9.x 不同) |
| DevEco product | 5.1.0(18) 兼容、hvigor modelVersion 5.1.0 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.highlight |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
highlight-core | 15(全 commonMain) | 纯 Kotlin + xregex + androidx.annotation(仅一个 @ColorInt) | 整体编译(jvm + 双 ohos),剥注解、vendor xregex |
highlight-compose | 12(common + android + jvm + js) | CMP runtime / ui(AnnotatedString / SpanStyle / remember) | 留盘不进构建(UI 层编不进社区 CMP) |
highlight-view-based | 16(全 androidMain) | Android Spannable / TextWatcher / appcompat | 留盘不进构建(无鸿蒙对应物) |
与 KStore"全库只有一个 expect"不同,highlight-core 自身零 expect,麻烦全在依赖上。Scheme.regex: XRegex 把 xregex 钉进公开 API 签名,.module 元数据验证的结论是残酷的:
com.neoutils.xregex:xregex:1.0.0-rc1
variants: jvmApi-published / jvmRuntime-published / jsApi-published / jsRuntime-published / metadataApiElements
(没有 ohosArm64 / ohosX64,也没有任何原生 target 变体)
好消息是这 90 行 common 源码只有一个外部假设——kotlin.text.Regex。处置:9 个文件逐字拷入 highlight/core/src/commonMain/kotlin/com/neoutils/xregex/,仅有的 3 个 expect 声明在 jvm / ohos 各补一份 actual。同作者、同 MIT 许可,vendor 无合规问题。
1.4 页面边界
标题区 Highlight · OpenHarmony Demo(libhighlight_napi.so → libohoshighlight.so)
Tab 切换 文本颜色 / 背景色 / 捕获组 / 代码高亮 / 自定义
前四 Tab 提示行(当前 kind 的语义说明)+ TextArea(预置样例文本,可编辑)
+「应用高亮」按钮 + 结果卡片(ForEach Span 渲染 segments)+ 状态行
自定义 Tab 额外提供 pattern 输入(默认 \bcolor\b)、textColor/backgroundColor 选择、
单色输入(默认 #E66123)、分组色串输入(如 #0000FF,#008000)
状态行 高亮 N 段 · 匹配 M 处;失败时显示 失败: <error 原文>
页面切换 Tab 时自动重置预置文本,点「应用高亮」才发起 Native 调用——每次调用都是一次完整的"正则匹配 + 碰撞合并 + 切分"管线。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
KStore 的经验是"先验证生态制品线再定适配路线"。对 Highlight 跑一遍同样的验证,结论走向另一个分支:eazytec Maven 上没有 com.neoutils.xregex 的 ohos 制品,Maven Central 上也没有(上游只发过 jvm / js)。"换线不换版"不可用,可选路线只剩三条:
| 路线 | 评估 |
|---|---|
改 API:XRegex 换成 kotlin.text.Regex | 动 Scheme / 两个 Scheme / 两个 Scope 共 6 个文件的公开签名,上游 diff 面最大,审核叙事最差。 |
| 等上游发 ohos 变体 | 上游 xregex 停在 1.0.0-rc1,不可控。 |
| vendor:源码内联 + 补 actual | 90 行纯 Kotlin 逐字拷贝,签名零变化,diff 面集中在"新增目录"。✅ |
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
Highlight 没有新版 kotlinx 依赖(引擎只用 kotlin.text),不存在 KStore 那种"coroutines 元数据 2.4.0 逼着 Kotlin 升 2.4.20"的压力——直接沿用社区已审的 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同线)。该线要求 Gradle 8.x,上游 wrapper 是 8.13,实测升到 8.14.1 与 cmp_qrose 完全对齐。工具链版本跟着依赖走,两条线(2.2.21 降 Gradle、2.4.20 留 9.x)的经验不能互相套用。
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_highlight"
include(":highlight:core")
include(":example:nativeApp")
2.3 编译真实引擎
highlight/core/build.gradle.kts 从"convention plugin + Android + 发布"收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
三处源码处置:UiColor.Integer 剥掉 @ColorInt 与 androidx.annotation import(commonMain 里唯一的 Android 符号);xregex 9 个 common 文件原样 vendor;3 个 expect 的 actual 这样写(jvm / ohos 同构):
// highlight/core/src/ohosMain/kotlin/com/neoutils/xregex/extension/XRegex.ohos.kt
actual fun XRegex.findAll(
text: CharSequence,
range: IntRange
): List<XMatch> {
val regex = Regex(
pattern = pattern,
options = flags.mapNotNull { it.toRegexOption() }.toSet() // i/m/s/x → RegexOption
)
return regex.findAll(text, range.first)
.takeWhile { it.range.first <= range.last } // range 语义 = 上游 scheme.range
.mapIndexed { index, result ->
XMatch(
index = index,
text = result.value,
range = result.range,
groups = result.groups.mapIndexed { groupIndex, group ->
if (group == null) return@mapIndexed null
XMatch.Group(index = groupIndex, text = group.value, range = group.range)
}
)
}.toList()
}
实测:
./gradlew :highlight:core:compileKotlinOhosArm64 :highlight:core:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :highlight:core:jvmTest # 7 项全绿
引擎的 15 个 commonMain 文件一个没动(除剥注解),vendor 的 9 个文件逐字保留。
2.4 构建隔离的处置
上游四个 example(compose / view-based / desktop / web)、highlight-compose、highlight-view-based、build-logic 组合构建全部留盘不进 settings.gradle.kts;kotlin-js-store/(yarn.lock,服务于已剥离的 js target)从仓库删除并加入 gitignore。根构建全局禁用 ohos 测试任务(commonTest 实际跑在 jvm 上):
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
com.neoutils.highlight.core/**(上游引擎 + vendor xregex,原样编译)
↓ Highlight / Scheme / Matcher / XRegex.findAll
Bridge.kt(@CName × 1,扁平 JSON 进出 + addOrMerge 移植)
↓ UTF-8 JSON
N-API highlightRender(request)
↓
ArkUI Index.ets(五 Tab,ForEach Span 渲染)
请求与响应:
请求 : {"kind":"text|background|groups|code|custom","text":"..."}
custom 追加:{"pattern":"\\bcolor\\b","type":"textColor|backgroundColor",
"color":"#E66123","colors":"#0000FF,#008000"}
响应 : {"ok":true,"kind":"...","count":N,
"segments":[{"text":"...","color":"#RRGGBB"|null,"background":"#RRGGBB"|null},...],
"matches":[{"text":"...","start":N,"end":N},...]}
{"ok":false,"error":"..."}
前四个 kind 在桥内使用与上游示例逐条对应的预置 scheme 集;custom 走通用构造路径(TextColorScheme / BackgroundColorScheme 直接实例化),一条通路同时覆盖两种着色与 fully / groups 两种匹配语义。
3.2 为什么输出 segments 而不是 AnnotatedString
上游 Compose 层的核心是 toAnnotatedString:遍历 scheme → regex.findAll → 按捕获组查 matcher.matches → addOrMerge 处理重叠。AnnotatedString / SpanStyle 编不进 ohos,但算法是纯区间运算。桥内用 (color: Long?, background: Long?) 的 Style 替代 SpanStyle,合并语义对齐 SpanStyle.merge(非空覆盖):
// example/nativeApp/.../Bridge.kt —— addOrMerge 的纯颜色移植(逐行对应上游 compose 扩展)
private fun MutableList<Span>.addOrMerge(span: Span) {
val collisions = filter { it.start < span.end && span.start < it.end }
if (collisions.isEmpty()) { add(span); return }
removeAll(collisions)
addAll(collisions.flatMap {
buildList {
if (it.start < span.start) add(it.copy(end = span.start)) // 左残段
add(Span(it.style.merge(span.style), // 重叠段:非空覆盖
maxOf(it.start, span.start), minOf(it.end, span.end)))
if (it.end > span.end) add(Span(it.style, span.end, it.end)) // 右残段
}
})
add(span)
}
UiColor 三态统一转 ARGB 再格式化:Hex / Rgb / Integer → alpha 为 FF 时输出 #RRGGBB,否则 #AARRGGBB。渲染端 ArkTS Span 原生支持这两个格式。
3.3 错误通道
整个导出函数一个 try/catch,Kotlin 异常永远不跨 FFI:
@CName("HighlightRender")
fun highlightRender(request: String): String = try {
/* kind 分发 → 样例/自定义 scheme → render → ok(...) */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、custom 缺 pattern、正则编译失败,全部走同一个 ok:false 通道——Demo 的自定义 Tab 输入 [unclosed 这类坏正则时,页面显示"失败: …"而不崩溃,这正是真机验收用例之一。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libhighlight_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoshighlight.so)
│ HighlightRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制匹配语义,所以选择 C ABI + JSON。与 KStore 的双符号不同,Highlight 的语义可以收敛为一个纯函数(文本 + 规则 → 着色结果,无状态、无版本握手需求),所以只有一个 @CName。
4.2 Kotlin/Native 导出函数
@CName("HighlightRender")
fun highlightRender(request: String): String // 五种 kind 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
HighlightRender | highlightRender(req) | scheme 构建 + findAll + addOrMerge + segments 切分 |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"highlightRender", nullptr, NapiHighlightRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 HighlightRender 并包装返回。KN 返回的字符串必须经 libohoshighlight_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoshighlight_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoshighlight SHARED IMPORTED)
set_target_properties(ohoshighlight PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(highlight_napi SHARED napi_init.cpp)
target_link_libraries(highlight_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoshighlight)
OHOS_ARCH 为空或 libohoshighlight.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const highlightRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 组装请求和触发渲染
private applyHighlight(kind: string): void {
const request: Record<string, string> = { 'kind': kind, 'text': this.text };
if (kind === 'custom') {
request['pattern'] = this.pattern;
request['type'] = this.customType;
request['color'] = this.colorText;
request['colors'] = this.colorsText;
}
try {
const raw: string = highlightRender(JSON.stringify(request));
const parsed: HighlightResult = JSON.parse(raw) as HighlightResult;
if (!parsed.ok) { this.status = `失败: ${parsed.error ?? 'unknown error'}`; return; }
this.segments = parsed.segments ?? [];
this.matches = parsed.matches ?? [];
this.status = `高亮 ${this.count} 段 · 匹配 ${this.matches.length} 处`;
} catch (e) { /* Native 异常同样收进状态行,不抛出 */ }
}
渲染端一个 ForEach 解决全部着色语义——四分支正好对应桥的 color / background 可空组合:
ForEach(this.segments, (seg: Segment, idx: number) => {
if (seg.color && seg.background) {
Span(seg.text).fontColor(seg.color).backgroundColor(seg.background)
} else if (seg.color) {
Span(seg.text).fontColor(seg.color)
} else if (seg.background) {
Span(seg.text).backgroundColor(seg.background)
} else {
Span(seg.text)
}
}, (seg: Segment, idx: number) => `${idx}`)
5.2 页面交互
五个 Tab 自上而下:提示行(当前 kind 的语义说明,如"演示 BackgroundColorScheme —— 匹配区域加背景色")、TextArea(预置样例文本,可编辑后重新点按钮验证)、「应用高亮」按钮、结果卡片(浅灰底圆角,Text 内 ForEach 渲染 Span)、状态行(高亮 N 段 · 匹配 M 处 或红色错误文本)。自定义 Tab 额外提供 pattern / 类型选择 / 单色 / 分组色串四个输入。
5.3 错误边界
ok:false 时错误原文进状态行(红字 #B91C1C),结果区清空;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并落 hilog,页面永不崩溃。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoshighlight)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + MiniJson + addOrMerge 移植
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.highlight、versionName 2.3.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/libohoshighlight_api.h
├── src/main/cpp/types/libhighlight_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 五 Tab 验收页面
6.2 构建、签名和安装
./gradlew :highlight:core:jvmTest # 7 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.highlight -m entry
四、完整代码对照
4.1 整体架构
com.neoutils.highlight.core/**(上游引擎原样编译)
│ Highlight / Scheme / Matcher / UiColor / range 合并
com.neoutils.xregex/**(vendor,原样编译)
│ XRegex / XMatch / findAll expect→actual
Bridge.kt + MiniJson
│ HighlightRender:kind 分发 + addOrMerge 移植 + segments 切分
libhighlight_napi.so
│ highlightRender
Index.ets
│ JSON parse + ForEach Span
▼
ArkUI 真机页面(五 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_highlight、compose/view/上游 example 移出 |
gradle/libs.versions.toml | 只留 Kotlin 2.2.21-1.0.0 + KMP 插件别名 |
build.gradle.kts(根) | 版本 2.3.0-ohos.1、全局禁用 Ohos*Test* 任务 |
highlight/core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64 + commonTest |
highlight/core/.../xregex/**(9 文件) | vendor 的 xregex common 源码,逐字保留 |
highlight/core/src/{jvm,ohos}Main/.../xregex/(各 3 文件) | 3 个 expect 的 actual(kotlin.text.Regex 实现,flags i/m/s/x 映射) |
highlight/core/src/commonTest/.../HighlightTest.kt | 新增 7 项交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、五种 kind、MiniJson、addOrMerge 移植、错误通道 |
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 | 鸿蒙适配主文档(22 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | Highlight { textColor { ... } } | DSL 建高亮器(HighlightScope / TextColorScope) |
| 引擎 | TextColorScheme / BackgroundColorScheme | 前景 / 背景着色 scheme(Scheme<UiColor> 实现) |
| 引擎 | Matcher.fully(v) / Matcher.groups(...) | 整体命中一色 / 按捕获组索引着色 |
| 引擎 | Scheme.range | 匹配限定在文本区间(IntRange.merge 交集) |
| 引擎 | UiColor.Hex / Rgb / Integer | 颜色三态,桥内统一转 #RRGGBB |
| vendor | XRegex.findAll(text, range) | 正则命中(ohos actual 基于 kotlin.text.Regex) |
| 移植 | addOrMerge(Span) | 重叠碰撞切分 + 非空覆盖合并(对应上游 SpanStyle.merge) |
| Native | HighlightRender | C ABI JSON 导出(唯一符号) |
| N-API | highlightRender(request) | 向 ArkTS 暴露能力 |
| ArkTS | applyHighlight(kind) | 组请求、刷新 segments / 状态行 |
4.4 ArkTS 与 Kotlin 的边界
const raw = highlightRender(JSON.stringify({
kind: 'custom', text: '"name" = "Irineu"',
pattern: '("\\w+")\\s*=\\s*("\\w+")', type: 'textColor',
colors: '#0000FF,#008000'
}));
// res.ok === true;segments = ["\"name\""(蓝), " = ", "\"Irineu\""(绿)]
val scheme = TextColorScheme(
regex = pattern.toXRegex(),
matcher = Matcher.groups(UiColor.Hex("#0000FF"), UiColor.Hex("#008000")),
)
for (result in scheme.regex.findAll(text, text.range)) {
for (group in result.groups) {
val color = scheme.matcher.matches[group.index] ?: continue
spans.addOrMerge(Span(style(color), group.range.first, group.range.last + 1))
}
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或 SpanStyle。
五、关键决策说明
决策 1:vendor xregex,而不是改 API 或等制品
"换线不换版"的前提是 ohos 制品存在;xregex 的结论是不存在且上游不活跃。改公开签名(XRegex → Regex)会让 6 个引擎文件进 diff,违背"引擎零改动"的审核底线;vendor 让 90 行同 MIT 源码逐字进仓、差异集中在一个新增目录,3 个 expect 的 actual 是唯一的新代码——jvm 与 ohos 同构,还顺带保住了 jvm 交叉验证。
决策 2:富文本合并算法移植进桥,而不是硬编 Compose 或让 ArkTS 重写
toAnnotatedString / addOrMerge 是纯区间运算,与 Compose 的耦合只在 SpanStyle 类型上。把 SpanStyle 换成 (color, background) 可空对、merge 换成非空覆盖,算法逐行保留——ArkTS 拿到的 segments 已经是"碰撞处理完"的结果,页面零合并逻辑。反过来若在 ArkTS 重写合并,同一个库就会出现两份算法,漂移只是时间问题。
决策 3:工具链沿用 2.2.21-1.0.0 已审线,Gradle 8.14.1
KStore 因 coroutines 制品线被迫升 2.4.20;Highlight 引擎只依赖 kotlin.text,没有外部逼升因素,沿用 qrose / haze 已审的 2.2.21-1.0.0 线并把 Gradle 对齐 8.14.1。先盘点依赖矩阵,再选工具链线——这是两条适配唯一通用的规则。
决策 4:compose / view-based 模块留盘,UI 语义由桥 + ArkTS 承接
highlight-compose 站在上游 CMP 1.8.2 上、highlight-view-based 站在 Android Spannable 上,前者待社区 CMP 支持后可接回,后者在鸿蒙没有对应物。留盘不删除:源码就是"接回条件满足时"的现成参考,也是改动面审计的对照物。
决策 5:上游无测试,自建 7 项 commonTest 交叉验证
KStore 有上游 51 项测试可零改动复用;Highlight 上游没有任何测试。引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码——新增 7 项测试(整词命中、捕获组、range 限定、flags 映射、DSL 等价、区间合并、findAll 稳定性)钉住 vendor 层与引擎的接缝,配合 llvm-nm 符号验收,构成"内核没漂"的闸门。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x86_64)、JDK 21(Eclipse Adoptium 21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :highlight:core:jvmTest
7 项用例全部通过(本仓新增):
| 用例 | 钉住什么 |
|---|---|
matchesFullWord | \b 整词命中与 range 下标(11…15) |
matchesGroups | 捕获组文本与顺序("name" / "Irineu") |
respectsSchemeRange | Scheme.range 区间限定:跳过首次命中 |
flagsMapToRegexOptions | vendor flags i → RegexOption.IGNORE_CASE 映射 |
builderProducesSameSchemes | DSL builder 与直接构造产出等价 scheme(vendor 转换无损) |
rangesMergeByIntersection | IntRange.merge 交集语义 |
xRegexFindAllIsStable | vendor findAll 多命中顺序与 group 0 一致性 |
6.3 构建与符号验证
:highlight:core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:highlight:core:jvmTest: 7 项全部通过
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: KN so + libc++_shared.so 回填 entry/libs/<abi>/
llvm-nm --defined-only: HighlightRender / libohoshighlight_symbols 均为全局 T 符号(双 ABI)
hvigor assembleHap: finished
install bundle successfully / start ability successfully
debugShared 产物:KN so arm64 约 3.8 MB、x86_64 约 3.4 MB(含 Kotlin/Native 运行时与正则引擎),libc++_shared.so 约 1.9 MB / 1.7 MB;HAP 内双 ABI 各三件套(libhighlight_napi.so / libohoshighlight.so / libc++_shared.so)。
6.4 功能用例
- 启动进入五 Tab 页面,标题行显示
Highlight · OpenHarmony Demo(libhighlight_napi.so → libohoshighlight.so)——桥链路一次可见; - 文本颜色 Tab:
color一词整体变橙(#E66123),状态行回显高亮段数与命中次数; - 背景色 Tab:
color同时被背景(蓝)与前景(白)两个 scheme 命中,合并渲染为蓝底白字一段——addOrMerge合并语义的真机证据(见 6.5 效果图); - 捕获组 Tab:
"name"蓝、"Irineu"绿,中间=保持原色——分组着色正确; - 代码高亮 Tab:
fun橙、函数名深青、参数高亮蓝、@Composable黄绿、字符串绿——四条 scheme 组合互不干扰; - 自定义 Tab:改 pattern / 类型 / 颜色后点应用,实时重新着色;
- 自定义 Tab 输入非法正则(如
[unclosed),状态行显示"失败: …",应用不崩溃; - 编辑任意 Tab 的文本再点应用,高亮随文本实时更新。
6.5 验证结论
匹配引擎、vendor 的 xregex、合并算法移植、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,五个 Tab 覆盖全部桥接入口(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Highlight.git
cd Highlight
export JAVA_HOME="/path/to/jdk-21"
./gradlew :highlight:core:compileKotlinOhosArm64 :highlight:core:compileKotlinOhosX64 # klib
./gradlew :highlight:core:jvmTest # 7 项交叉验证
./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.highlight -m entry
hdc shell hilog | grep Highlight
八、遗留问题与改进方向
8.1 踩坑复盘
kotlin.text.MatchGroup没有index属性——xregex 的XMatch.Group(index, ...)需要组号,映射时必须用mapIndexed拿下标,直接group.index编译不过(jvm / ohos 两套 actual 各踩一次);Scheme<*>星号投影下matcher.matches的元素类型退化为Any——toArgb(color)直接报类型不匹配,解法是在when (scheme)的 smart-cast 分支里访问scheme.matcher;Matcher泛型经 if 双分支 + spread 展开推断退化为Any——val matcher: Matcher<UiColor> = ...显式标注;- commonMain 里唯一的 Android 符号是
@ColorInt,不剥掉会把androidx.annotation拖进 ohos 编译——上游注解剥离是"零依赖内核"的前置动作(cmp_qrose 阶段 2 同款); - xregex 在 Maven Central 只有 jvm / js 变体,
.module元数据先验证再定路线,别等编译期才炸Could not resolve; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 2.2.21 旧线要求 Gradle 8.x,上游 8.13 升 8.14.1 与已审仓对齐;命令行跑 gradlew 还需
JAVA_HOME指向 JDK 21(org.gradle.java.home只管 daemon,不管启动器); - 上游
.gitignore的/build是根锚定,盖不住example/nativeApp/build/——必须改成非锚定build,否则git add -A会把 KLIB、so 等构建产物卷进提交; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI + KN + libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - IMPORTED 的 KN so 要
IMPORTED_NO_SONAME TRUE+$ORIGINrpath,否则 Windows 构建机的绝对路径会写进 RUNPATH; - 链接期 CAdapter 生成的
api.cpp:448[-Wreturn-type]警告是已知噪音(qrose 仓同款),不影响符号导出; - DevEco 打开
example/harmonyApp子目录而非仓库根,否则 hvigor 找不到模块边界;签名材料由 IDE 自动写入build-profile.json5,证书绝不进入仓库。
8.2 已知问题
SpanStyleScheme(fontStyle/fontWeight等富文本属性)未进桥——当前 segments 只有color/background两个通道,与Span的能力对齐即可覆盖主路径;- custom kind 限单 scheme、分组色仅接受
#RRGGBBHex 形式; highlight-compose/highlight-view-based留盘不编译:前者待社区 CMP 支持后接回,后者(Android Spannable / TextWatcher)在鸿蒙无对应物;- android / js 目标剥离(源文件留在树里),发布、convention plugin 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.highlight匹配的签名。
8.3 未来优化方向
- 桥增加
fontStyle/fontWeight/fontFamily通道,对齐SpanStyleScheme全语义; - custom kind 支持多 scheme 数组与
scheme.range区间参数; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)验收矩阵与 uitest 自动化截图。
九、总结
9.1 核心难点回顾
上游 highlight-core(15 个 commonMain 文件)
→ 验证 xregex 制品线(结论:无 ohos 变体)→ vendor 90 行 + 3 expect × 双平台 actual
→ 剥 @ColorInt、根构建替换(cmp_qrose 模式)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(已审线)
→ ohosArm64 / ohosX64 KLIB
→ C ABI(@CName × 1)+ addOrMerge 纯颜色移植 → segments
→ C++ N-API(DisposeString 纪律)
→ ArkTS ForEach Span 渲染
→ ArkUI 真机五 Tab 验收
9.2 封装层次
- 引擎层:上游原版
Highlight/Scheme/Matcher/UiColor与 DSL scope; - 正则层:vendor 的
XRegexcommon 源码 + jvm / ohos actual(kotlin.text.Regex); - 桥层:kind 分发、
addOrMerge移植、segments 切分、错误通道; - N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:五 Tab 状态管理、JSON 组包、
ForEachSpan渲染与状态行; - DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 制品线验证的结论决定路线:同样是"依赖挡路",KStore 等到了 ohos 制品线就走"换线不换版",Highlight 等不到就走"vendor + 补 actual"——两条路线都以
.module元数据的当天可达性为准,而不是历史结论或对上游的乐观预期; - 算法移植优于语义重写:富文本合并这类纯逻辑,逐行移植成平台无关形态(纯颜色 Style),比在 ArkTS 重写一份可靠得多——上游算法是经过 Compose 三个平台 exercised 的实现,diff 面就是可信度;
- 上游没有测试时,交叉验证要自建:引擎在
commonMain、JVM 与 ohos 同码,7 项针对 vendor 接缝与核心语义的测试加上llvm-nm符号验收,就是"内核没漂"的最小闸门。
9.4 适配成果
当前适配已完成:
- Highlight
2.3.0真实引擎的双 ABI Kotlin/Native 编译(上游commonMain零重写,仅剥一个注解); xregex 1.0.0-rc1的 vendor 内联(9 文件逐字 + 6 个 actual),ohos 侧正则能力与 jvm 完全同源;addOrMerge碰撞合并算法的纯颜色移植与 1 个 C ABI 符号、C++ N-API 包装层(llvm-nm验证全局 T 符号);- DevEco Stage ArkUI 五 Tab 验收页面(文本色、背景色合并、捕获组、代码高亮、自定义正则、非法输入不崩溃);
- 7 项 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)