本文记录 ChitraLekhan 接入 OpenHarmony 的完整过程,覆盖图片标注库盘点、Compose 制品线缺失下的路线选型、零依赖绘图内核提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

ChitraLekhan 是基于 Compose Multiplatform + Canvas API 的图片标注库:给定一张底图,它提供自由手绘 / 圆 / 正多边形 / 矩形四种笔画、撤销 / 重做 / 清空三栈操作、画笔颜色与粗细配置、平移缩放浏览,以及把显示坐标系笔画映射回原图坐标系的 scaleStrokesToImageSize。本次适配与 KStore 那类「真编译整库」路线有本质区别:上游的全部公开 API 都是 Compose API(ChitraLekhanCanvas Composable、mutableStateListOf 状态、DrawScope 绘制扩展),而社区 Maven 上没有 ohos 的 Compose 制品,因此按社区已过审的 qrose / Vico / Reorderable / DrawBox / SignaturePad 同一条路处理——把绘图状态引擎做成零依赖纯 Kotlin 核心编进 ohos,Compose UI 原样隔离到 composeMain 不参与编译,鸿蒙侧用 Kotlin/Native 光栅化 + @CName C 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 已从上游 8.13 换成 8.14.1,2.2.21 工具链线要求 Gradle 8.x)
DevEco product5.1.0(18) 兼容,hvigor modelVersion 5.1.0
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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.ktandroidx.compose.runtime State + ui.graphics按语义重写为零依赖 ChitraLekhanEngine
ChitraLekhanCanvas.kt / rememberChitraLekhan.ktComposable + 指针输入 + graphicsLayergit mv 进 composeMain
components/(Toolbar / ColorPicker / DrawModeSelector / DrawScope 扩展)Compose UIgit mv 进 composeMain
utils/GraphicUtils.kt(Path.drawQuadraticBezier)androidx.compose.uigit mv 进 composeMain
utils/ComposeUtils.kt(色板与 thicknesses)androidx.compose.ui.graphics.Colorgit mv 进 composeMain;色板/笔宽值在 Demo 侧逐值复刻
androidMain/.../GraphicsUtils.kt(Android Bitmap 导出)android.graphics + Composegit mv 进 composeMain(改名 GraphicsUtils.android.kt)
composeResources/drawable/(工具栏图标)Compose resourcesgit mv 进 composeMain

expect 声明全库 0 处、协程依赖剥除后为 0、第三方依赖为 0——内核是纯标准库 Kotlin,这是它与 KStore(真编译整库 + 8 行 actual)路线差异的根源。

1.4 引擎语义的逐行对齐

ChitraLekhanEngine 与上游 ChitraLekhan 类逐行对应,三处上游原生行为被显式钉住:

  1. updateDrawing 的 Rectangle 分支用存储的对角点重建——矩形拖动时第二对角点不跟随指针(上游 0.0.5 原样行为)。鸿蒙内核不改它,由 rectangleUpdateKeepsSecondDiagonalFixedLikeUpstream 测试钉住;需要矩形跟手的宿主走引擎新增的显式扩展 updateRectangleTo(offset)(fork API,与 qrose 新增 Qrose.encodeQr 同一性质);
  2. DrawingStroke.Polygon 的构造器 init 块把首点追加到尾部作闭合点——副作用是每缩放一次多边形点数 +1,由 scaledPolygonGrowsByOneClosingPointLikeUpstream 钉住;
  3. 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 方法覆盖内核
ChitraLekhanRenderchitraLekhanRender(req)connect / mode / color / width / start / move / undo / redo / clear / export
ChitraLekhanVersionchitraLekhanVersion()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.tomlKotlin 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.kt2 个 C ABI、有状态单例、MiniJson、strokeList 序列化、错误通道
nativeApp/.../Rasterizer.kt距离场光栅化(贝塞尔细分 / 圆环 / 矩形边界、圆头圆角)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(实时层 + 栈操作 + 两种导出)
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)显示坐标 → 原图坐标映射(上游同名函数,剥协程包装)
NativeChitraLekhanRender / ChitraLekhanVersionC ABI JSON 导出
N-APIchitraLekhanRender(req) / chitraLekhanVersion()向 ArkTS 暴露能力
ArkTScallBridge(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):

