本文记录
multiplatform-markdown-renderer接入 OpenHarmony 的完整过程,覆盖 Compose UI 库的模块与依赖边界盘点、解析内核抽取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机渲染页面、HAP 构建和真机验收。
multiplatform-markdown-renderer是 mikepenz 开发的 Compose Multiplatform Markdown 渲染库:输入 Markdown 文本,先用 JetBrains 的org.jetbrains:markdown解析成 AST,再转换成AnnotatedString与组件树,最终由 Compose 渲染出标题、列表、表格、代码块、引用、GitHub Alerts 等全部元素,支持 Android / iOS / Desktop / Web / macOS。本次适配与 KStore 那类"引擎真编译整库"的路线有本质区别:这是一个纯 Compose UI 库 ,50 多个@Composable文件是它的全部价值,而 OpenHarmony 上目前没有 Compose 组合宿主(社区org.jetbrains.composeohos 线只发 klib、跑不起组合)。因此本适配沿用社区已审核的 cmp_qrose 路线 :把"Markdown 文本 → 结构化样式文档"的解析内核抽成纯 Kotlin 真实编译成 OpenHarmony KLIB,Compose UI 整体停靠到非编译目录,@CNameC ABI + N-API 暴露给 ArkTS,由 ArkUI 原生组件递归渲染内核输出的文档树------真机上跑的是真实的 GFM 解析器与上游遍历语义,不是 ArkTS 重写的解析逻辑。
项目地址: AtomGit/oh-tpc/multiplatform-markdown-renderer.git
上游仓库: mikepenz/multiplatform-markdown-renderer
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
multiplatform-markdown-renderer 为 Compose Multiplatform 应用提供"Markdown 文本 → 富文本 UI"这一件事的完整答案:Markdown(...) 一个 composable 吃进任意 Markdown 字符串,rememberMarkdownState 管理解析状态,MarkdownComponents 允许逐组件定制渲染,MarkdownAnnotator 允许在 AnnotatedString 层改写行内样式,m2/m3 两套主题开箱即用,coil2/coil3 承接图片加载,-code 模块接 highlights 做语法高亮,还支持流式渲染(打字机效果)、Lazy 长文档和 extendedspans 扩展下划线。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要 Markdown 渲染能力,但上游构建(Kotlin 2.4.20 / Gradle 9.8.0 / AGP 9.4.0)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 手写一遍渲染,页面可以显示文字,却无法证明公共解析内核在 OpenHarmony ARM64 设备上完成编译、链接和执行------那只是一份和上游无关的复制品。适配需要同时验证解析内核、引用链接解析、列表序号规则、实体转换、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 构建逻辑在外部 | 上游 target 声明、AGP、发布全在外部 Maven 工件 com.mikepenz:version-catalog:0.21.0(convention 插件)里,它不认识 ohos target,无法与鸿蒙工具链共存于一个 Gradle 构建。 |
| 依赖矩阵 | 解析器 org.jetbrains:markdown 上游用的 0.7.16 没有 ohos 变体;-code 模块依赖 JVM-only 的 highlights;-coil2 / -coil3 依赖 Coil------后三者无内核价值也无 ohos 制品。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| Compose 跑不起来 | ohos 上没有组合宿主,@Composable 编得进 klib、跑不了组合------UI 必须由 ArkUI 呈现。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:解析内核的 ohos 编译(上游 Compose 文件 git mv 停靠、字节不动)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。Markdown 语义完全由上游原版解析器与逐支对应的遍历逻辑负责,ArkTS 只负责调用 Native、解析 JSON 和渲染组件树。
1.2 库提供的能力
Demo 提供一个真实的 Markdown 解析渲染会话,对应桥接的全部入口:
- 解析(parse) :
markdownDispatch接收任意 Markdown 文本,运行真实的 GFM 解析器(GFMFlavourDescriptor),返回结构化块 / 样式树 JSON; - 全特性渲染:标题(1-6 级 + Setext)、段落、粗体 / 斜体 / 删除线、行内代码、围栏 / 缩进代码块(含语言标签)、引用、有序(含起始序号)/ 无序 / 嵌套列表、GFM 任务列表、表格(含对齐)、行内链接 / 自动链接 / 引用链接、图片、分隔线、GitHub Alerts 五类;
- 实时编辑:TextArea 改一个字点渲染,块数与耗时随内容变化------证明每次都是真实解析而非静态文案;
- 版本握手 :
markdownVersion()返回 fork 版本号,来自真实 Native 调用。
响应包含:
| 字段 | 作用 |
|---|---|
ok / op |
操作是否成功与回显,失败时 error 带原文。 |
version |
fork 版本握手(0.45.0-ohos.1)。 |
blockCount |
顶层块数量,页面状态行的数据来源。 |
document.blocks |
递归块 / 样式树,ArkUI 渲染的唯一数据源。 |
内核侧完整能力(实体转换 EntityConverter、引用链接解析 ReferenceLinkHandler、列表标记策略 BulletHandler、GFM Alert 类型 MarkdownAlertType)全部来自上游 commonMain 原文件随编译进入 .so,JVM 侧保留 35 项 jvmTest 交叉验证用例(12 项内核新测试 + 上游 23 项 AST 形状测试),证明 ohos .so 里执行的解析逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 解析内核所需的 5 个上游纯 Kotlin 文件(EntityConverter / BulletHandler / MarkdownAlertType / ReferenceLinkHandler / Extensions 纯函数部分)原样保留编译;50+ 个 Compose 文件 git mv 停靠 composeMain(字节不动,可逆);新增 document 内核包(模型 + AST→文档转换 + 门面)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);-m2 / -m3 / -code / -coil2 / -coil3 与 sample/ 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 解析器钉社区 ohos 线 org.jetbrains:markdown:0.7.7-1.0.0-02(唯一换线依赖);内核不依赖 kotlinx-coroutines / collections-immutable,把 ohos 制品验证面压到最小。 |
| UI 完整 | 双 Tab 覆盖全部块 / 样式类型:编辑渲染(TextArea + 12 个递归 @Builder)+ 关于(版本握手、架构分层、链接)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Compose 渲染层停靠在
composeMain/,ArkTS Demo 用原生 Text / Span / ImageSpan / Divider 组件把内核输出的文档树画出来;两者的语义对齐由内核遍历逻辑逐支对应上游渲染管线保证。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点 6 个模块的依赖边界、验证 markdown ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、脱离 convention 插件
第 3 阶段:源码集拆分 ── 50+ Compose 文件 git mv 停靠 composeMain、3 个混合文件外科手术拆分
第 4 阶段:内核编写 ── document 包:块/样式模型 + AST→文档转换(逐支对应上游)+ 解析门面
第 5 阶段:原生桥接 ── @CName C ABI、MiniJson、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 双 Tab Demo、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
text
multiplatform-markdown-renderer/ 核心库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
├── src/commonMain/.../document/ 新增:内核三件(模型 / 转换 / 门面)
├── src/commonMain/.../{model,utils}/ 上游纯 Kotlin 文件保留(EntityConverter 等 5 个)
├── src/composeMain/ 停靠:50+ Compose 文件(git mv,字节不动)
├── src/composeTest/ 停靠:3 个依赖 Compose 状态类的上游测试
multiplatform-markdown-renderer-m2/-m3/-code/-coil2/-coil3/ Compose/图片加载子模块,留盘不进 settings
sample/ 上游多平台示例,留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式 (根构建即 ohos 构建):settings.gradle.kts 只 include :multiplatform-markdown-renderer 与 :example:nativeApp,rootProject.name = "ohos_multiplatform_markdown_renderer"。上游的 convention 插件(com.mikepenz.convention.*)、dokka / kover / vanniktech 发布、binary-compatibility-validator、AGP classpath 全部随根构建替换剥离------本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.8.0(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| 解析器 | org.jetbrains:markdown:0.7.7-1.0.0-02(社区 ohos 制品线) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.markdown |
bash
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 形态 | 处置 |
|---|---|---|
multiplatform-markdown-renderer |
56 个文件:50+ 个 import Compose,平台差异仅 MarkdownLogger(expect/actual) |
拆分编译:纯 Kotlin 留下,Compose 停靠,新增内核 |
-m2 / -m3 |
纯 Compose 主题(Colors / Typography / 组件默认实现) | 留盘不进构建 |
-code |
MarkdownHighlightedCode + dev.snipme:highlights(JVM-only) |
留盘不进构建 |
-coil2 / -coil3 |
Coil 图片加载封装 | 留盘不进构建 |
核心模块逐文件扫描 import 后,真正能脱离 Compose 编译的只有 5 个:EntityConverter(HTML 实体 / 反斜杠转义还原)、BulletHandler(列表标记策略接口)、MarkdownAlertType(GFM Alert 五类型)、ReferenceLinkHandler(引用链接解析,唯一 Compose 依赖是 @Stable 注解,删注解保留)、Extensions(getUnescapedTextInNode / lookupLinkDefinition / innerList 等纯函数------尾部 2 个 Compose 声明拆出去停靠)。另有 2 个混合文件做外科手术:MarkdownLogger 去掉 Snapshot.withoutReadObservation 包装,Extensions 的 toPxOrZero / codeSpanStyle 搬到 composeMain/utils/Extensions.compose.kt。
expect 声明全库只有 1 处:internal expect fun platformLog(...)。这里有个和 KStore 相反的坑:鸿蒙 Kotlin 的默认层级模板把 nativeMain 挂在 ohos 目标下,上游 nativeMain 的 actual 直接服务 ohos------不需要也不能 再写 ohosMain actual,否则 Conflicting overloads。
1.4 页面边界
text
标题区 Tab 切换 渲染 / 关于
编辑区 TextArea 多行输入(默认内置全特性演示文档)
操作区 渲染按钮(触发 markdownDispatch)
状态行 最近一次结果("渲染成功 N 个块 · 耗时 x ms · 库版本 0.45.0-ohos.1")
渲染区 Scroll + 12 个递归 @Builder 渲染块/样式树(标题字号分级、代码块等宽横向滚动、
引用左竖条、列表缩进、任务勾选、表格对齐、Alert 五色、分隔线、图片)
关于区 版本握手、库简介、架构分层、特性清单、仓库与社区链接
页面启动时先调 markdownVersion() 做握手,再对默认文档做一次真实解析渲染。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上 org.jetbrains:markdown 有没有 ohos 制品。拉 .module 元数据逐个验证:
text
org.jetbrains:markdown:0.7.16(上游版本,eazytec 上只是 Maven Central 代理)
variants: jvm / js / wasmJs / ios / macos / linux / mingw ...
ohos 变体:0 个 ------ 不能用
org.jetbrains:markdown:0.7.7-1.0.0-02(社区 ohos 重发布线)
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published
+ jvmApiElements / jvmRuntimeElements ------ jvm + ohos 双线齐全
两个关键结论:其一,上游依赖坐标不能照抄,必须换社区线 0.7.7-1.0.0-02,这是本仓唯一"换版本"的依赖;其二,社区线同时发布 jvm 变体,jvm() 交叉验证用同一个版本号,不需要双线隔离。解析器在本内核用到的 API 面(GFM 解析、AST 遍历、Entities 实体表)在 0.7.7 与 0.7.16 之间无差异,降版本不降语义。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
上游源码本来就是 Kotlin 2.4.20 编的,鸿蒙线钉 2.4.20-0.1.0-07 同代零降级;旧线 klib(0.7.7-1.0.0-02 构建于 2.2.21 线)可被新编译器消费(元数据向前兼容),实测一次通过。Gradle 9.8.0 上游 wrapper 原样沿用(KStore 先例 9.6.1、compottie 先例 9.7.1,2.4.20 线支持 Gradle 9.x)------只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互套。
更要命的是上游构建逻辑在外部 convention 插件里:com.mikepenz.convention.kotlin-multiplatform 负责声明全部 target(android / jvm / wasmJs / js / ios / macos),它来自 Maven Central 的 com.mikepenz:version-catalog:0.21.0,不认识 ohos,也无法与鸿蒙 Kotlin 共存。处置就是整棵替换根构建,settings.gradle.kts 把 eazytec 仓库写在最前:
kotlin
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()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_multiplatform_markdown_renderer"
include(":multiplatform-markdown-renderer")
include(":example:nativeApp")
2.3 编译真实内核
multiplatform-markdown-renderer/build.gradle.kts 收敛为纯 KMP 模块:
kotlin
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(libs.markdown) // org.jetbrains:markdown:0.7.7-1.0.0-02
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游 commonMain 里的 kotlinx-coroutines 与 kotlinx-collections-immutable 只被 Compose 部分消费,内核同步解析不需要------直接砍掉,把 ohos 制品验证面压到只剩 markdown 一个。实测:
bash
./gradlew :multiplatform-markdown-renderer:compileKotlinOhosArm64 :multiplatform-markdown-renderer:compileKotlinOhosX64
# BUILD SUCCESSFUL
2.4 测试依赖的处置:停靠测试 + 禁用 ohos 测试任务
上游 commonTest 有 5 个测试文件,其中 3 个(AsMarkdownStateTest / ParseMarkdownTest / StreamingMarkdownStateTest)依赖被停靠的 Compose 状态类------注意它们与被停靠类同包 ,grep import 看不出来,必须读源码分类------一并停靠到 src/composeTest/;2 个纯 AST 形状测试(AlertAstShapeTest / TableAstShapeTest)保留,加上 12 项新内核测试在 JVM 跑。根构建全局禁用 ohos 测试任务:
kotlin
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
text
com.mikepenz.markdown/**(上游纯 Kotlin 文件 + document 内核,真实编译)
↓ GFMFlavourDescriptor 解析 → lookupLinkDefinition 预扫 → AST→文档转换
Bridge.kt(@CName × 2,扁平 JSON 进、递归块树出)
↓ UTF-8 JSON
N-API markdownVersion() / markdownDispatch()
↓
ArkUI Index.ets(递归 @Builder 渲染)
请求与响应:
text
请求 : {"op":"version"}
{"op":"parse","markdown":"# Hello","eolAsNewLine":true} // eolAsNewLine 缺省 true
响应 : {"ok":true,"op":"parse","version":"0.45.0-ohos.1","blockCount":N,
"document":{"blocks":[<block>,...]}}
{"ok":false,"error":"..."}
块与 span 的递归形状:
jsonc
// 块
{"type":"heading","level":1,"spans":[<span>...]}
{"type":"paragraph","spans":[<span>...]}
{"type":"code","language":"kotlin" | null,"code":"..."}
{"type":"quote","children":[<block>...]}
{"type":"list","ordered":false,"items":[{"depth":0,"bullet":"• ","number":null,
"task":null|"checked"|"unchecked","spans":[...],"children":[<block>...]}]}
{"type":"table","alignments":["left","center","right"],"headers":[[<span>...]...],
"rows":[[[<span>...]...]...]}
{"type":"alert","alertType":"NOTE|TIP|IMPORTANT|WARNING|CAUTION","title":"Note","children":[...]}
{"type":"image","alt":"..." | null,"url":"..."}
{"type":"thematicBreak"}
// span
{"type":"text","text":"...","bold":false,"italic":false,"strikethrough":false}
{"type":"code","text":"..."}
{"type":"link","destination":"...","spans":[<span>...]}
{"type":"image","alt":"..." | null,"url":"..."}
3.2 载荷为什么是递归块 / 样式树
内核模型是平台中立的数据类(MarkdownDocument / MarkdownBlock / MarkdownSpan),块级分发对应上游 MarkdownElementInternal 的 when(ATX_1-6、SETEXT、PARAGRAPH、CODE_FENCE / CODE_BLOCK、BLOCK_QUOTE、双列表、IMAGE、HORIZONTAL_RULE、TABLE、ALERT),行内遍历对应上游 buildMarkdownAnnotatedString 的 when------把 AnnotatedString.Builder.pushStyle / pop 的栈式样式换成标志位递归 (EMPH 携带 italic=true、STRONG 携带 bold=true),相邻同标志位文本运行合并。四个链接分支(INLINE_LINK / 短长引用链接 / 自动链接 / 邮件链接)逐字移植,含 referenceLinkHandler.store 副作用与 mapAutoLinkToType 映射;代码围栏的偏移裁剪算法、列表起始序号(CommonMark start-number)、任务列表 CHECK_BOX 判定同样逐字来自上游实现。
这样设计的直接收益:同一份 Markdown,内核产出的结构与上游 Compose 管线消费的结构语义一致------ArkTS 拿到的不是"另一个渲染器"的输出,而是上游渲染语义的数据化表达。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
kotlin
@CName("MarkdownDispatch")
fun markdownDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
failure(t.message ?: t.toString()) // {"ok":false,"error":"..."}
}
非法 JSON、缺 markdown 字段、不支持的 op、解析器异常,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
text
ArkTS
│ JSON string
▼
C++ N-API entry(libmarkdown_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosmarkdown.so)
│ MarkdownDispatch / MarkdownVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复刻解析语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
kotlin
@CName("MarkdownDispatch")
fun markdownDispatch(request: String): String // version / parse 一口进
@CName("MarkdownVersion")
fun markdownVersion(): String = "0.45.0-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
MarkdownDispatch |
markdownDispatch(req) |
GFM 解析 + 引用链接预扫 + AST→文档转换 + JSON 序列化 |
MarkdownVersion |
markdownVersion() |
fork 版本握手(真机版本行的数据来源) |
example/nativeApp 的双 ABI sharedLib(baseName = "ohosmarkdown")链接后经 copyKnSo* 任务把 libohosmarkdown.so 与 ~/.konan/.../libc++_shared.so 三件套回填 harmonyApp/entry/libs/<abi>/。
4.3 C++ N-API 方法分发
cpp
napi_property_descriptor desc[] = {
{"markdownDispatch", nullptr, NapiMarkdownDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"markdownVersion", nullptr, NapiMarkdownVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosmarkdown_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosmarkdown_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
cmake
add_library(ohosmarkdown SHARED IMPORTED)
set_target_properties(ohosmarkdown PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(markdown_napi SHARED napi_init.cpp)
target_link_libraries(markdown_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosmarkdown)
OHOS_ARCH 为空或 libohosmarkdown.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
typescript
export const markdownDispatch: (request: string) => string;
export const markdownVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和触发渲染
typescript
aboutToAppear(): void {
this.version = markdownVersion(); // 版本握手:来自真实 Native 调用
this.render();
}
private render(): void {
const request: ParseRequest = { op: 'parse', markdown: this.markdown };
const res = JSON.parse(markdownDispatch(JSON.stringify(request))) as ParseResponse;
if (res.ok && res.document !== undefined) {
this.docBlocks = res.document.blocks;
this.status = `渲染成功 ${res.blockCount} 个块 · 耗时 ${Date.now() - start}ms · 库版本 ${res.version}`;
} else {
this.status = `渲染失败: ${res.error ?? '未知错误'}`;
}
}
接口全部显式声明(MdBlock / MdSpan / MdListItem / ParseResponse 等 6 个 interface),零 any。
5.2 页面交互
渲染 Tab 自上而下:TextArea 编辑区(默认内置全特性演示文档)、渲染按钮、状态行、滚动渲染区。渲染区由 12 个递归 @Builder 组成:RenderBlocks 按 block.type 分发到 RenderHeading(1-6 级字号 30/26/22/18/16/14)、RenderParagraph、RenderCode(等宽字体 + 语言标签 + 横向滚动)、RenderQuote(左竖条)、RenderList(标记列 + 任务勾选 + 嵌套缩进)、RenderTable(表头加粗 + 按分隔行对齐)、RenderAlert(五色块 + emoji 图标)、RenderImage、Divider;行内由 RenderSpans 生成 Span / ImageSpan(粗体 700 / 斜体 / 删除线 / 链接蓝色下划线)。关于 Tab 展示版本握手行、库简介、ArkTS → NAPI → C ABI → Kotlin 内核 → org.jetbrains:markdown 分层说明、特性清单与仓库 / 社区链接。
5.3 对比度和错误边界
链接蓝色下划线、代码块灰底等宽、Alert 五色区分语义;ok:false 时错误原文进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并打 hilog。
第 6 阶段:示例与验证
6.1 工程结构
text
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosmarkdown)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 2 + 文档→JSON(继承自 cmp_qrose 审核模式)
│ └── MiniJson.kt 手写极简 JSON(不引 kotlinx.serialization)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.markdown、versionName 0.45.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/libohosmarkdown_api.h
├── src/main/cpp/types/libmarkdown_napi/{index.d.ts, oh-package.json5}
├── src/main/module.json5 requestPermissions: ohos.permission.INTERNET(网络图片)
└── src/main/ets/pages/Index.ets ArkUI 验收页面(双 Tab)
6.2 构建、签名和安装
bash
./gradlew :multiplatform-markdown-renderer:jvmTest # 35 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 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 的完整配置)。
bash
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.markdown -m entry
四、完整代码对照
4.1 整体架构
text
org.intellij.markdown/**(社区 ohos 线 0.7.7-1.0.0-02,GFM 解析器原样编译)
│ MarkdownParser.buildMarkdownTreeFromString
com.mikepenz.markdown 纯 Kotlin 上游文件(EntityConverter / ReferenceLinkHandler / BulletHandler / MarkdownAlertType)
│ getUnescapedTextInNode / lookupLinkDefinition / 标记策略
com.mikepenz.markdown.document/**(新增内核)
│ MarkdownDocumentBuilder(块级/行内遍历,逐支对应上游 Compose 管线)
Bridge.kt + MiniJson
│ MarkdownDispatch / MarkdownVersion
libmarkdown_napi.so
│ markdownDispatch / markdownVersion
Index.ets
│ JSON parse + 递归 @Builder
▼
ArkUI 真机页面(双 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts |
极简 ohos 构建、eazytec Maven 置前、ohos_multiplatform_markdown_renderer、五个 Compose 子模块与 sample 移出 |
gradle/libs.versions.toml |
Kotlin 2.4.20-0.1.0-07 + markdown 0.7.7-1.0.0-02 钉版 |
build.gradle.kts(根) |
版本 0.45.0-ohos.1、全局禁用 Ohos*Test* 任务 |
multiplatform-markdown-renderer/build.gradle.kts |
纯 KMP:jvm + ohosArm64 + ohosX64,唯一依赖 api(libs.markdown) |
.../src/commonMain/.../document/(3 文件) |
新增内核:模型 / AST→文档转换 / parseMarkdownDocument 门面 + 默认 BulletHandler |
.../src/composeMain/(50+ 文件) |
上游 Compose 文件 git mv 停靠(含 Extensions.compose.kt 拆分件) |
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 验收页面(双 Tab、12 个递归 @Builder) |
README.OpenHarmony_CN.md |
鸿蒙适配主文档(21 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 解析器 | MarkdownParser(GFMFlavourDescriptor()) |
Markdown 文本 → AST(上游同款调用) |
| 内核 | parseMarkdownDocument(content, eolAsNewLine, ...) |
门面:解析 + 引用链接预扫 + AST→文档 |
| 内核 | MarkdownDocumentBuilder |
块级 / 行内遍历(逐支对应上游渲染管线) |
| 上游保留 | lookupLinkDefinition |
引用链接 / 引用图片定义预扫(上游 parseBlocking 同序) |
| 上游保留 | EntityConverter.replaceEntities |
反斜杠转义还原;HTML 实体按上游行为原样保留 |
| 上游保留 | BulletHandler |
列表标记策略(默认 • 与 N. ,与上游 CompositionLocal 默认逐字一致) |
| Native | MarkdownDispatch / MarkdownVersion |
C ABI JSON 导出 |
| N-API | markdownDispatch(req) / markdownVersion() |
向 ArkTS 暴露能力 |
| ArkTS | render() |
触发解析并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
typescript
const res = JSON.parse(markdownDispatch(JSON.stringify({
op: 'parse', markdown: '# Hello **OpenHarmony**'
})));
// res.ok === true → res.document.blocks[0] =
// {type:'heading', level:1, spans:[{type:'text',text:'Hello '},
// {type:'text',text:'OpenHarmony',bold:true}]}
kotlin
val document = parseMarkdownDocument("# Hello **OpenHarmony**")
// document.blocks[0] == MarkdownHeadingBlock(1, listOf(
// MarkdownTextSpan("Hello "), MarkdownTextSpan("OpenHarmony", bold = true)))
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:内核抽取 + Compose 停靠,而不是硬接社区 Compose 线
社区 org.jetbrains.compose:*:1.12.0-0.1.0-07 确实发布 ohos klib(reveal / compose-ripple-indication 已用),但 ohos 上没有组合宿主------@Composable 编得进 so、跑不了组合。这两个库的无头价值在状态机与几何,而 markdown-renderer 的价值恰恰全是 @Composable 渲染组件,硬接没有收益。正确切面是"解析 → 结构化文档"这一层:它天然无 UI 依赖,抽取后内核与 ArkUI 渲染各自稳定,Compose 文件 git mv 停靠字节不动、随时可逆。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.8.0
上游源码本来就是 Kotlin 2.4.20 编的,同代零降级;旧线 klib(markdown 0.7.7-1.0.0-02)可被新编译器消费,实测链接通过。Gradle 方面 2.4.20 线支持 9.x(KStore 先例),上游 wrapper 9.8.0 原样保留------只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。
决策 3:markdown 换社区线 0.7.7-1.0.0-02,五个 Compose 子模块留盘
上游的 0.7.16 没有 ohos 变体,社区重发布线停在 0.7.7-1.0.0-02------这是本仓唯一降版本的依赖,内核用到的 API 面在两版之间无差异,上游 23 项 AST 形状测试照跑全绿就是直接证据。-m2 / -m3 / -code / -coil2 / -coil3 没有内核价值(-code 依赖 JVM-only 的 highlights,-coil* 依赖 Coil),照 qrose 处理 qrose-core、KStore 处理 kstore-storage 的先例留盘不进构建。
决策 4:桥载荷 = 递归块 / 样式树 + 手写 MiniJson
KStore 的载荷是单个 JsonElement,本库的载荷天然是递归结构(块含子块、链接含嵌套 span)。样式以标志位(bold / italic / strikethrough)累加并合并相邻同标志文本运行,对应上游 AnnotatedString 栈式 pushStyle 的语义,也让 ArkTS 侧一个 Span 一个节点直接可渲。序列化用与 qrose 同款的手写 MiniJson(parser + writer 约 200 行),不引 kotlinx.serialization 插件与运行时,so 依赖面最小。
决策 5:jvmTest 交叉验证 + 新增内核测试
内核逻辑在 commonMain,JVM 与 ohos 跑同一份代码。35 项 jvmTest 里 12 项是新写的内核结构测试(标题 / 链接 / 列表 / 表格 / Alert / 实体行为等),23 项是上游原 AST 形状测试零改动保留------后者钉住"换版本不换解析行为",前者钉住"遍历语义没漂"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11 x64、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
bash
./gradlew :multiplatform-markdown-renderer:jvmTest
35 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
MarkdownDocumentTest(新增) |
12 | 内核结构语义:标题 / 段落样式合并、代码围栏与行内代码、三类链接、引用链接解析、列表起始序号与嵌套、任务列表、引用与 Alert、表格对齐、分隔线与图片 span、转义与实体行为、eolAsNewLine 开关、空文档 |
AlertAstShapeTest(上游保留) |
7 | GFM Alert 的 AST 形状锚点 |
TableAstShapeTest(上游保留) |
16 | GFM 表格 CELL 的 AST 形状锚点 |
markdown 从 0.7.16 换到 0.7.7-1.0.0-02 后上游 AST 测试照跑全绿------这正是"降版本不降语义"的直接证据。
6.3 HAP 验证
text
:multiplatform-markdown-renderer:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: MarkdownDispatch / MarkdownVersion 均为全局 T 符号(双 ABI 实测)
BuildNativeWithCmake: finished(双 ABI 编出 libmarkdown_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
KN so(debugShared 未 strip)arm64 约 6.0MB、x86_64 约 5.4MB,含 Kotlin/Native 运行时与 GFM 解析器;HAP 内每个 ABI 三件套(libmarkdown_napi.so / libohosmarkdown.so / libc++_shared.so)齐全。
6.4 功能用例
- 启动后「关于」Tab 顶部显示
markdownVersion() → 0.45.0-ohos.1(来自真实 Native 调用,非静态文字); - 「渲染」Tab 默认演示文档完整渲染:六级标题字号递减、粗 / 斜 / 删除线、行内代码等宽着色、围栏代码块带 kotlin 语言标签、引用左竖条、无序 + 嵌套列表、有序列表从 3 起、任务列表 ☑ / ☐、表格三列三种对齐、AtomGit 链接蓝色下划线、NOTE / TIP 双 Alert 色块、分隔线;
- 编辑 TextArea 内容后点渲染,状态行的块数与耗时随内容变化------证明每次都是真实解析;
- 清空输入点渲染,返回 0 块不崩溃;输入畸形内容不崩溃,状态行给出
ok:false错误原文(错误通道); - 网络图片块由 ArkUI
Image原生加载(module.json5 已声明 INTERNET 权限),无网络时降级显示; hdc shell hilog | grep MarkdownNapi可见每次请求的字节数日志。
6.5 验证结论
解析内核、实体转换、引用链接解析、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,渲染 Tab 展示了真实的 Markdown 解析渲染会话:版本握手、全特性默认文档、实时编辑重渲染与错误通道(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
bash
git clone https://atomgit.com/oh-tpc/multiplatform-markdown-renderer.git
cd multiplatform-markdown-renderer
export JAVA_HOME="/path/to/jdk-21"
./gradlew :multiplatform-markdown-renderer:compileKotlinOhosArm64 :multiplatform-markdown-renderer:compileKotlinOhosX64 # klib
./gradlew :multiplatform-markdown-renderer:jvmTest # 35 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 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.markdown -m entry
hdc shell hilog | grep MarkdownNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:multiplatform-markdown-renderer真正编译成ohosArm64/ohosX64动态库------对 UI 库而言,可编译的切面是解析内核,不是渲染组件; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;org.jetbrains:markdown的0.7.16在 Maven Central 与 eazytec 上都没有 ohos 变体(后者只是代理镜像),必须钉社区线0.7.7-1.0.0-02------验证方法是拉.module元数据 grepohos,不能只看仓库里"有这个版本";- 上游构建逻辑全在外部 convention 插件(
com.mikepenz:version-catalogMaven 工件)里,target 声明照抄进 ohos 构建必然失败------处置只有整棵根构建替换; - 同包引用不需要 import:commonTest 里 3 个测试与被停靠类同包,grep import 看不出依赖,必须读源码分类,否则停靠后测试编译爆炸在很晚的阶段才暴露;
- 鸿蒙 Kotlin 默认层级模板把
nativeMain挂在 ohos 目标下,ohosMain里再写platformLogactual 会Conflicting overloads------上游 nativeMain actual 直接服务 ohos,不写就是对的; String.format在 Kotlin/Native 不可用(java.*一系同理),MiniJson 的\uXXXX转义要查表手拼十六进制;@Stable是唯一 Compose 依赖的类型(ReferenceLinkHandler)要"删注解保留"而不是整体停靠------内核需要它做引用链接解析,这是 qrose「剥离注解」先例的直接复用;- ArkTS 枚举成员与直觉不符:
TextAlign用LEFT/RIGHT(Center却是帕斯卡 case),CopyOptions没有InMemory(合法成员None/InApp/LocalDevice)------以本机 SDK 类型检查报错为准逐个替换; - ArkTS 的
Span不可嵌套,链接内含粗体等样式时拍平为着色下划线文本(flattenSpansText),不要尝试嵌套 Span; - GFM 两个 Alert 之间必须空行分隔,否则第二个
[!TIP]会被并进上一个 Alert 的内容; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可;KN 返回字符串必须DisposeString归还运行时;签名材料由 DevEco 自动写入build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- Compose 渲染层(
Markdown(...)composable、m2 / m3 主题、extendedspans、coil2 / coil3 图片加载、highlights 语法高亮)不在 ohos 交付面内,停靠在composeMain/待组合宿主成熟后接回; - 语法高亮依赖 JVM-only 的
dev.snipme:highlights,ohos 上代码块为等宽纯文本; - 图片由 ArkUI
Image原生加载,不经 Coil,SVG / 变换等 Coil 能力不可用; - 链接内嵌套样式在 ArkTS 侧拍平为纯文本(Span 不可嵌套);
- 流式渲染(
StreamingMarkdownState)是 Compose 侧能力,未跨桥,桥每次全量解析; - android / js / wasmJs / ios / macos 目标剥离(源文件留在树里),发布、dokka、kover 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.markdown匹配的签名。
8.3 未来优化方向
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 长文档 Lazy 化的 ArkTS 侧最佳实践示例(按块懒加载);
- 评估社区 ohos 高亮方案替代 highlights 的可行性;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 跟进上游
org.jetbrains:markdown新版本的 ohos 重发布(当前社区线停在 0.7.7)与 Compose-ohos 组合宿主进展,触发 Compose 层接回评估。
九、总结
9.1 核心难点回顾
text
上游 multiplatform-markdown-renderer(50+ Compose 文件 + 5 个纯 Kotlin 文件)
→ 验证 markdown ohos 制品线(0.7.16 无变体 → 钉 0.7.7-1.0.0-02)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.8.0 沿用)
→ 根构建替换(cmp_qrose 模式,脱离外部 convention 插件)
→ Compose 文件 git mv 停靠 composeMain + 3 个混合文件外科手术拆分
→ document 内核(块/样式模型 + 逐支对应上游的 AST→文档转换)
→ C ABI(@CName × 2)+ MiniJson 递归块树
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + 递归 @Builder
→ ArkUI 真机双 Tab 验收(版本握手 / 全特性渲染 / 实时编辑 / 错误通道)
9.2 封装层次
- 解析层:社区 ohos 线
org.jetbrains:markdown原样编译(GFM 解析器、实体表); - 内核层:上游纯 Kotlin 文件 + 新增
document包(模型、AST→文档转换、门面); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、接口显式声明(零 any)、渲染分发、错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配切面在"数据不在像素":Compose 库接鸿蒙,先找那条不依赖 UI 框架的数据管线(本库是解析→结构化文档),把它真实编译过 FFI,渲染层交给 ArkUI 等价实现------切面找错,就会变成整库重写;
- 依赖坐标不能照抄,制品线要当天验证 :同一个坐标在代理仓库"存在"不等于"有 ohos 变体",
.module元数据 grepohos是唯一可信的验收;换线(甚至降版本)之后,上游 AST 测试零改动全绿就是语义没漂的证明; - 停靠要可逆,拆分要留痕 :Compose 文件用
git mv整体停靠(字节不动)、混合文件只拆出 Compose 声明并留注释------接回路线的成本在适配当天就应该被设计好,而不是事后再想。
9.4 适配成果
当前适配已完成:
- multiplatform-markdown-renderer
0.45.0-ohos.1解析内核的双 ABI Kotlin/Native 编译(上游纯 Kotlin 文件零重写,新增document内核包 3 文件); - 社区 markdown ohos 制品线在渲染库上的完整落地(
org.jetbrains:markdown:0.7.7-1.0.0-02,jvm + ohos 双线同版本); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,双 ABI 实测); - DevEco Stage ArkUI 双 Tab 验收页面(版本握手、全特性渲染、实时编辑重渲染、错误通道);
- 35 项 JVM 交叉验证全绿(12 项内核新测试 + 23 项上游 AST 测试零改动)+ 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。