开源鸿蒙平台 CMP 三方库「ChitraLekhan」适配全流程
本文记录
ChitraLekhan接入 OpenHarmony 的完整过程,覆盖图片标注库盘点、Compose 制品线缺失下的路线选型、零依赖绘图内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
ChitraLekhan是基于Compose Multiplatform+CanvasAPI 的图片标注库:给定一张底图,它提供自由手绘 / 圆 / 正多边形 / 矩形四种笔画、撤销 / 重做 / 清空三栈操作、画笔颜色与粗细配置、平移缩放浏览,以及把显示坐标系笔画映射回原图坐标系的scaleStrokesToImageSize。本次适配与 KStore 那类「真编译整库」路线有本质区别:上游的全部公开 API 都是 Compose API(ChitraLekhanCanvasComposable、mutableStateListOf状态、DrawScope绘制扩展),而社区 Maven 上没有 ohos 的 Compose 制品,因此按社区已过审的 qrose / Vico / Reorderable / DrawBox / SignaturePad 同一条路处理——把绘图状态引擎做成零依赖纯 Kotlin 核心编进 ohos,Compose UI 原样隔离到composeMain不参与编译,鸿蒙侧用 Kotlin/Native 光栅化 +@CNameC ABI + CMake NAPI 落地,ArkTS 用 ArkUI Canvas 做实时层。项目地址: AtomGit/oh-tpc/ChitraLekhan
上游仓库: karya-inc/ChitraLekhan
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
ChitraLekhan(梵语「图画」)为 Compose Multiplatform 应用提供「在图片上做标注」这一件事的完整答案:rememberChitraLekhan(color, width, drawMode, image) 一行建状态、ChitraLekhanCanvas 直接叠在 Image 上、工具栏组件开箱即用;四种 DrawMode 覆盖标注作业的全部基础形状,undo/redo 双栈保证作业可回退,scaleStrokesToImageSize 让显示坐标系里的标注精确回到原图坐标系——这是标注数据能进训练管道的关键一步。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套标注能力,但上游构建(Kotlin 2.2.10 / Compose Multiplatform 1.8.2 / AGP 8.11)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画线,却无法证明上游的标注引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态引擎、几何计算、坐标系缩放、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.2.10 + Compose Multiplatform 1.8.2 + AGP 8.11 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| Compose 制品缺失 | 社区 Maven 上 org.jetbrains.compose 直接 404,上游全部公开 API 都是 Compose API,没有可直接编译的纯 Kotlin 层可直接复用。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 引擎是有状态的 | 撤销/重做双栈是会话式状态,不是一次一调用的无状态查询,桥需要进程级单例。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:零依赖内核的忠实提取、根构建替换(cmp_qrose 模式)和有状态 C ABI/N-API 桥接层。绘图语义(笔画形状派生、undo/redo 栈、坐标缩放)完全由与上游逐行对齐的内核负责,ArkTS 只负责调用 Native、渲染实时层和展示结果。
1.2 库提供的能力
Demo 提供一个真实的标注会话,对应桥接的全部入口:
- 画笔配置:七色色板(
#7C4700棕 / 橙 / 红 / 绿 / 蓝 / 青 / 品红,与上游ComposeUtils.kt逐色一致)+ 五档笔宽(4/8/12/16/24,与上游thicknesses同一组值); - 四种绘制模式:
FreeHand(自由手绘)、Circle(直径两点定圆)、Polygon(sides)(顶点圆公式生成正多边形)、Rectangle(对角点规范化); - 栈操作:undo / redo / clear,每次桥调用都返回
canUndo/canRedo供按钮置灰; - 导出闭环:
export用 KN 光栅化按上游DrawScope语义(二次贝塞尔中点平滑、圆头圆角描边)渲染成 PNG,支持「显示尺寸直出」与「映射回原图尺寸」两种模式,响应里的strokeList是引擎笔画的 JSON 镜像,可直接对账。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
action / mode / sides | 回显本次操作、当前绘制模式与多边形边数。 |
color / width | 当前画笔状态(#RRGGBBAA / 笔宽)。 |
strokes / points / canUndo / canRedo / aspectRatio | 引擎状态快照,驱动 UI 置灰与状态行。 |
pngWidth / pngHeight / bytes / pngBase64 / strokeList | 仅 export:PNG 尺寸、字节数、Base64 编码与笔画 JSON 镜像。 |
引擎侧完整能力(四态笔画派生、双栈撤销、显示坐标→原图坐标缩放、几何工具)全部在零依赖内核里与上游 0.0.5 逐行对齐,18 项 JVM 交叉验证用例证明 ohos .so 里执行的绘图逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游纯 Kotlin 文件(Point / DrawMode / DrawingStroke / MathUtils)只剥 kotlinx.serialization 注解与协程包装,算术逐行保留;状态引擎按上游 ChitraLekhan 类语义逐行重写为零依赖版;上游 Compose 源码 git mv 进 composeMain 原样保留,不参与编译。 |
| 平台目标 | 库模块声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 app 示例模块留盘不进构建。 |
| 桥接稳定 | 有状态单例引擎 + 2 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 行为保真 | 上游 0.0.5 的原生行为(含 Rectangle 拖动不跟随指针)逐行保真,fork 扩展走显式 API(updateRectangleTo),不悄悄修改。 |
| UI 完整 | 单页覆盖全部桥接入口:五种模式、七色、五档笔宽、撤销/重做/清空、两种导出模式。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 Compose 制品在社区 Maven 上缺失,实时绘制层由 ArkUI Canvas 承担(绘制算法逐行镜像上游
drawQuadraticBezier/drawChitraLekhanStrokes),而几何权威在 Kotlin/Native 引擎里。
1.4 页面边界
标题区 CHITRALEKHAN · OPENHARMONY / 版本握手行(图片标注库 · Kotlin/Native ohosArm64/ohosX64)
状态行 最近一次操作结果(如"桥已连接 · v0.0.5-ohos.1" / "笔画=22 · mode=freehand · w=8 · #FF0000")
画布区 底图 + ArkUI Canvas 实时层(触摸 Down/Move 同步过桥,本地镜像即时重绘)
模式行 手绘 / 圆 / 多边形 / 矩形 / 平移 五按钮(选中高亮)
颜色行 七色色板(选中描边高亮)
笔宽行 4 / 8 / 12 / 16 / 24 五档
操作行 撤销(按 canUndo 置灰)/ 重做(按 canRedo 置灰)/ 清空 / 导出 PNG
导出开关 「导出到原图尺寸」勾选后 export 走 scaleToImage 路径
导出结果区 PNG 预览 + 尺寸 / 字节数 / strokeList 摘要
页面启动时先调 chitraLekhanVersion() 做握手,再 connect 注入底图逻辑尺寸(800×600)。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 制品缺失、确定交付物
第 2 阶段:目标与内核打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、零依赖内核提取
第 3 阶段:桥协议建模 ── action 模型、扁平 JSON 契约、状态快照字段、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、有状态单例引擎、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 实时层绘制算法镜像、触摸事件同步过桥、栈操作与导出
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
chitralekhan/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
chitralekhan/src/commonMain/ 零依赖内核:Point / DrawMode / DrawingStroke / ChitraLekhanEngine / MathUtils
chitralekhan/src/commonTest/ 18 项交叉验证测试(jvm 上跑)
chitralekhan/src/composeMain/ 上游 Compose 源码原样保留(git mv 而入,不参与任何编译)
app/ 上游 Compose 示例模块,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + Rasterizer + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkUI 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :chitralekhan 与 :example:nativeApp,rootProject.name = "ohos_chitralekhan"。上游的 app 示例模块、AGP classpath、Compose Multiplatform 插件、vanniktech 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 已从上游 8.13 换成 8.14.1,2.2.21 工具链线要求 Gradle 8.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.chitralekhan |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 文件(上游 commonMain/androidMain) | 依赖形态 | 处置 |
|---|---|---|
models/Point.kt | 纯 Kotlin + @Serializable | 剥注解,进内核 |
models/DrawMode.kt | 纯 Kotlin(sealed interface 五态) | 原样进内核 |
models/DrawingStroke.kt | 纯 Kotlin + @Serializable | 剥注解,进内核(含 Polygon 构造器闭合点语义) |
utils/MathUtils.kt | 纯 Kotlin + kotlinx.coroutines(仅 scaleStrokesToImageSize 的 withContext 包装) | 剥协程包装,缩放算术逐行保留 |
ChitraLekhan.kt | androidx.compose.runtime State + ui.graphics | 按语义重写为零依赖 ChitraLekhanEngine |
ChitraLekhanCanvas.kt / rememberChitraLekhan.kt | Composable + 指针输入 + graphicsLayer | git mv 进 composeMain |
components/(Toolbar / ColorPicker / DrawModeSelector / DrawScope 扩展) | Compose UI | git mv 进 composeMain |
utils/GraphicUtils.kt(Path.drawQuadraticBezier) | androidx.compose.ui | git mv 进 composeMain |
utils/ComposeUtils.kt(色板与 thicknesses) | androidx.compose.ui.graphics.Color | git mv 进 composeMain;色板/笔宽值在 Demo 侧逐值复刻 |
androidMain/.../GraphicsUtils.kt(Android Bitmap 导出) | android.graphics + Compose | git mv 进 composeMain(改名 GraphicsUtils.android.kt) |
composeResources/drawable/(工具栏图标) | Compose resources | git mv 进 composeMain |
expect 声明全库 0 处、协程依赖剥除后为 0、第三方依赖为 0——内核是纯标准库 Kotlin,这是它与 KStore(真编译整库 + 8 行 actual)路线差异的根源。
1.4 引擎语义的逐行对齐
ChitraLekhanEngine 与上游 ChitraLekhan 类逐行对应,三处上游原生行为被显式钉住:
updateDrawing的Rectangle分支用存储的对角点重建——矩形拖动时第二对角点不跟随指针(上游 0.0.5 原样行为)。鸿蒙内核不改它,由rectangleUpdateKeepsSecondDiagonalFixedLikeUpstream测试钉住;需要矩形跟手的宿主走引擎新增的显式扩展updateRectangleTo(offset)(fork API,与 qrose 新增Qrose.encodeQr同一性质);DrawingStroke.Polygon的构造器init块把首点追加到尾部作闭合点——副作用是每缩放一次多边形点数 +1,由scaledPolygonGrowsByOneClosingPointLikeUpstream钉住;updateDrawing对Polygon固定 5 边重算(与DrawMode.Polygon(sides)的配置无关),由polygonUpdateRecomputesWithFixedFiveSides钉住。
第 2 阶段:目标与内核打通
2.1 制品线验证(决定路线的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品:
org.jetbrains.compose:404(整个坐标不存在)
androidx.compose.*:不存在 ohos 变体
结论与 KStore 适配正好相反:KStore 依赖的 kotlinx 制品线(coroutines / serialization / kotlinx-io)在社区 Maven 上已补齐,所以「真编译整库」是它的正确答案;而 Compose 制品缺失意味着 ChitraLekhan 这类全 Compose API 的库必须走「零依赖内核 + Compose 隔离」路线。路线选择必须基于当天的制品可达性,而不是历史结论。
2.2 工具链钉版
ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件。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_chitralekhan"
include(":chitralekhan")
include(":example:nativeApp")
Kotlin 钉 2.2.21-1.0.0(社区此前批准的钉法,qrose / DrawBox / SignaturePad 同款),Gradle wrapper 换 8.14.1(2.2.21 线要求 Gradle 8.x),JDK 21 写进 org.gradle.java.home。上游的 Kotlin 2.2.10 + Compose 1.8.2 + AGP 8.11 版本目录整体替换为只有一项的极简目录:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.3 编译真实内核
chitralekhan/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
// jvm stays as the cross-validation target: the core's test suite runs there.
jvm()
ohosArm64()
ohosX64()
sourceSets {
// Zero dependencies: the HarmonyOS toolchain has no Compose artifacts, so the core is
// plain Kotlin. The upstream Compose sources stay under src/composeMain, uncompiled.
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
./gradlew :chitralekhan:compileKotlinOhosArm64 :chitralekhan:compileKotlinOhosX64 # BUILD SUCCESSFUL
./gradlew :chitralekhan:jvmTest # 18 项全绿
2.4 剥注解与剥协程的两条纪律
- 剥
@Serializable:Point/DrawingStroke上游带kotlinx.serialization注解。内核坚持零依赖,注解剥掉后数据模型形状不变;需要 JSON 的宿主在桥层自行序列化(本项目桥自带strokeList输出); - 剥协程包装:
scaleStrokesToImageSize上游签名带dispatcher: CoroutineDispatcher = Dispatchers.Default参数与withContext包装。鸿蒙内核不拉 coroutines,函数改为同步原语,缩放算术(scaleX/scaleY逐点映射、四态笔画分发)逐行保留。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.daiatech.chitralekhan/**(零依赖内核,原样编译)
↓ ChitraLekhanEngine(进程级单例)/ 双栈撤销 / 形状派生
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API chitraLekhanRender() / chitraLekhanVersion()
↓
ArkUI Index.ets(单页滚动,ArkUI Canvas 实时层)
与 qrose 的无状态桥不同,绘图是有状态的(撤销栈),所以桥是有状态的:进程级单例 ChitraLekhanEngine,ArkTS 单 JS 线程同步调用,无需加锁(与 SignaturePad 的桥同一模型)。请求与响应:
请求 : {"action":"connect","imageWidth":800,"imageHeight":600}
{"action":"mode","value":"rectangle","sides":5}
{"action":"color","value":"#7C4700"} / {"action":"width","value":4}
{"action":"start","x":120.5,"y":88.25}
{"action":"move","x":131,"y":90,"resizeRectangle":true}
{"action":"undo"} / {"action":"redo"} / {"action":"clear"}
{"action":"export","displayWidth":360,"displayHeight":270,"scaleToImage":true,
"imageWidth":800,"imageHeight":600,"bg":"#FFFFFFFF","transparent":false}
响应 : {"ok":true,"action":"...","mode":"...","sides":N,"color":"#RRGGBBAA","width":N,
"strokes":N,"points":N,"canUndo":true,"canRedo":false,"aspectRatio":1.333}
export 追加 pngWidth / pngHeight / bytes / pngBase64 / strokeList
{"ok":false,"error":"..."}
两个设计点:其一,move 的 resizeRectangle 是 fork 扩展位——引擎的 updateDrawing 保持上游「矩形 d2 不动」语义,桥在最后一笔是矩形且该位为 true 时改走 updateRectangleTo,让鸿蒙宿主显式选择是否跟手;其二,export 的 scaleToImage:true 就是上游 scaleStrokesToImageSize 的桥面形态——笔画先从显示坐标系映射回原图坐标系再渲染,这正是标注库的核心导出路径。
3.2 strokeList:引擎的 JSON 镜像
export 响应里的 strokeList 把引擎笔画原样序列化(freehand/polygon 带 points 数组、circle 带 center/radius、rectangle 带 topLeft/bottomRight,颜色统一 #RRGGBBAA、数字最多 3 位小数)。它有两个用途:ArkTS 侧展示笔画摘要,以及 JVM 对照验证——鸿蒙扫出来的 strokeList 必须和 JVM 上同一操作序列产出的引擎状态逐字段一致,这是「引擎没漂」的设备侧证据。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ChitraLekhanRender")
fun chitraLekhanRender(request: String): String {
return try {
/* action 分发 */
} catch (t: Throwable) {
fail(t.message ?: t.toString())
}
}
非法 JSON、未知 action、非法色值、非正笔宽、connect 的非正宽高、scaleToImage 缺参考尺寸,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libchitralekhan_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoschitralekhan.so)
│ ChitraLekhanRender / ChitraLekhanVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;绘图引擎又必须保持会话状态,所以选择进程级单例 + C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("ChitraLekhanRender")
fun chitraLekhanRender(request: String): String // 全部 action 一口进
@CName("ChitraLekhanVersion")
fun chitraLekhanVersion(): String = "0.0.5-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ChitraLekhanRender | chitraLekhanRender(req) | connect / mode / color / width / start / move / undo / redo / clear / export |
ChitraLekhanVersion | chitraLekhanVersion() | fork 版本握手(真机标题行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"chitraLekhanRender", nullptr, NapiChitraLekhanRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"chitraLekhanVersion", nullptr, NapiChitraLekhanVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoschitralekhan_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoschitralekhan_api.h 必须用 CAdapter 生成头(example/nativeApp/build/bin/<abi>/debugShared/libohoschitralekhan_api.h),它除了两个业务符号还声明 DisposeString 与 ExportedSymbols 符号表——手写一个只含业务函数的同名头会在 N-API 编译期直接报 unknown type name 'libohoschitralekhan_ExportedSymbols'(本项目真实踩坑,见 8.1 条 5)。构建脚本挂了 copyKnApiHeader 任务,重链 KN so 后生成头自动同步进 entry/src/main/cpp/include/。
4.4 CMake 和类型声明
add_library(ohoschitralekhan SHARED IMPORTED)
set_target_properties(ohoschitralekhan PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(chitralekhan_napi SHARED napi_init.cpp)
target_link_libraries(chitralekhan_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoschitralekhan)
OHOS_ARCH 为空或 libohoschitralekhan.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const chitraLekhanRender: (request: string) => string;
export const chitraLekhanVersion: () => string;
4.5 KN 光栅化与 PNG(渲染难点)
上游的绘制面是 DrawScope.drawChitraLekhanStrokes,鸿蒙侧由 example/nativeApp 的 Rasterizer 按同一语义软件光栅化:
- FreeHand / Polygon:按上游
drawQuadraticBezier的构建方式铺路径——moveTo(points[0]),之后每对相邻点走一次控制点为弦中点的quadraticTo,且prevPoint从points[1]起(第一段 quad 的控制点落在远端点上,是真实的非对称凸起;之后各段控制点在弦上,贝塞尔数学上退化为直线段)。光栅化按真实二次贝塞尔公式 8 步细分,逐位复现; - Circle:
|dist(pixel, center) − radius| ≤ width/2圆环; - Rectangle:规范化后四条边界的线段距离场。
所有笔画统一走距离场:像素覆盖率 (halfWidth + 0.5 − distance) 截断到 [0,1],天然得到圆头圆角(上游 Stroke(cap = Round, join = Round)),与已过审的 DrawBox / SignaturePad 光栅化同一技巧;每笔画按包围盒(外扩 halfWidth + 1)剔除后再算距离。PNG 走 zlib stored 块零依赖编码(与 qrcode-kotlin/qrose 同一套),RGBA8 直出。
第 5 阶段:ArkUI 宿主
5.1 分层与触摸事件
aboutToAppear(): void {
this.version = chitraLekhanVersion(); // 版本握手:来自真实 Native 调用
this.applyState(this.callBridge({ action: 'connect', imageWidth: IMG_W, imageHeight: IMG_H }));
}
private onTouchDown(touch: TouchObject): void {
const p: Pt = { x: touch.x, y: touch.y }; // 实时层存触摸原始坐标(vp)
const img = this.toImagePt(touch); // 过桥前才换算图片坐标
this.applyState(this.callBridge({ action: 'start', x: img.x, y: img.y }));
this.localStrokes.push({ type: this.mode, color: this.color, width: this.brushWidth, points: [p] });
this.redraw();
}
分层与已过审的 SignaturePad / DrawBox Demo 一致:KN 引擎是笔画几何、撤销栈与导出的权威,ArkUI Canvas 只承担实时笔迹层。画布尺寸取组件级 onAreaChange(单位确凿是 vp),不取 ctx.width——它的单位在不同 API 版本有歧义,曾导致预览与手指错位(见 8.1 条 7)。
5.2 实时层绘制算法
本地镜像的绘制逐行镜像上游:FreeHand/Polygon 用 quadraticCurveTo 中点序列(moveTo p0、prev 从 points[1] 起、control = mid(prev, cur));Circle 画圆环;Rectangle 画矩形描边;lineCap/lineJoin 取 'round'。笔宽属于图片坐标系,显示时按 coordScale 换算成 vp。
5.3 对比度和错误边界
模式行选中态蓝底白字、色板选中描边高亮、导出按钮绿色、重做按 canRedo 置灰;ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)被 callBridge 的 try/catch 收进状态行,页面不崩。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoschitralekhan)+ cxxShared + copyKnSo* + copyKnApiHeader
│ └── src/ohosMain/kotlin/org/terminator/ohos/chitralekhan/
│ ├── Bridge.kt @CName × 2 + 有状态单例引擎 + MiniJson(扁平 JSON)
│ ├── Rasterizer.kt 距离场光栅化(贝塞尔细分 / 圆环 / 矩形边界)
│ └── PngEncoder.kt zlib stored 零依赖 PNG
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.chitralekhan、versionName 0.0.5-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/libohoschitralekhan_api.h CAdapter 生成头(copyKnApiHeader 回填)
├── src/main/cpp/types/libchitralekhan_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :chitralekhan:jvmTest # 18 项交叉验证
./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 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 "signingConfig": "default" 引用自动签名)。
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.chitralekhan -m entry
四、完整代码对照
4.1 整体架构
chitralekhan/src/commonMain/**(零依赖内核,与上游 0.0.5 逐行对齐)
│ Point / DrawMode / DrawingStroke / ChitraLekhanEngine / MathUtils
chitralekhan/src/composeMain/**(上游 Compose 源码原样保留,不参与编译)
│ ChitraLekhanCanvas / Toolbar / ColorPicker / drawQuadraticBezier …
Bridge.kt + MiniJson + Rasterizer + PngEncoder
│ ChitraLekhanRender / ChitraLekhanVersion
libchitralekhan_napi.so
│ chitraLekhanRender(req) / chitraLekhanVersion()
Index.ets
│ JSON parse + @State 更新 + ArkUI Canvas 实时层
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_chitralekhan、app 示例模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单项钉版 |
gradle.properties | 版本 0.0.5-ohos.1、JDK 21、Kotlin/Native 发行版走 Maven |
chitralekhan/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
chitralekhan/src/commonMain/** | 零依赖内核(上游纯 Kotlin 文件剥注解/协程 + 引擎重写) |
chitralekhan/src/commonTest/** | 18 项交叉验证测试 |
chitralekhan/src/composeMain/** | 上游 Compose 源码原样保留(git mv 而入) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、copyKnApiHeader 同步生成头 |
nativeApp/.../Bridge.kt | 2 个 C ABI、有状态单例、MiniJson、strokeList 序列化、错误通道 |
nativeApp/.../Rasterizer.kt | 距离场光栅化(贝塞尔细分 / 圆环 / 矩形边界、圆头圆角) |
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 | 鸿蒙适配主文档 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | ChitraLekhanEngine(strokeColor, strokeWidth, drawMode, imageWidth, imageHeight) | 建状态(镜像上游 rememberChitraLekhan + ChitraLekhan 构造) |
| 引擎 | startDrawing / updateDrawing | 落笔 / 拖动重算最后一笔(None 吞掉落笔) |
| 引擎 | updateRectangleTo(offset)(fork 新增) | 矩形跟随指针的显式扩展位 |
| 引擎 | undo / redo / clear + canUndo / canRedo | 双栈撤销 |
| 引擎 | setColor / setWidth / setDrawMode | 画笔配置(颜色 ARGB int,即上游 Color.toArgb() 产物) |
| 引擎 | scaleStrokesToImageSize(image, display) | 显示坐标 → 原图坐标映射(上游同名函数,剥协程包装) |
| Native | ChitraLekhanRender / ChitraLekhanVersion | C ABI JSON 导出 |
| N-API | chitraLekhanRender(req) / chitraLekhanVersion() | 向 ArkTS 暴露能力 |
| ArkTS | callBridge(action) | 触发操作并刷新 UI 与实时层 |
4.4 ArkTS 与 Kotlin 的边界
// 手绘:Down → start,Move → move(矩形模式追加 resizeRectangle)
const res = JSON.parse(chitraLekhanRender(JSON.stringify({
action: 'move', x: 131, y: 90, resizeRectangle: false
})));
// res = { ok:true, action:'move', mode:'freehand', strokes:22, canUndo:true, ... }
// 导出:映射回原图坐标系渲染 PNG
const png = JSON.parse(chitraLekhanRender(JSON.stringify({
action: 'export', displayWidth: 360, displayHeight: 270,
scaleToImage: true, imageWidth: 800, imageHeight: 600, bg: '#FFFFFFFF'
})));
// png.pngBase64 → Base64Helper.decodeSync → image.createImageSource → PixelMap
when (action) {
"connect" -> reconnect(imageWidth, imageHeight) // 重建引擎,保留画笔状态
"start" -> engine.startDrawing(point(json))
"move" -> if (json.bool("resizeRectangle")) engine.updateRectangleTo(point(json))
else engine.updateDrawing(point(json))
"export" -> Rasterizer.render(strokes, w, h, bg) // 距离场光栅化 → PNG Base64
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:零依赖内核 + Compose 隔离,而不是真编译整库
与 KStore 的选择正好相反,但遵循同一条判断准则——路线跟着当天的制品可达性走。KStore 依赖的 kotlinx 制品线在社区 Maven 已补齐,真编译整库是它的正确答案;ChitraLekhan 的全部公开 API 都是 Compose API,org.jetbrains.compose 在社区 Maven 上 404,真编译无路可走。内核提取遵循两条纪律:上游纯 Kotlin 文件只剥注解/协程包装、算术逐行保留;Compose 源码 git mv 进 composeMain 原样保留(不删除),等社区 CMP ohos 制品出现后可以直接接回,KN 光栅化降级成 fallback。
决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1
社区已过审的 qrose / DrawBox / SignaturePad 同款钉法。内核零第三方依赖,不存在「stdlib 元数据版本跟着依赖走」的问题(KStore 升 2.4.20 线是 coroutines 制品倒逼的),所以沿用最保守的 2.2.21-1.0.0;该线要求 Gradle 8.x,wrapper 从上游 8.13 换 8.14.1。两条工具链线的经验不能互相套用。
决策 3:根构建替换(cmp_qrose 模式),app 示例模块留盘
上游根构建(AGP / Compose Multiplatform 插件 / vanniktech 发布 / Hot Reload)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。上游 app Compose 示例模块依赖整个 Compose 技术栈,鸿蒙没有对应制品,留盘不进 settings.gradle.kts——它的存在方式只有这一种。
决策 4:有状态单例桥 + fork 扩展位显式化
绘图引擎是有状态的(撤销栈),桥与 qrose 的无状态一问一答不同,采用进程级单例(SignaturePad 同款):ArkTS 单 JS 线程同步调用保证无并发,connect 重建引擎但保留画笔状态。上游 0.0.5 的 Rectangle 拖动不跟随指针是原生行为——本仓不悄悄「修」,引擎逐行保真并由测试钉住;需要跟手的宿主走引擎新增的 updateRectangleTo 或桥协议的 resizeRectangle:true。fork 对上游的每一处偏离都必须是显式、可关闭、有文档的。
决策 5:18 项 jvmTest 交叉验证 + 行为保真测试
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;18 项 jvmTest 是「内核没漂」的闸门,其中三组专门钉上游原生行为(Rectangle d2 不动、Polygon 构造器闭合点、拖动固定 5 边)——保真测试钉的不只是「正确性」,还有「与上游逐位一致」。设备侧证据由 export 响应的 strokeList 提供:鸿蒙跑出的笔画 JSON 与 JVM 同操作序列逐字段对账。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :chitralekhan:jvmTest
18 项用例全部通过(failures=0, errors=0):
| 测试组 | 用例数 | 钉住什么 |
|---|---|---|
| FreeHand | 3 | 起笔单点、按列表替换追加、空栈 move 是 no-op |
| None | 1 | DrawMode.None 吞掉 startDrawing |
| Circle | 2 | 直径两点 → 半径/中点推导;起笔零半径 |
| Polygon | 3 | 起笔顶点塌在锚点 + 构造器闭合点;拖动固定 5 边;顶点圆公式 |
| Rectangle | 3 | 上游 d2 不跟随指针;updateRectangleTo 扩展位;对非矩形 no-op |
| 栈操作 | 4 | undo/redo 往返、空栈 no-op、clear 双清、setter 影响后续笔画 |
| 几何/缩放 | 2 | aspectRatio/isPortrait、缩放算术与多边形闭合点 +1 |
内核零依赖、零平台源码集,JVM 与 ohos 跑的就是同一份字节码语义——这正是「引擎没漂」的直接证据。
6.3 HAP 验证
:chitralekhan:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ChitraLekhanRender / ChitraLekhanVersion 均为全局 T 符号(双 ABI 验证)
BuildNativeWithCmake: finished(双 ABI 编出 libchitralekhan_napi.so)
napi_init.cpp 用 DevEco SDK clang 预编译 + 链接预验证通过(x86_64-linux-ohos)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
entry/libs 三件套:KN so arm64 约 3.7MB、x86_64 约 3.4MB(debugShared 未 strip,含 Kotlin/Native 运行时),libc++_shared.so 随 triple 配套拷入;NAPI so 由 CMake 现场编出(约 33KB)。
6.4 功能用例
- 启动后标题行出现
v0.0.5-ohos.1(来自真实 Native 调用chitraLekhanVersion(),非静态文字); - 手绘模式落笔拖动,实时层跟手绘制,松手后状态行刷新
笔画=N · mode=freehand · w=8 · #FF0000; - 圆模式从圆心拖到半径端点,生成直径两点定圆;多边形模式生成正多边形;矩形模式拖动跟手(走
resizeRectangle扩展位)(见 7.1 截图,9 笔画混合会话); - 颜色切换后新笔画用新色,色板选中描边高亮;笔宽切换后线宽即时变化(上游
thicknesses同一组值); - 撤销逐笔回退、重做按钮按
canRedo实时置灰/恢复、清空后双栈同清; - 勾选「导出到原图尺寸」后导出,PNG 尺寸为
imageWidth × imageHeight(800×600),笔画先经scaleStrokesToImageSize映射;不勾选则按画布显示尺寸直出;strokeList摘要随导出信息行显示。
6.5 验证结论
绘图状态引擎、几何计算、坐标系缩放、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的标注会话:版本握手、四种笔画模式、七色五档画笔、双栈撤销与导出(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/ChitraLekhan.git
cd ChitraLekhan
export JAVA_HOME="/path/to/jdk-21"
./gradlew :chitralekhan:compileKotlinOhosArm64 :chitralekhan:compileKotlinOhosX64 # klib
./gradlew :chitralekhan:jvmTest # 18 项交叉验证
./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.chitralekhan -m entry
hdc shell hilog | grep ChitraLekhan
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让
:chitralekhan真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游纯 Kotlin 文件带着
@Serializable注解,内核坚持零依赖就得剥掉——数据模型形状不变,JSON 序列化职责移交桥层(strokeList); scaleStrokesToImageSize上游用withContext(Dispatchers.Default)包装,剥协程时保持缩放算术逐行不动,只去掉 suspend 语义;- KN CAdapter 生成头不能手写替代:
napi_init.cpp需要ExportedSymbols符号表与DisposeString声明,手写只含业务函数的同名头会在 N-API 编译期报unknown type name 'libohoschitralekhan_ExportedSymbols'。处置:构建脚本挂copyKnApiHeader任务,把build/bin/<abi>/debugShared/libohoschitralekhan_api.h自动同步进cpp/include/,重链不过期; - ArkTS 组件成员不能与
CustomComponent基类属性方法撞名:画布换算系数取名scale直接编译失败(Property 'scale' in type 'Index' is not assignable to the same property in base type 'CustomComponent'),改名coordScale; - 画布尺寸不要取
ctx.width:它的单位在不同 API 版本有歧义,曾导致换算比例错误、预览与手指错位。处置:实时层直接用触摸原始坐标绘制(绝对跟手),换算系数改用组件级onAreaChange(单位确凿 vp)计算; CanvasRenderingContext2D的lineCap/lineJoin不存在CapStyle/JoinStyle枚举,用字符串'round'(已过审 SignaturePad Demo 同款);- ArkUI
Button默认水平内边距较大,固定 44vp 宽度装不下两位数文本(笔宽按钮 12/16/24 显示成省略号),加宽到 56vp 并显式收窄 padding; - 根构建的
.idea/*忽略规则盖不住嵌套目录——DevEco 打开example/harmonyApp后生成的.idea/.deveco/(含 clangd 数据库)与.clangd/.clang-tidy要单独追加 gitignore,否则混进提交; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();KN 异常不能直接抛过 FFI,统一收进ok:false的 JSONerror字段。
8.2 已知问题
- HarmonyOS 上没有 Compose Canvas:上游
ChitraLekhanCanvas(含双指缩放/平移的graphicsLayer手势)仍在composeMain;Demo 的None模式只落状态不跟手——平移缩放是 UI 层能力,等 CMP ohos 工具链到齐后随composeMain接回; - redo 后实时层不重放:
redo只动引擎栈,ArkUI 本地镜像不做重放(避免两端各造一份绘制状态机),导出以 KN 引擎为准;后续可把重放接到strokeList回读上; - 底图是占位图:
sample.png暂用图标副本,业务接入时替换资源并同步IMG_W/IMG_H;引擎不持有位图(上游持ImageBitmap只为宽高比),导出 PNG 也不含底图,与上游createBitmapFromStrokes的「白底笔画图」语义一致; - PNG 未压缩:zlib stored 块直出,短笔画几十 KB 可接受,大画布会变胖;
- android / ios / desktop / web 目标剥离(源文件留在树里),发布、Compose 插件一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.chitralekhan匹配的签名。
8.3 未来优化方向
- 把
redo的实时层重放接到strokeList回读上,让撤销/重做在 UI 层完全闭环; None模式补双指缩放/平移(PinchGesture+PanGesture),对齐上游transformable体验;- 底图合成导出:把底图与笔画图在 ArkTS 层叠放输出完整标注图,或下沉 KN 侧做 PNG 解码合成;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)验收矩阵与自动化截图脚本(uitest + capture)。
九、总结
9.1 核心难点回顾
上游 ChitraLekhan(全 Compose API)
→ 验证 Compose 制品缺失(org.jetbrains.compose 404)→ 定零依赖内核路线
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
→ 根构建替换(cmp_qrose 模式,app 模块留盘)
→ 内核提取(剥 @Serializable / 剥协程 / 引擎逐行重写)+ composeMain 隔离
→ 18 项 jvmTest 钉住上游语义(含三组上游原生行为保真测试)
→ ohosArm64 / ohosX64 KLIB + 双 ABI sharedLib
→ C ABI(@CName × 2)+ 有状态单例桥 + 距离场光栅化 PNG
→ C++ N-API(CAdapter 生成头 + DisposeString 纪律)
→ ArkTS JSON 解析 + ArkUI Canvas 实时层
→ ArkUI 真机单页验收(四模式 / 七色五档 / 双栈撤销 / 两种导出)
9.2 封装层次
- 引擎层:零依赖纯 Kotlin 内核,与上游 0.0.5 逐行对齐(四态笔画派生、双栈撤销、坐标缩放、几何工具);
- 隔离层:上游 Compose 源码
composeMain原样保留,恢复路径常在; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON,距离场光栅化按上游
DrawScope语义渲染 PNG; - N-API 层:完成参数检查、字符串转换和 Native 内存释放(CAdapter 生成头 +
copyKnApiHeader回填); - ArkTS 层:管理页面状态、实时绘制层(算法逐行镜像上游)、操作分发与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线选择必须基于当天的制品可达性:同样是 CMP 库,KStore 靠 kotlinx 制品线走「真编译整库」,ChitraLekhan 因 Compose 制品缺失走「零依赖内核 + composeMain 隔离」——两条路线在同一个社区并存,判断依据只有一条:目标依赖在社区 Maven 上是否存在;
- 保真测试钉的不只是正确性,还有「与上游逐位一致」:上游的
Rectangled2 不动、Polygon构造器闭合点这类原生行为必须显式测试钉住、显式 API 扩展,绝不能在适配时「顺手修」——否则交叉验证就失去了「内核没漂」的证明力; - 预览层与权威层分离要贯彻到底:ArkUI Canvas 只做跟手的实时层(触摸原始坐标直绘),几何权威全在 KN 引擎(图片坐标系);两套坐标的换算系数只从确凿来源(组件级
onAreaChange)取,任何单位歧义(如ctx.width)都会变成真机上的「位置不对」。
9.4 适配成果
当前适配已完成:
- ChitraLekhan
0.0.5零依赖绘图内核的双 ABI Kotlin/Native 编译(上游纯 Kotlin 文件只剥注解/协程,引擎逐行重写对齐,Compose 源码composeMain原样保留); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 全局 T 符号;DevEco SDK clang 预编译/预链接验证); - 距离场软件光栅化 + 零依赖 PNG 编码(贝塞尔细分、圆环、矩形边界、圆头圆角);
- DevEco Stage ArkUI 单页验收页面(四种笔画模式、七色五档画笔、双栈撤销、显示/原图两种导出、版本握手);
- 18 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 三件套)、签名、设备安装和真机验证;
- 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)