测试组用例数钉住什么
FreeHand3起笔单点、按列表替换追加、空栈 move 是 no-op
None1DrawMode.None 吞掉 startDrawing
Circle2直径两点 → 半径/中点推导;起笔零半径
Polygon3起笔顶点塌在锚点 + 构造器闭合点;拖动固定 5 边;顶点圆公式
Rectangle3上游 d2 不跟随指针;updateRectangleTo 扩展位;对非矩形 no-op
栈操作4undo/redo 往返、空栈 no-op、clear 双清、setter 影响后续笔画
几何/缩放2aspectRatio/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 功能用例

  1. 启动后标题行出现 v0.0.5-ohos.1(来自真实 Native 调用 chitraLekhanVersion(),非静态文字);
  2. 手绘模式落笔拖动,实时层跟手绘制,松手后状态行刷新 笔画=N · mode=freehand · w=8 · #FF0000;
  3. 圆模式从圆心拖到半径端点,生成直径两点定圆;多边形模式生成正多边形;矩形模式拖动跟手(走 resizeRectangle 扩展位)(见 7.1 截图,9 笔画混合会话);
  4. 颜色切换后新笔画用新色,色板选中描边高亮;笔宽切换后线宽即时变化(上游 thicknesses 同一组值);
  5. 撤销逐笔回退、重做按钮按 canRedo 实时置灰/恢复、清空后双栈同清;
  6. 勾选「导出到原图尺寸」后导出,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 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让 :chitralekhan 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游纯 Kotlin 文件带着 @Serializable 注解,内核坚持零依赖就得剥掉——数据模型形状不变,JSON 序列化职责移交桥层(strokeList);
  4. scaleStrokesToImageSize 上游用 withContext(Dispatchers.Default) 包装,剥协程时保持缩放算术逐行不动,只去掉 suspend 语义;
  5. KN CAdapter 生成头不能手写替代:napi_init.cpp 需要 ExportedSymbols 符号表与 DisposeString 声明,手写只含业务函数的同名头会在 N-API 编译期报 unknown type name 'libohoschitralekhan_ExportedSymbols'。处置:构建脚本挂 copyKnApiHeader 任务,把 build/bin/<abi>/debugShared/libohoschitralekhan_api.h 自动同步进 cpp/include/,重链不过期;
  6. ArkTS 组件成员不能与 CustomComponent 基类属性方法撞名:画布换算系数取名 scale 直接编译失败(Property 'scale' in type 'Index' is not assignable to the same property in base type 'CustomComponent'),改名 coordScale;
  7. 画布尺寸不要取 ctx.width:它的单位在不同 API 版本有歧义,曾导致换算比例错误、预览与手指错位。处置:实时层直接用触摸原始坐标绘制(绝对跟手),换算系数改用组件级 onAreaChange(单位确凿 vp)计算;
  8. CanvasRenderingContext2D 的 lineCap/lineJoin 不存在 CapStyle/JoinStyle 枚举,用字符串 'round'(已过审 SignaturePad Demo 同款);
  9. ArkUI Button 默认水平内边距较大,固定 44vp 宽度装不下两位数文本(笔宽按钮 12/16/24 显示成省略号),加宽到 56vp 并显式收窄 padding;
  10. 根构建的 .idea/* 忽略规则盖不住嵌套目录——DevEco 打开 example/harmonyApp 后生成的 .idea/.deveco/(含 clangd 数据库)与 .clangd/.clang-tidy 要单独追加 gitignore,否则混进提交;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();KN 异常不能直接抛过 FFI,统一收进 ok:false 的 JSON error 字段。

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 三条经验

  1. 路线选择必须基于当天的制品可达性:同样是 CMP 库,KStore 靠 kotlinx 制品线走「真编译整库」,ChitraLekhan 因 Compose 制品缺失走「零依赖内核 + composeMain 隔离」——两条路线在同一个社区并存,判断依据只有一条:目标依赖在社区 Maven 上是否存在;
  2. 保真测试钉的不只是正确性,还有「与上游逐位一致」:上游的 Rectangle d2 不动、Polygon 构造器闭合点这类原生行为必须显式测试钉住、显式 API 扩展,绝不能在适配时「顺手修」——否则交叉验证就失去了「内核没漂」的证明力;
  3. 预览层与权威层分离要贯彻到底: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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