开源鸿蒙平台 CMP 三方库「Thistle」适配全流程
本文记录
Thistle接入 OpenHarmony 的完整过程,覆盖富文本标记库盘点、解析器依赖 kudzu 的鸿蒙制品缺失应对、真实解析器编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUIText+Span真机页面、HAP 构建和真机验收。
Thistle是 copper-leaf 出品的 Kotlin Multiplatform 字符串标记库:业务方写一段带标记的普通字符串({{b}}bold{{/b}}、{{foreground color=red}}red{{/foreground}}),它用 PEG 组合子解析器(kudzu)解析成平台无关的 AST,再交给各平台的渲染器,变成 AndroidSpannable、ComposeAnnotatedString或控制台 ANSI 转义码。本次适配与社区两条既有路线都有区别:既不是 qrose / haze / PreCompose / voyager 的"零依赖内核提取",也不完全等同于 KStore 的"真编译整库 + 补制品线"——Thistle的解析引擎 kudzu 在社区 Maven 上没有任何 ohos 制品(上游只发布 JVM jar,没有 klib),因此采用源码 vendor 进仓:kudzu-core 4.0.0 的 129 个commonMain文件原样入仓编成 OpenHarmony KLIB,上游thistle-core/thistle-console的源码零改动;Compose 渲染器(thistle-compose-ui)因 Compose 插件 1.2.0 / Kotlin 1.7.20 与鸿蒙工具链不对齐而隔离在构建之外,由OhosDefaults在桥里复刻ComposeDefaults的 13 个标签语义,ArkUI 页面用原生Text+Span逐段着色作为验收宿主。
项目地址: AtomGit/oh-tpc/thistle.git
上游仓库: copper-leaf/thistle
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Thistle 为 Kotlin Multiplatform 应用提供"一份标记,多端渲染"这一件事的完整答案:ThistleParser(AndroidDefaults(context)) 一行建解析器,{{tag attr=value}} 语法覆盖粗斜体、下划线删除线、前景背景色、字体族、上下标,{name} 插值把动态值从 context 表带进文案,未声明标签直接报错而不是静默吞掉。解析与渲染严格分层:thistle-core 只负责"标记字符串 → AST",AndroidDefaults / ComposeDefaults / ConsoleDefaults 都是往 ThistleSyntaxBuilder 里注册标签工厂、再配一个 ThistleRendererFactory 的插件。KMP 项目把这些能力放在 commonMain,由 Android、桌面共享。OpenHarmony 应用同样需要这套富文本标记能力——服务端下发富文本、跨端一致的文案样式、新手引导高亮都是真实场景,但上游构建(Kotlin 1.7.20 / Gradle 7.6 / buildSrc 约定插件)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示文字,却无法证明公共解析器在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证解析器引擎、解析器依赖(kudzu)、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 1.7.20 / Gradle 7.6 / buildSrc 约定插件(AGP 7.2、Compose 插件 1.2.0)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | thistle-core 的每一个解析入口都长在 kudzu 的组合子上,而 Maven Central 的 kudzu-core:4.0.0 只有 JVM jar——jar 里是 .class 字节码,Kotlin/Native 的 ohos 目标消费不了。 |
| 渲染语义跨不了桥 | thistle-compose-ui 的产物是 Compose AnnotatedString,依赖 androidx.compose.ui.text,鸿蒙侧没有对应制品;上游 Compose 插件 1.2.0 与社区 CMP 1.9.2 两条版本轴对不齐。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:真实解析器与 ANSI 渲染器的 ohos 编译、kudzu 源码 vendor、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。解析语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和用 Text + Span 展示结果。
1.2 库提供的能力
Demo 提供一个真实的标记解析会话,对应桥接的全部入口:
- 标记渲染:一段文本里混排
{{b}}/{{i}}/{{u}}/{{strikethrough}}/{{foreground color=...}}/{{background color=...}}/{{style style=...}}/{{typeface typeface=...}}/{{monospace}}/{{sans}}/{{serif}}/{{superscript}}/{{subscript}}十三个标签,解析渲染成样式化分段; - 插值:
{name}(单花括号)从请求的context对象取值,数字、布尔、带空格逗号的字符串都按上游语义toString替换; - 三种颜色写法:11 个颜色名(
red/green/#rrggbb/#aarrggbb大小写不敏感)与上游Color.Companion.parse同一张表;color=#rrggbb被上游解析器转成 ARGB Int,桥再序列化回颜色串; - 嵌套叠加:
{{foreground color=blue}}{{b}}bold blue{{/b}}{{/foreground}}样式逐层合并,分段在样式变化处切分; - 错误通道:未闭合标签、未声明标签、缺失插值键全部走同一个
ok:false通道带回上游异常原文,不崩、不跨语言抛异常。
响应是一个扁平的分段数组:
| 字段 | 作用 |
|---|---|
ok | 解析渲染是否成功,失败时 error 带上游异常原文。 |
spans | 样式化分段数组,每段必有 text;样式字段全可选,无样式的段只有一个 text。 |
| 样式字段 | fontColor / backgroundColor / fontWeight / fontStyle / fontFamily / textDecoration / baselineShift / fontSize 八个可选键,与 ArkUI Span 属性几乎一一对应。 |
解析器侧完整能力(kudzu 的嵌套标签、属性表、错误恢复;core 的标签动态校验、值解析器族)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 15 项桥协议冒烟测试交叉验证,证明 ohos .so 里执行的解析逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | thistle-core 与 thistle-console 的全部 commonMain 文件原样编译,不提取、不重写;kudzu-core 4.0.0 源码原样 vendor,仅新增 ohosMain / jvmMain 各一个 KudzuPlatform actual。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,三个库模块保留 jvm() 作交叉验证,examples/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);thistle-compose-ui、thistle-android、docs 留盘不进构建;绑死旧工具链的 buildSrc 与上游 CI 整体删除。 |
| 桥接稳定 | 使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | thistle-core 对外的唯一依赖从 Maven 坐标换成 project(":kudzu-core"),kudzu 源码与上游 4.0.0 逐字一致。 |
| UI 完整 | 三页签覆盖全部桥接入口:渲染(标记输入 + context + 结果区)、标签速查(13 个标签逐一自证)、JSON(原始请求 / 响应检视)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
Thistle上游的 Compose 渲染器隔离在构建之外,ArkTS Demo 用的是鸿蒙原生Text+Span组件——这正是 thistle 的天然优势:它本来就把解析与渲染分层,桥只需要复刻SpanStyle的字段语义,不需要复刻 Painter。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 kudzu 无 ohos 制品、确定交付物
第 2 阶段:工具链与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、kudzu 源码 vendor
第 3 阶段:桥协议建模 ── 标记文本 + context 载荷、spans JSON 契约、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 三页签、Text+Span 逐段渲染、标签速查自证、JSON 检视
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
kudzu-core/ vendor 进仓的 kudzu-core 4.0.0(129 个 commonMain 文件 + ohos/jvm 两个 actual,进 ohos 构建)
thistle-core/ 上游解析引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64,源码零改动)
thistle-console/ 上游 ANSI 渲染器(进 ohos 构建,commonMain 原样,源码零改动)
thistle-compose-ui/ 上游 Compose AnnotatedString 渲染器,留盘不进构建
thistle-android/ 上游 Spannable 渲染器,留盘不进构建
examples/nativeApp/ Kotlin/Native 动态库(ThistleBridge + Bridge + MiniJson + JVM 冒烟测试)
examples/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/ examples/android 等 上游文档站与旧示例,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :kudzu-core、:thistle-core、:thistle-console 与 :examples:nativeApp,rootProject.name = "ohos_thistle"。上游的 buildSrc 约定插件(版本管理 / 发布 / lint / testing)、binary-compatibility-validator、Orchid 文档站、GitHub Actions 发布链全部随根构建替换剥离——本 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) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(三个库模块 jvm() 保留作交叉验证) |
| Gradle | 8.14.1(2.2.21 线要求 8.x,wrapper 升级自上游 7.6) |
| kudzu | 4.0.0 源码 vendor 进仓(无制品线可换——这是与 kotlinx 系仓库的本质差异) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.thistle |
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
kudzu-core | 129 | 纯 Kotlin,唯一 expect 是 KudzuPlatform(isLetter / isDigit / isLetterOrDigit) | vendor 整体编译(jvm + 双 ohos) |
thistle-core | 15 | 纯 Kotlin + kudzu,零 UI、零 AGP、零 expect | 整体编译(源码零改动) |
thistle-console | 12 | 纯 Kotlin + thistle-core,ANSI SGR 码表 | 整体编译(源码零改动) |
thistle-compose-ui | 16 | androidx.compose.ui.text 全家桶 | 留盘不进构建 |
thistle-android | 20 | Android Spannable | 留盘不进构建 |
expect 声明全链路只有 1 处:kudzu 的 public expect object KudzuPlatform。上游给了 jvm / ios / js 三个 actual,ohos fork 补两个——ohosMain 与 jvmMain 各一份,实现与上游 jvm actual 同款(stdlib 的 Char.isLetter() 系列在 Native 上原生可用),这就是整个 fork 在 vendor 模块里仅有的新增源文件:
// kudzu-core/src/ohosMain/kotlin/com/copperleaf/kudzu/KudzuPlatform.kt
public actual object KudzuPlatform {
public actual fun isLetter(char: Char): Boolean = char.isLetter()
public actual fun isDigit(char: Char): Boolean = char.isDigit()
public actual fun isLetterOrDigit(char: Char): Boolean = char.isLetterOrDigit()
}
thistle-core 值得单独说明:它对外的唯一依赖就是 io.github.copper-leaf:kudzu-core:4.0.0,SequenceParser、PredictionParser、IdentifierTokenParser、TagParser……每一个解析入口都长在 kudzu 的组合子上。没有 kudzu 就没有 thistle,所以 vendor kudzu 是本次适配的成败手,处置见第 2.1 节。
1.4 页面边界
标题区 thistle · OpenHarmony / 副题行(富文本标记库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 渲染 / 标签速查 / JSON
标记输入区 thistle 标记多行文本域(默认文案覆盖全部 13 个标签)
context 区 context JSON 文本域(默认 {"name":"HarmonyOS"},可留空)
操作区 渲染按钮
结果区 Text + ForEach(Span) 逐段着色;失败时红色错误文本
标签速查区 13 个标签的名称、参数说明、示例源码 + 实时桥渲染自证
JSON 区 最近一次请求 / 响应的原始 JSON(等宽字体滚动区)
页面启动时把 13 个标签的示例逐个送过真实桥做自证渲染,再对默认文案做一次完整渲染。
第 2 阶段:工具链与依赖打通
2.1 kudzu 制品缺失的应对(本适配的成败手)
适配 thistle 与适配 kotlinx 系仓库的第一步就分岔了。KStore 一类仓库可以先查社区 Maven 上的 ohos 制品线;thistle 的依赖 kudzu 是 copper-leaf 自家的小众解析器组合子库,社区 Maven 和 Maven Central 上都只有 JVM jar:
io.github.copper-leaf:kudzu-core:4.0.0
artifacts: kudzu-core-4.0.0.jar / -all.jar / -sources.jar ← 无任何 .klib
jar 里是 .class 字节码,ohosArm64 / ohosX64 目标配置期就会报「依赖没有 ohos 变体」。三条路里选了 vendor:换解析器是白做(解析器就是这类库的全部价值);手写 ohos 变体发布等于替上游维护 fork 制品;把 4.0.0 的 sources jar 原样解包进仓是最诚实的方式——commonMain 的 129 个文件逐字节与上游一致(kudzu 本身是纯 Kotlin,无任何平台 API),只补 ohosMain / jvmMain 两个 actual,thistle-core 的依赖从 Maven 坐标换成 project(":kudzu-core")。vendor 的出处、license(与 thistle 同为 BSD-3、同一位上游作者)和做法都写进了 kudzu-core/README.md。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已审仓库(qrose / haze)的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1,本仓沿用。两点说明:其一,上游 thistle 本来就是 Kotlin 1.7.20 编的,2.2.21 读 1.7 元数据毫无压力,kudzu 4.0.0 同理;其二,Gradle 经验必须按工具链线区分——KStore 适配时 Kotlin 2.4.20-0.1.0-07 线可以沿用 Gradle 9.x,而 2.2.21-1.0.0 旧线必须配 Gradle 8.x(本仓 wrapper 从上游 7.6 升到 8.14.1),两条线的经验不能互相套用。
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_thistle"
include(":kudzu-core")
include(":thistle-core")
include(":thistle-console")
include(":examples:nativeApp")
2.3 编译真实解析器
三个库模块的构建文件收敛为同一形状(以 thistle-core 为例):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(project(":kudzu-core")) // 上游是 Maven 坐标,fork 换 project 依赖
}
}
// 上游 kotest 套件保留源码,fork 不跑(见 2.4)
tasks.matching { it.name.contains("Test") }.configureEach { enabled = false }
}
实测:
./gradlew :kudzu-core:compileKotlinOhosArm64 :kudzu-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :thistle-core:compileKotlinOhosArm64 :thistle-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :thistle-console:compileKotlinOhosArm64 :thistle-console:compileKotlinOhosX64 # BUILD SUCCESSFUL
上游 thistle-core / thistle-console 的全部 commonMain 文件一个没动,kudzu 只有两个 actual 是新的。
2.4 测试依赖的处置:kotest 与 Kotlin 2.2.21 未对齐
上游测试用 kotest 5.5.1(engine / assertions / datatest / property 四件),构建于 Kotlin 1.7 时代,测试框架与 Kotlin 2.2.21 工具链的适配没有验证过。处置是两条纪律的组合:上游测试文件零改动保留在 commonTest,各模块显式禁用全部 Test 任务,根构建再兜一层:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
解析正确性改由 examples/nativeApp 的 JVM 冒烟测试兜底(见第六节)——它跑的不是上游测试,而是桥协议本身,但跑在与 ohos 完全相同的 commonMain 代码上。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.copperleaf.thistle.core/**(上游解析引擎,原样编译)
↓ ThistleParser(OhosDefaults()) / kudzu PEG → AST
ThistleBridge.kt(commonMain:OhosDefaults 渲染 + MiniJson 序列化)
↓ UTF-8 JSON
C ABI ThistleRender(const char*) → N-API thistleRender(string)
↓
ArkUI Index.ets(三页签)
请求与响应:
请求 : {"text":"Hello {name}! {{b}}bold{{/b}}","context":{"name":"HarmonyOS"}}
响应 : {"ok":true,"spans":[
{"text":"Hello "},
{"text":"HarmonyOS"},
{"text":"! "},
{"text":"bold","fontWeight":"bold"}
],"error":null}
失败 : {"ok":false,"spans":[],"error":"Unknown tag: boldx. Valid tag names: [foreground, ...]"}
三个不对称设计:spans 是扁平分段数组而不是样式区间树——ArkUI 的 Span 是"文本段 + 属性"模型,扁平分段 ForEach 一把就能渲染;分段粒度是样式变化处,相同样式的连续文本合成一段;样式字段全可选,无样式的段只有一个 text,ArkTS 侧逐字段判空。
3.2 OhosDefaults:为什么镜像 ComposeDefaults
上游渲染器是插件式的:ThistleSyntaxBuilder.Defaults 接口 = 标签注册表 + 渲染器工厂。鸿蒙桥实现了一个 OhosDefaults,按上游 ComposeDefaults 的清单一比一注册 13 个标签——名称、参数名、取值域全部照抄,产物不是 SpanStyle 而是平台无关的 SpanSegment:
public class OhosDefaults : ThistleSyntaxBuilder.Defaults<OhosThistleRenderContext, SpanStyleModel, List<SpanSegment>> {
override fun applyToBuilder(builder: ThistleSyntaxBuilder<OhosThistleRenderContext, SpanStyleModel, List<SpanSegment>>) {
with(builder) {
tag("foreground") { OhosForegroundColor() }
tag("background") { OhosBackgroundColor() }
tag("style") { OhosStyle() }
tag("typeface") { OhosFontFamily() }
tag("monospace") { OhosFontFamily("monospace") }
tag("sans") { OhosFontFamily("sans") }
tag("serif") { OhosFontFamily("serif") }
tag("strikethrough") { OhosTextDecoration("lineThrough") }
tag("subscript") { OhosBaselineShift("subscript") }
tag("superscript") { OhosBaselineShift("superscript") }
tag("b") { OhosStyle(hardcodedWeight = "bold") }
tag("i") { OhosStyle(hardcodedFontStyle = "italic") }
tag("u") { OhosTextDecoration("underline") }
}
}
}
每个标签工厂都用上游的 checkArgs + parameter / enum 委托实现,参数名与上游逐字一致(color=、style=、typeface=),连上游 ComposeTextDecoration 参数映射表里 "underline" 也指向 LineThrough 的历史笔误都原样镜像——尽管 13 个默认标签全部走硬编码路径、永远碰不到那张表,镜像它保证了"语义对齐上游"这句话经得起逐行核对。
颜色语义同样照抄上游 Color.kt:颜色名 11 个 + #rrggbb / #aarrggbb;color=#ffcc00 会被上游的 hexColorAsIntValueParser 转成置了 alpha 的 ARGB Int,桥序列化时把全不透明的折回 #rrggbb 短形式(与输入写法一致),带透明的才输出 #aarrggbb;未知颜色降级为无样式(对齐上游 Color.Unspecified),不报错。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ThistleRender")
fun thistleRender(request: String): String = try {
/* 解析 + 渲染 + 序列化 */
} catch (t: Throwable) {
"""{"ok":false,"spans":[],"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、解析错误(Parse error at 1:18 ...)、未声明标签(Unknown tag: ...)、缺失插值键(Context must contain value for key '...'),全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libthistle_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosthistle.so)
│ ThistleRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻解析语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("ThistleRender")
fun thistleRender(request: String): String // 一口进:标记 + context → spans JSON
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ThistleRender | thistleRender(req) | ThistleParser(OhosDefaults()) 构造 + kudzu 解析 + 13 标签渲染 + JSON 序列化 |
桥的全部逻辑放在 examples/nativeApp/src/commonMain(ThistleBridge.kt),ohosMain 只留 @CName 一层薄壳——这是刻意安排:jvmSmokeTest 在 JVM 上跑的与设备上是同一份代码。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"thistleRender", nullptr, NapiThistleRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosthistle_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosthistle_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),并与 KN 生成头(build/bin/<abi>/debugShared/libohosthistle_api.h)逐符号核对后才提交。
4.4 CMake 和类型声明
add_library(ohosthistle SHARED IMPORTED)
set_target_properties(ohosthistle PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(thistle_napi SHARED napi_init.cpp)
target_link_libraries(thistle_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosthistle)
OHOS_ARCH 为空或 libohosthistle.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。KN so 本体由 examples/nativeApp 的 linkDebugSharedOhosArm64 / linkDebugSharedOhosX64 链出,copyKnSo* 任务自动把 so 与匹配 triple 的 libc++_shared.so 回填进 entry/libs/<abi>/。
export const thistleRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 触发渲染和刷新结果
import { thistleRender } from 'libthistle_napi.so';
import { LengthMetrics } from '@kit.ArkUI';
private renderCore(markup: string, contextJson: string): RenderResponse {
const req: Record<string, Object> = { 'text': markup };
const ctx = contextJson.trim();
if (ctx.length > 0) {
req['context'] = JSON.parse(ctx) as Record<string, Object>; // 非法 JSON 本地拦截,不发 Native 调用
}
const raw = thistleRender(JSON.stringify(req));
return JSON.parse(raw) as RenderResponse;
}
结果区把 spans 摊进一个 Text,逐段条件链式上样式;ok:false 时错误原文红色显示,全程 try/catch 不崩:
Text() {
ForEach(spans, (s: SpanStyle) => {
Span(s.text)
.fontColor(s.fontColor ?? '#111827')
.backgroundColor(s.backgroundColor ?? Color.Transparent)
.fontWeight(s.fontWeight === 'bold' ? FontWeight.Bold : FontWeight.Normal)
.fontStyle(s.fontStyle === 'italic' ? FontStyle.Italic : FontStyle.Normal)
.fontFamily(this.mapFontFamily(s.fontFamily))
.decoration({ type: /* underline / lineThrough / none */ })
.baselineOffset(LengthMetrics.fp(/* superscript +8 / subscript -4 / 0 */))
.fontSize(s.fontSize ?? 16)
}, (s: SpanStyle) => JSON.stringify(s))
}
5.2 页面交互
渲染 Tab 自上而下:标记输入域(默认文案一次覆盖全部 13 个标签)、context JSON 输入域、渲染按钮、结果卡片与状态行(渲染成功 · 28 段)。标签速查 Tab 用静态数组列出 13 个标签的名称、参数说明与示例源码,aboutToAppear 时把每条示例真实送过桥渲染出 Span 自证——页面自己就是 13 个标签的验收用例。JSON Tab 用等宽字体滚动区展示最近一次请求 / 响应的原始 JSON,桥协议一眼可查。
5.3 对比度和错误边界
ok:false 时错误原文红色进结果区,context JSON 非法时本地报错、不发起 Native 调用;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进错误显示。
第 6 阶段:示例与验证
6.1 工程结构
examples/
├── nativeApp/
│ ├── build.gradle.kts jvm() + 双 ABI sharedLib(baseName=ohosthistle)+ cxxShared + copyKnSo* + jvmSmokeTest
│ ├── src/commonMain/kotlin/example/nativeapp/
│ │ └── ThistleBridge.kt OhosDefaults + 13 标签工厂 + OhosThistleRenderer + MiniJson + JSON 序列化
│ ├── src/ohosMain/kotlin/example/nativeapp/
│ │ └── Bridge.kt @CName("ThistleRender") 薄壳
│ └── src/jvmMain/kotlin/example/nativeapp/
│ └── Main.kt 桥协议冒烟测试(15 项)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.thistle、versionName 4.0.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/libohosthistle_api.h
├── src/main/cpp/types/libthistle_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(三页签)
6.2 构建、签名和安装
./gradlew :examples:nativeApp:jvmSmokeTest # 15 项桥协议冒烟
./gradlew :examples:nativeApp:linkDebugSharedOhosArm64 :examples:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 examples/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 回填完整配置)。
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.thistle -m entry
四、完整代码对照
4.1 整体架构
com.copperleaf.kudzu/**(上游 4.0.0 源码 vendor,原样编译)
│ PEG 组合子:Sequence / Choice / Tag / Value Parser 族
com.copperleaf.thistle.core/**(上游原样编译)
│ ThistleParser / ThistleSyntaxBuilder / 标签动态校验 / 值解析器族
com.copperleaf.thistle.console/**(上游原样编译)
│ ConsoleDefaults + SGR 码表(不经桥,鸿蒙侧随编译可用)
ThistleBridge.kt(commonMain)
│ OhosDefaults 13 标签 / SpanSegment 模型 / MiniJson
libthistle_napi.so
│ thistleRender(req)
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(Text + Span 逐段着色)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_thistle、四个模块 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 插件钉版 |
build.gradle.kts(根) | 版本 4.0.1-ohos.1、全局禁用 Ohos*Test* 任务 |
kudzu-core/ | vendor kudzu-core 4.0.0(129 文件 + ohos/jvm 两个 actual) |
thistle-core / thistle-console / kudzu-core 的 build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,禁 Test 任务 |
examples/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、jvmSmokeTest |
nativeApp/.../ThistleBridge.kt | OhosDefaults、SpanSegment 渲染器、MiniJson、错误通道 |
nativeApp/.../Bridge.kt | @CName("ThistleRender") 薄壳 |
nativeApp/.../Main.kt | JVM 桥协议冒烟测试(15 项) |
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 验收页面(三页签) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(17 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 解析器 | ThistleParser(OhosDefaults()) | 建解析器:标签注册表 + PEG 语法一次成型 |
| 解析器 | thistle.parse(ParserContext.fromString(text)) | 标记串 → AST,未声明标签在此抛 ThistleUnknownTagException |
| 渲染器 | thistle.newRenderer().render(rootNode, context) | AST + context → List<SpanSegment> |
| 标签 | foreground / background / style / typeface / monospace / sans / serif / strikethrough / subscript / superscript / b / i / u | 13 个标签,参数与语义镜像上游 ComposeDefaults |
| 插值 | {name} | 单花括号,从 context 取值 toString 替换 |
| Native | ThistleRender | C ABI JSON 导出 |
| N-API | thistleRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | renderCore(markup, context) | 触发解析并刷新 Text + Span |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(thistleRender(JSON.stringify({
text: 'Hi {name}, {{b}}bold{{/b}} and {{foreground color=red}}red{{/foreground}}',
context: { name: 'ohos' }
})));
// res.ok === true;res.spans 逐段带 text + 可选样式字段
val thistle = ThistleParser(OhosDefaults())
val rootNode = thistle.parse(ParserContext.fromString(text))
val spans = thistle.newRenderer().render(rootNode, context) // List<SpanSegment>
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:kudzu 源码 vendor 进仓,而不是换依赖或重写解析器
thistle 的全部价值就在 kudzu 组合子搭出来的解析器里。社区 Maven 没有 kudzu 的 ohos 制品(连 klib 都没有),换线不可行;在 ArkTS 里重写解析器必出分叉;替上游维护一份 ohos fork 制品又超出单个库适配的范围。sources jar 原样解包进仓是最诚实的方式:129 个 commonMain 文件逐字与上游 4.0.0 一致、license 同为 BSD-3、只补两个 KudzuPlatform actual,出处写进模块 README 供审阅。
决策 2:工具链钉 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
沿用社区已审钉法(qrose / haze 同款)。上游 Kotlin 1.7.20 的源码与 kudzu 4.0.0 的元数据在 2.2.21 下零兼容成本;Gradle 必须配 8.x——KStore 适配的「2.4.20 线沿用 Gradle 9.x」经验在这条旧线上不成立,工具链线的经验不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),compose-ui / android 留盘
上游根构建(buildSrc 约定插件、binary-compatibility-validator、Orchid 文档站、GitHub Actions)对鸿蒙没有保留价值,整体替换为极简 ohos 构建;buildSrc 绑死 Kotlin 1.7.20 / AGP 7.2,Gradle 每次构建都会先编它,直接删除。thistle-compose-ui 留盘有三个理由:对照 ComposeDefaults 写 OhosDefaults 时不用翻上游仓库;等社区 CMP 工具链与上游版本对齐后改回 source set 就能接;diff 历史保持完整。留盘不编和删掉是两回事。
决策 4:OhosDefaults 镜像 ComposeDefaults,ArkUI 原生渲染而不是光栅化
qrose 光栅化是因为条码的终点本来就是一张位图;文本的终点就是文本组件。光栅化文本会同时失去可选、可缩放、无级字体、系统无障碍四样东西,等于在 KN 里重写一遍文本排版。ArkUI Span 原生就有 fontColor / fontWeight / fontFamily / decoration / baselineOffset,和 SpanStyle 字段几乎一一对应——桥只复刻 SpanStyle 的字段语义,不复刻 Painter。13 个标签的名称、参数、取值域与上游逐字对齐,验收时同一标记串可以拿上游 Compose / Android 渲染结果逐段比对。
决策 5:桥逻辑进 commonMain + JVM 冒烟测试
ThistleBridge.kt 放 commonMain,ohosMain 只留 @CName 壳,nativeApp 同时开 jvm() 目标挂 jvmSmokeTest——JVM 与 ohos 编译同一份桥代码,15 项冒烟用例(插值、13 标签、三种颜色写法、嵌套叠加、错误通道)在 JVM 上验证过的行为就是设备上会跑的行为。上游 kotest 与新工具链未对齐不能跑,交叉验证的闸门就移到桥协议层:段落边界、每段样式字段值都钉死在用例里。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release(HarmonyOS SDK 5.1.0(18))、hvigor / hdc 命令行与鸿蒙真机(arm64)。
6.2 JVM 桥协议冒烟验证
./gradlew :examples:nativeApp:jvmSmokeTest
15 项用例全部通过(跑在与 ohos 相同的 commonMain 桥代码上):
| 用例组 | 钉住什么 |
|---|---|
| 插值 | {name} 从 context 取值;数字、布尔 context 值按 toString 替换 |
{{b}} / {{i}} / {{u}} / {{strikethrough}} | 硬编码速记标签的 fontWeight / fontStyle / textDecoration |
{{style style=bold / style=italic}} | 参数化样式标签(上游只收一个 style= 参数) |
| 颜色三写法 | 颜色名 → #rrggbb;#rrggbb 经 ARGB Int 折回短形式;#aarrggbb 保留 |
| 未知颜色 | 降级为无样式(对齐上游 Color.Unspecified),不报错 |
{{superscript}} / {{subscript}} | baselineShift + 附带 fontSize 缩小 |
| 字体族 | monospace / sans / serif 硬编码 + typeface=cursive 参数化 |
| 嵌套叠加 | {{foreground color=blue}}{{b}}…{{/b}}{{/foreground}} 样式合并、分段切分 |
| 参数化装饰映射 | 上游 ComposeTextDecoration 映射表的历史行为原样镜像 |
| 错误通道 | 未闭合标签 / 未声明标签 / 缺失插值键全部 ok:false 不崩 |
| 空文本 | 回退默认演示文案并补默认 context |
6.3 HAP 验证
:kudzu-core / :thistle-core / :thistle-console compileKotlinJvm: BUILD SUCCESSFUL
:kudzu-core / :thistle-core / :thistle-console compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填 entry/libs/<abi>/ 三件套)
生成头 libohosthistle_api.h 与仓内手写头逐符号核对:ThistleRender / libohosthistle_symbols / DisposeString 一致
CompileArkTS: finished(修复 baselineOffset 的 LengthMetrics 签名后)
PackageHap: finished
install bundle successfully
start ability successfully
HAP 内含双 ABI 各三件套(libthistle_napi.so / libohosthistle.so / libc++_shared.so);KN so 为 debugShared 未 strip 版本,含 Kotlin/Native 运行时,release 打包后显著缩小。
6.4 功能用例
- 启动后渲染 Tab 自动对默认文案执行一次完整解析渲染,状态行显示
渲染成功 · 28 段; - 默认文案混排全部 13 个标签,结果卡片里粗体、斜体、下划线、删除线、红色前景、
#ffcc00背景、上标、下标、等宽、style=bold、衬线/无衬线同屏可见(见第七节效果图); {name}插值取自 context JSON({"name":"HarmonyOS"}),渲染为Hello HarmonyOS!;- 标签速查 Tab 对 13 个标签逐条展示参数说明与示例源码,并用真实桥调用渲染出自证结果;
- JSON Tab 可见最近一次请求 / 响应的原始 JSON,与 JVM 冒烟用例的断言口径一致;
- 篡改 context 为非法 JSON,被前置校验本地拦截,不产生 Native 调用;
- 输入未闭合 / 未声明标签(如
{{b}}unclosed、{{nosuchtag}}),结果区红色显示上游异常原文,应用不崩; - 清空文本再渲染,回退到内置演示文案——桥的兜底语义生效。
6.5 验证结论
解析器引擎(kudzu + thistle-core)、ANSI 渲染器(thistle-console)、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的解析会话:13 个标签、插值、三种颜色写法、嵌套叠加与错误通道(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/thistle.git
cd thistle
./gradlew :thistle-core:compileKotlinOhosArm64 :thistle-console:compileKotlinOhosArm64 # klib
./gradlew :examples:nativeApp:jvmSmokeTest # 15 项桥协议冒烟
./gradlew :examples:nativeApp:linkDebugSharedOhosArm64 :examples:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 examples/harmonyApp(不要打开仓库根目录),勾选自动签名
cd examples/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.thistle -m entry
hdc shell hilog | grep Thistle
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:kudzu-core/:thistle-core/:thistle-console真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;kudzu-core:4.0.0在 Maven Central 上只有 JVM jar,连 klib 都没有——「依赖没有 ohos 变体」报的是配置期,vendor 源码是唯一诚实解;- expect/actual 的 actual 按目标补齐:kudzu 的
KudzuPlatform只补 ohos actual 不够,fork 同时保留jvm()目标,jvmMain的 actual 缺一个编译就报Expected KudzuPlatform has no actual declaration in module <commonMain> for JVM; - include 了的模块必须有自己的
build.gradle.kts——漏建的模块在 Gradle 眼里是空工程,消费方报No matching variant ... No variants exist,报错信息完全指向依赖解析而不是漏文件,极易查错方向; - thistle 的插值是单花括号
{name}(DefaultThistleSyntax默认语法),写成{{name}}会被当成未声明标签报Parse error ... Expected '{{/'——Demo 默认文案、文档、测试用例都按单花括号写; color=#ffcc00会被上游hexColorAsIntValueParser转成置了 alpha 的 ARGB Int,桥序列化时全不透明的要折回#rrggbb短形式,否则响应里出现#ffffcc00这类冗余串;- MiniJson 从扁平 map 取
text时必须剥引号反转义,否则渲染出来的每段文本都带一对引号——这种错在编译期零提示,全靠冒烟测试钉住; - 上游
{{style}}标签只收一个style=参数(取值bold/italic),没有weight=——Demo 文案按想象写weight=bold会直接ok:false; - 上游测试统一 JVM 保留、模块级禁用 Test 任务 + 根构建禁用
Ohos*Test*:kotest 5.5.1 与 Kotlin 2.2.21 未对齐,测试框架的债不应阻塞 ohos 编译链接; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - ArkTS 的
Span.baselineOffset在 API 12+ 收LengthMetrics,传裸number编译期直接报10505001——用LengthMetrics.fp(...)包一层(@kit.ArkUI导出); - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();仓内手写的极简头文件要与 KN 生成头逐符号核对后再提交。
8.2 已知问题
thistle-compose-ui(ComposeAnnotatedString渲染器)与thistle-android(Spannable渲染器)源码留盘不进构建:上游 Compose 插件 1.2.0 / Kotlin 1.7.20 与社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 两条版本轴对不齐;等工具链对齐后改回 source set 接回,OhosDefaults+ JSON 桥降级为 fallback;thistle-console的 ANSI 渲染随编译进入.so,但鸿蒙侧没有开箱即用的终端宿主,桥不输出 SGR 串——它的价值在「上游测试与解析语义对照」和未来终端类应用消费;- 上游 kotest 测试未跑(与 Kotlin 2.2.21 未对齐,源码零改动保留),解析正确性由 JVM 冒烟测试 + 真机逐段比对兜底,强度低于单测回归;
- 上游
ComposeTextDecoration参数映射表里"underline"也指向LineThrough的历史笔误被原样镜像;13 个默认标签全走硬编码路径不受影响,只有绕过默认标签集的参数化装饰才可能碰到; - 桥不暴露注册自定义标签的入口(core 的扩展点是开放的,但要业务方自己写标签工厂再编进
.so),鸿蒙侧当前交付的是上游ComposeDefaults的 13 个默认标签; - HAP 安装必须使用与 bundle name
org.terminator.ohos.thistle匹配的签名。
8.3 未来优化方向
- 社区 CMP 工具链与上游 Compose 版本对齐后,把
thistle-compose-ui接回 ohos source set,JSON 桥降级为 fallback; - kotest 出与 Kotlin 2.x 对齐的版本后,恢复上游测试源集,把交叉验证升级回上游原版用例;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 把 jvmSmokeTest 的用例口径开放成 ArkTS 侧可执行的对账脚本(真机渲染结果与 JVM 断言自动比对);
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)与其他设备 ABI 的验收矩阵。
九、总结
9.1 核心难点回顾
上游 thistle-core + thistle-console(全部 commonMain,零改动)
→ 盘点依赖:kudzu 无任何 ohos 制品(连 klib 都没有)
→ kudzu-core 4.0.0 源码 vendor 进仓 + 补 ohos/jvm 两个 actual
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1,2.2.21 线专属)
→ 根构建替换(cmp_qrose 模式,compose-ui/android 留盘,buildSrc 删除)
→ ohosArm64 / ohosX64 KLIB
→ C ABI(@CName("ThistleRender"))+ spans JSON 契约
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机三页签验收(13 标签 / 插值 / 三种颜色写法 / 错误通道)
9.2 封装层次
- 解析层:上游原版 kudzu 4.0.0 组合子 +
ThistleParser/ThistleSyntaxBuilder,标签动态校验与值解析器族原样; - 渲染层:
OhosDefaults镜像上游ComposeDefaults的 13 个标签语义,产物为平台无关的SpanSegment; - Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、context 校验、渲染分发、错误显示与三页签导航;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线跟着依赖的制品可达性走:kotlinx 系仓库可以「真编译整库 + 补制品线」,依赖小众库的仓库就得「源码 vendor + 补 actual」——路线没有优劣,只有当天的制品事实;vendor 的底线是逐字一致 + 出处可查 + 差异(actual)最少;
- 工具链经验按线区分、不能互套:
2.2.21-1.0.0线配 Gradle 8.x,2.4.20线可留 Gradle 9.x;上游 Kotlin 版本与鸿蒙钉版的元数据兼容性,是决定兼容成本的第一变量; - 桥逻辑进 commonMain 是 KMP 库适配的杠杆:JVM 冒烟测试跑的就是设备上跑的代码,上游测试跑不了时,把交叉验证的闸门移到桥协议层(段落边界 + 每段样式字段值)依然能钉住「内核没漂」。
9.4 适配成果
当前适配已完成:
Thistle 4.0.1(4.0.1-ohos.1)真实解析器的双 ABI Kotlin/Native 编译(上游thistle-core/thistle-console源码零改动,kudzu vendor 源码仅两个 actual);- kudzu-core 4.0.0 源码 vendor 进仓的完整落地(129 个 commonMain 文件 + ohos/jvm actual,BSD-3 出处可查);
- 1 个 C ABI 符号和 C++ N-API 包装层(生成头与仓内头逐符号核对一致);
- DevEco Stage ArkUI 三页签验收页面(渲染、标签速查、JSON 检视);
- 15 项 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)