本文记录 DotKit 接入 OpenHarmony 的完整过程,覆盖像素绘图库盘点、鸿蒙工具链钉版、真实绘图引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、有状态 C ABI/N-API 桥接、ArkUI 像素编辑器页面、HAP 构建、ArkTS 严格模式踩坑和真机验收。

DotKit 是基于 Kotlin Multiplatform 的像素绘图库:不可变状态 DotKitState、Layer / LayerManager 图层系统(逐层透明度、可见 / 锁定、z-order、alpha 合成)、HistoryManager + CanvasCommand 命令模式 Undo/Redo(≤50 步)、六种工具(Brush / Line / Shape / Eraser / Fill / Eyedropper),上游再以 Compose Multiplatform UI(DotKitCanvas / DotKitController / ZoomPanHandler)交付 Android / Desktop / iOS / Wasm。本次适配与社区早期路线(qrose 的"无状态桥、一次请求一张图")有一处关键区别:绘图是交互式语义,一笔跨越 down / move / up 三次调用,所以桥必须做成有状态单例(同 cmp_drawbox 先例)——DotKitState、HistoryManager、当前 Tool 与笔画进行中的 ToolState 全部活在 Kotlin/Native 侧,ArkUI Canvas 只负责实时显示,KN 侧是权威状态持有者与导出位图生产者。而与 cmp_drawbox 相比,DotKit 又幸运得多:它的引擎天生就在零依赖的 dotkit-core 纯 Kotlin 模块里,不需要"手洗"内核,core 算法一行未动。

项目地址: AtomGit/oh-tpc/DotKit.git

上游仓库: kez-lab/DotKit

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

DotKit 上游把"像素编辑器"这件事拆成了两层:dotkit-core 是纯 Kotlin 引擎——DotKitState.create(w, h) 建状态,Tool.onDown / onMove / onUp 三段式驱动笔画,onUp 产出 CanvasCommand 进 HistoryManager 历史栈,LayerManager.composite() 按图层顺序做 source-over alpha 合成;dotkit-compose 是 Compose UI——DotKitCanvas 按「合成 → 画像素 → 叠加工具预览(supportsPreview)→ 网格线(gridVisible && zoom>=4)」四步管线渲染,DotKitController 提供命令式 API,ZoomPanHandler 提供视口手势。KMP 项目把这些能力放在 commonMain,由 Android、Desktop、iOS、Wasm 共享。OpenHarmony 应用同样需要这套绘图能力,但上游构建(Kotlin 2.2.21 / Gradle 9.2 / Compose Multiplatform 1.9.3)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画方格,却无法证明公共绘图引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证绘图引擎、图层合成、历史栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Gradle 9.2 wrapper 与 HarmonyOS Kotlin 2.2.21-1.0.0 插件线不兼容(该线按 Gradle 8.x 验证),wrapper 必须降级。
依赖矩阵上游 dotkit-core 声明了 kotlinx-coroutines-core,Maven Central 没有 ohos 变体(处置见 2.4)。
Compose UI 隔离dotkit-compose 全部站在 compose.runtime / compose.foundation / compose.material3 / compose.ui 语义面上,社区 CMP 1.9.x 工具链对不齐,整模块不能编进 ohos。
交互语义跨不了无状态桥一笔画跨 down / move / up 三次调用,ToolState、历史栈必须在调用之间存活——qrose 式无状态桥会把编辑器降级成导出器。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:真实引擎的 ohos 编译、根构建工具链替换(cmp_qrose 模式)、有状态 C ABI/N-API 桥接层。绘图语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解码 PNG 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的像素编辑会话,对应桥接的全部入口(17 个 action 分九组):

  • 画布(init / resize / clear):DotKitState.create 建默认状态 + 第一层,历史清零;改尺寸保留主副色;清空当前活动图层;
  • 工具(tool):brush / eraser / line / rect / circle / fill / eyedropper 七值,笔刷 1…16,rect/circle 吃 fillMode(fill / stroke);
  • 指针(down / move / up):三段式笔画,move 阶段响应带工具预览叠加的合成图(对应上游 supportsPreview 的橡皮筋手感),up 产出 CanvasCommand 落入历史栈;
  • 历史(undo / redo):HistoryManager 语义,响应带 canUndo / canRedo;
  • 颜色(primary / secondary / swap + 取色回 picked):颜色统一 #RRGGBBAA 字符串,取色只回 picked 是否回填由用户决定(与上游 EyedropperTool 语义一致);
  • 图层(layer 八类子操作):add / remove / select / opacity / visible / lock / move / duplicate,图层 id 由 core 生成,ArkTS 只从响应 layers[] 回读;
  • 导入导出(load / save / snapshot):load 从转义字符串恢复完整状态,save 序列化当前状态(响应带 stateJson),snapshot 出 PNG。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
action / width / height回显本次操作与画布尺寸。
pngBase64 / bytes当前合成结果的 PNG(zlib stored、含工具预览叠加)与字节数。
canUndo / canRedo / tool / toolSize / grid历史与工具状态回显。
primary / secondary / picked主副色与取色结果(picked 仅取色动作返回)。
layers[]图层全量快照:id / name / active / visible / locked / opacity。

引擎侧完整能力(图层 alpha 合成、命令模式历史栈、六工具算法)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧 desktop 目标与 ohos 双目标编同一份代码,证明 ohos .so 里执行的绘图逻辑没有漂移。

1.3 实现适配

维度要求
代码复用dotkit-core 的全部 commonMain 文件原样编译,不提取、不重写;仅新增 ohosMain 的 ImageExporter actual(零依赖 PNG 编码器,core 唯一平台差异点)。
平台目标dotkit-core 声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证;库模块不出 sharedLib,example/nativeApp 出双 ABI 动态库。
构建隔离工具链替换(eazytec Maven 置前 + Kotlin 2.2.21-1.0.0 + Gradle 8.14.1),dotkit-compose 与 sample 整模块不参与 ohos 编译——效果等价 qrose 的 composeMain 隔离,只是粒度是整个模块,Android / Desktop / iOS / Wasm 构建保持上游原样。
桥接稳定有状态单例引擎 + 1 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律core 收敛为零第三方依赖(移除无 ohos 变体且源码零引用的 coroutines),上游源码零改动。
UI 完整单页覆盖全部 17 个桥接入口:画布区、七工具、笔刷滑条、调色板、历史、图层面板、网格开关、PNG 导出与 JSON 往返。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的 DotKitCanvas / DotKitController 未编进 ohos(等社区 compose.ui ohos 制品与 CMP 工具链对齐后接回),ArkTS Demo 用 ArkUI Canvas 把桥暴露的编辑器语义画出来——KN 管权威状态,ArkUI 管实时显示,两端职责不重叠。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 core 零 Compose 依赖、确定交付物
第 2 阶段:目标与依赖打通 ── eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1 降级、coroutines 处置
第 3 阶段:桥协议建模     ── 17 个 action 的 JSON 契约、有状态引擎、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 像素编辑器页面、PixelMap 解码、指针节流、ArkTS 严格模式适配
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
dotkit-core/                          上游纯 Kotlin 引擎(进 ohos 构建:desktop + ohosArm64 + ohosX64)
dotkit-core/src/ohosMain/             新增:ImageExporter actual(零依赖 PNG 编码器,core 唯一新增源码集)
dotkit-compose/                       上游 Compose UI,整模块不编 ohos(源码原样保留)
sample/                               上游 Compose 示例(Android/Desktop/iOS/Wasm 构建不变)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + Engine + Rasterizer + PngEncoder + StateJson + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkUI 像素编辑器页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份,CN 是主文档)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式:settings.gradle.kts include 原有三模块并追加 :example:nativeApp,eazytec 双仓写在 pluginManagement 与 dependencyResolutionManagement 最前。上游根构建的插件别名(compose-multiplatform、kotlin-android、hot-reload、serialization、buildConfig)全部保留——因为 dotkit-compose 与 sample 还要按上游方式继续构建四个平台,这一点与"根构建整体替换"的 KStore 不同,DotKit 的根构建是加法而不是替换。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle8.14.1(上游 wrapper 9.2.0 降级,原因见 2.1)
Compose / AGPCompose Multiplatform 1.9.3 + AGP 8.12.3(非 ohos 模块沿用上游,实测在 HarmonyOS Kotlin 下编译通过)
版本1.0.0-ohos.1(上游 main 快照未发版)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.dotkit
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 形态依赖处置
dotkit-core状态 / 图层 / 六工具 / 历史,全部纯 Kotlin 类零第三方(见 2.4),零 UI、零 AGP整体编译(desktop + 双 ohos),源码零改动
dotkit-composeDotKitCanvas / DotKitController / ZoomPanHandlercompose.runtime / foundation / material3 / ui整模块不编 ohos,源码原样保留
sampleCompose 示例 appcore + compose不编 ohos,四平台构建不变

core 的 expect 声明只有 1 处:expect object ImageExporter(exportPNG + exportDataURL),上游给了 android / desktop / ios / wasmJs 四个 actual。ohos 补第五个——零依赖 PNG 编码器(RGBA color type 6、zlib stored 块、标准 CRC32,不链系统 zlib),这是整个 fork 在 core 里唯一的新增源码:

// dotkit-core/src/ohosMain/kotlin/io/github/kez/dotkit/export/ImageExporter.ohos.kt
actual object ImageExporter {
    actual fun exportPNG(pixels: IntArray, width: Int, height: Int): ByteArray = /* 零依赖 PNG 编码 */
    actual fun exportDataURL(pixels: IntArray, width: Int, height: Int): String = /* Base64 Data URL */
}

逐文件排查 @Immutable / @Stable / @Js* / @Jvm* 这类 Native 认不全的注解,dotkit-core 的排查结果为零,源码一行没动——上游引擎和 UI 分层干净,不需要像 qrose 那样把 Painter 从 commonMain 挪去 composeMain,也不需要像 cmp_drawbox 那样先手洗一层引擎。

1.4 页面边界
标题区          DotKit / KMP 像素绘图库 · HarmonyOS NAPI Demo
状态行          最近一次操作结果(如"ok brush 32x32 png=4196B undo=1/0")
画布区          ArkUI Canvas:透明棋盘格 + KN 回传合成位图 + 网格线(cell 滑条 6..24)
工具区          画笔 / 橡皮 / 直线 / 矩形 / 圆 / 填充 / 取色 七按钮 + 笔刷滑条 1..16 + fillMode 切换
颜色区          主副色色块 + 交换 + 24 色调色板 + 副色模式开关 + 取色回显
历史区          撤销 / 重做按钮(按 canUndo / canRedo 灰置)
画布操作        新建 16/32/64、网格开关、清空图层、快照、导出 PNG、尺寸调整
图层管理        半模态面板:激活 / 可见 / 锁定 / 透明度 / 上移下移 / 复制 / 删除 / 新建
JSON 区         DotKitState 文本域 + 加载 JSON / 导出 JSON(save ↔ load 往返)

页面启动时 aboutToAppear 立即发 init(32×32 默认画布),状态行随即出现第一条真实响应。

第 2 阶段:目标与依赖打通

2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

社区此前批准的 CMP 库钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze / cmp_drawbox 同款)。KStore 那类存储库升到 2.4.20-0.1.0-07 是被 kotlinx 制品线的 stdlib 元数据逼的;DotKit 相反——core 收敛为零第三方依赖后没有任何制品逼版本,而 dotkit-compose / sample 还要继续用 Compose Multiplatform 1.9.3(与 Kotlin 2.2.21 同代配对),所以工具链钉在社区基线 2.2.21-1.0.0。

代价是 Gradle:这条插件线按 Gradle 8.x 验证,上游 wrapper 的 9.2.0 在 ohos 目标配置期不稳,wrapper 降到 8.14.1(AGP 8.12.3 要求 ≥8.13,兼容)。同时关闭 configuration cache——KN link 任务和 copyKnSo 这类 finalizer 在 cache 下复用有问题,org.gradle.java.home(Temurin 21)写进 gradle.properties 钉死打包 JDK。

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")
        google { /* 上游内容过滤保留 */ }
        gradlePluginPortal()
        mavenCentral()
    }
}

rootProject.name = "DotKit"

include(":dotkit-core")
include(":dotkit-compose")
include(":sample")
include(":example:nativeApp")
2.2 编译真实引擎

dotkit-core/build.gradle.kts 在上游基础上只做加法(该模块的 iosMain 是手工 creating 风格,ohosMain 按同样风格补,不套 default hierarchy template):

kotlin {
    // ...上游的 android / jvm("desktop") / ios* / wasmJs 原样保留...
    ohosArm64()
    ohosX64()

    sourceSets {
        // ...上游 sourceSets 原样保留...
        val ohosArm64Main by getting
        val ohosX64Main by getting
        val ohosMain by creating {
            dependsOn(commonMain.get())
            ohosArm64Main.dependsOn(this)
            ohosX64Main.dependsOn(this)
        }
    }
}

注意库模块不开 binaries.sharedLib——sharedLib 只放 example/nativeApp,否则每个模块都吐一份带运行时的 so,HAP 体积和符号都会乱。实测:

./gradlew :dotkit-core:compileKotlinDesktop                                  # JVM 交叉验证基线
./gradlew :dotkit-core:compileKotlinOhosArm64 :dotkit-core:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :dotkit-compose:compileKotlinDesktop :sample:compileKotlinDesktop  # Compose 模块在 HarmonyOS Kotlin 下照常编译

引擎全部 commonMain 文件一个没动,只有 ohosMain 的 PNG actual 是新的。上游 dotkit-core 没有自带单元测试(desktopTest 是空任务),"内核没漂"的验证靠 JVM / ohos 双目标编译加桌面 sample 与鸿蒙 Demo 的同序列操作对比(见 6.2)。

2.3 依赖处置:coroutines 没有 ohos 制品

上游 dotkit-core 的 commonMain 声明了 kotlinx-coroutines-core。Maven Central 的官方版本没有 ohosArm64 / ohosX64 变体,加上 ohos 目标后依赖解析直接失败。逐文件 grep 确认 core 源码对 coroutines 零引用(它只是上游 README 宣称的"唯一依赖",实际代码没用),处置是从 commonMain 移除该依赖——core 收敛为零第三方依赖,上游源码一行没改。若业务侧确需协程,应换 eazytec 仓库的 ohos 适配线(KStore 用的 1.11.0-0.1.0-07 那条),不要混 Central 官方版。

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.kez.dotkit/**(上游引擎,原样编译)
        ↓ DotKitState / LayerManager / HistoryManager / Tool 三段式
Engine.kt(进程级单例)+ Bridge.kt(@CName,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API dotKitRender()
        ↓
ArkUI Index.ets(像素编辑器单页)

请求与响应(扁平 JSON,一层):

请求 : {"action":"init","width":32,"height":32}
       {"action":"tool","name":"brush","size":3,"fillMode":"fill"}
       {"action":"down","x":10,"y":20} / {"action":"move",...} / {"action":"up",...}
       {"action":"undo"} / {"action":"redo"} / {"action":"swap"} / {"action":"clear"} / {"action":"grid"}
       {"action":"primary","color":"#RRGGBBAA"} / {"action":"secondary",...}
       {"action":"layer","op":"opacity","id":"...","value":0.5}
       {"action":"load","json":"<转义的 DotKitState JSON>"} / {"action":"save"} / {"action":"snapshot"}
响应 : {"ok":true,"action":"...","width":32,"height":32,"bytes":4196,"pngBase64":"...",
        "canUndo":true,"canRedo":false,"tool":"brush","toolSize":3,"grid":true,
        "primary":"#...","secondary":"#...","picked":"#...","layers":[...]}
       {"ok":false,"error":"..."}

除 init / load 外,每个成功响应都带当前完整渲染与全量状态快照——ArkTS 侧不需要自己维护任何业务状态,单向信任桥的回包即可。

3.2 为什么必须有状态单例(本桥与 qrose 桥的本质区别)

qrose 是"一次请求、一张 PNG",无状态桥够用。DotKit 是交互式编辑:一笔画跨越 down / move / up 三次调用,笔画进行中的 ToolState(起点、当前点、受影响像素)、历史栈、当前工具与颜色都必须在调用之间存活。若每次调用都传全量状态,JSON 会膨胀,undo 语义也做不对——历史栈在谁手里,撤销就只能在谁那做。所以引擎放在 KN 侧做进程级单例:

/** One global engine per process; the ArkTS demo serializes every call,
 *  so the engine state is safe without locks. */
internal object Engine {
    var state: DotKitState? = null
    val history = HistoryManager()
    var tool: Tool = BrushTool(1)
    var toolState: ToolState? = null
}

线程安全靠 ArkTS 单 JS 线程天然串行,KN 引擎不加锁;move 是高频事件,节流放在 ArkTS 侧(见 5.1)。

3.3 错误通道

导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("DotKitRender")
fun dotKitRender(request: String): String = try {
    /* action 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、未 init 就画、未知 action、图层 id 不存在,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。工具的像素写入自带 isInBounds 守卫,桥再把指针坐标 coerce 到画布内——ShapeTool 的 FILL 模式会在边界检查前物化整个点列表,远界坐标不 clamp 会巨量分配。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libdotkit_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdotkit.so)
  │ DotKitRender
  ▼
引擎(状态 + 历史 + 工具)→ 光栅化 → PNG → Base64 → DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制绘图语义,所以选择 C ABI + JSON。光栅化把 state.composite() 的 ARGB 结果与工具预览像素(getPreviewPixels,0.7 alpha SrcOver 叠加,对齐上游 DotKitCanvas 管线第 3 步)编成 PNG,透明像素保持 alpha=0,棋盘格由 ArkTS 垫。

4.2 Kotlin/Native 导出函数
@CName("DotKitRender")
fun dotKitRender(request: String): String   // 17 个 action 一口进,有状态
C ABI 符号ArkTS 方法覆盖内核
DotKitRenderdotKitRender(req)DotKitState.create + 六工具三段式 + HistoryManager + LayerManager 八类子操作 + ImageExporter PNG + StateJson 往返

一个符号覆盖全部入口——协议按 action 字段分发,以后加能力只加 action 分支,C 签名永远是 const char* -> const char*,ArkTS 侧 dotKitRender(string): string 永远不用改。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"dotKitRender", nullptr, NapiDotKitRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosdotkit_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。napi_get_value_string_utf8 的缓冲区在该 API 版本标成非 const,先量长度再开 std::vector<char>、拷完再构造 std::string。libohosdotkit_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。

三名一致性是"能安装、一点就崩"类故障的总闸:CMake target dotkit_napi = nm_modname "dotkit_napi" = ArkTS import 'libdotkit_napi.so',napi_property_descriptor 的 utf8name = index.d.ts 导出名 dotKitRender = C 符号 DotKitRender。

4.4 CMake 和类型声明
add_library(ohosdotkit SHARED IMPORTED)
set_target_properties(ohosdotkit PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(dotkit_napi SHARED napi_init.cpp)
target_link_libraries(dotkit_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdotkit)

OHOS_ARCH 为空或 libohosdotkit.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const dotKitRender: (request: string) => string;

KN so 的产出与回填由 Gradle 负责:example/nativeApp 出双 ABI sharedLib(baseName=ohosdotkit),cxxShared 辅助函数在 ~/.konan/dependencies 下按 triple 定位 libc++_shared.so,linkDebugSharedOhosArm64/X64 经 copyKnSo* 任务把三件套自动拷进 entry/libs/<abi>/,不手工借 so。

第 5 阶段:ArkUI 宿主

5.1 统一调用封装与节流
private call(req: Record<string, Object>): Promise<Result> {
  this.busy = true;
  // 同步调桥 → JSON.parse → Base64 解码 → createImageSource → createPixelMap
  // → 旧 PixelMap release() → @State 更新 → Canvas 重绘 → flushPending()
}

三个交互细节:其一,move 高频,busy 标志位在上一帧未落地时把最新坐标存进 pendingMove(合并采样),帧落地后 flushPending 补发,up 会丢弃过期 move;其二,PixelMap 每次换新前 release() 旧的,滚动/连画场景不释放会涨内存;其三,取色动作的响应带 picked 时,Demo 再发一次 primary 把颜色落回 KN 侧,保证两端一致。

5.2 页面交互

画布区自下而上三层:透明棋盘格(灰白相间,随 cell 重算)→ Canvas.drawImage 画 KN 回传的合成位图 → 网格线(仅 grid=true 时画 1px 线,cell 6…24 由滑条控制)。工具区七个按钮 + 笔刷滑条 + fillMode 切换;颜色区主副色块、交换按钮、24 色调色板与副色模式开关;图层管理用半模态面板展示响应 layers[] 全量快照,操作后逐个调桥刷新。KN 的 composite() 是权威:层透明度只在 KN 合成时乘一次,ArkUI 直接画合成位图不再叠加,避免双重混合。

5.3 ArkTS 严格模式适配(真机编译实战)

第一轮 DevEco 编译在 Index.ets 上撞出一组 ArkTS 严格模式报错,全部有明确改法,记录如下给后来者:

报错根因改法
'fs' is not exported from Kit '@kit.CoreFileKit'Kit 导出名不是 fsimport { fileIo as fs } from '@kit.CoreFileKit'
Cannot find name 'RenderingSettings' + 'LengthMetricsUnit' only refers to a type这两个类型名在该 SDK 不是全局名用全局声明的 RenderingContextSettings + 单参构造 new CanvasRenderingContext2D(settings)(cmp_drawbox 同款)
Property 'width'/'height' in type 'Index' is not assignable...@State width / @State height 与 CustomComponent 基类的 .width() / .height() 通用属性方法撞名状态变量改名 gridW / gridH
Conversion of type 'LayerInfo' to type 'Record<string, Object>' may be a mistakeArkTS 禁止接口 → Record 直接转换放弃 cast,直接读已类型化的字段构造对象字面量
Use explicit types instead of "any", "unknown"根因是上面的 fs 导入失败,fs.openSync 被解析成 any导入修复后显式标注 const file: fs.File
ArkTS:WARN 'getContext' has been deprecated旧 APIthis.getUIContext().getHostContext(),取不到时抛错走 catch 提示

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdotkit)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/
│       ├── Bridge.kt                         @CName + parseColor + 协议注释(继承 cmp_qrose 审核模式)
│       ├── Engine.kt                         有状态单例:state / history / tool / toolState 与 action 分发
│       ├── Rasterizer.kt                     composite + 预览叠加 → RasterImage
│       ├── PngEncoder.kt                     零依赖 PNG(RGBA + zlib stored)
│       ├── StateJson.kt                      DotKitState ↔ JSON(load/save 的生产端)
│       └── MiniJson.kt                       扁平 JSON 解析(继承自 cmp_qrose)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.dotkit、versionName 1.0.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/libohosdotkit_api.h
        ├── src/main/cpp/types/libdotkit_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 像素编辑器验收页面
6.2 构建、签名和安装
./gradlew :dotkit-core:compileKotlinDesktop                                      # JVM 交叉验证
./gradlew :dotkit-core:compileKotlinOhosArm64 :dotkit-core:compileKotlinOhosX64  # KLIB
./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 的 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.dotkit -m entry

四、完整代码对照

4.1 整体架构

io.github.kez.dotkit/**(上游引擎原样编译)
    │ DotKitState / Layer / LayerManager / HistoryManager / 六工具
dotkit-core/src/ohosMain/**(唯一新增源码集)
    │ ImageExporter actual(零依赖 PNG)
Engine.kt + Bridge.kt + Rasterizer.kt + PngEncoder.kt + StateJson.kt + MiniJson.kt
    │ DotKitRender(@CName,有状态)
libdotkit_napi.so
    │ dotKitRender
Index.ets
    │ JSON parse + PixelMap 解码 + @State 更新
    ▼
ArkUI 真机像素编辑器(棋盘格 + 合成位图 + 网格线)

4.2 文件清单

文件职责
settings.gradle.ktseazytec Maven 置前、追加 :example:nativeApp,上游三模块保留
gradle.propertiesorg.gradle.java.home(Temurin 21)、关 configuration cache、version=1.0.0-ohos.1、KN 发行版走 Maven
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0,其余版本(Compose 1.9.3 / AGP 8.12.3)不动
gradle/wrapperGradle 9.2.0 → 8.14.1
dotkit-core/build.gradle.kts加 ohosArm64() / ohosX64() + 手工 ohosMain 源集;commonMain 移除 coroutines
dotkit-core/src/ohosMain/新增 ImageExporter actual(零依赖 PNG)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Engine.kt + Bridge.kt有状态单例、@CName("DotKitRender")、17 action 分发、错误通道
nativeApp/.../Rasterizer.kt + PngEncoder.ktcomposite + 预览叠加 → 零依赖 PNG
nativeApp/.../StateJson.ktDotKitState ↔ JSON(load / save,补齐上游 README 宣称但未实现的转换器)
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鸿蒙适配主文档(18 节)

4.3 关键 API 对照

层次上游 API桥 action作用
引擎DotKitState.create(w, h)init建默认状态 + 第一层,历史清零
引擎Tool.onDown / onMove / onUpdown / move / upToolState 由引擎单例持有,三段式跨调用
引擎HistoryManager.undo / redo(≤50 步)undo / redocanUndo / canRedo 随响应回传
引擎LayerManager 八类操作layer 的 op 子操作图层 id 由 core 生成,ArkTS 只回读
引擎setPrimaryColor / setSecondaryColor / swapColorsprimary / secondary / swap颜色统一 #RRGGBBAA
引擎EyedropperTooltool=eyedropper + up → picked只拾色不自动改主色,与上游语义一致
导出ImageExporter.exportPNGsnapshot → pngBase64KN 零依赖 PNG(zlib stored)
导出(上游 README 宣称的 DotKitJsonConverter,源码未实现)load / save桥内 StateJson 补齐同一能力
渲染DotKitCanvas 管线 1–3 步KN 光栅化合成 + 预览叠加在 KN,语义对齐 JVM
渲染DotKitCanvas 管线第 4 步(网格)ArkUI Canvas 网格线视图层概念,不进导出 PNG
视口ZoomPanHandler(0.1x–32x)ArkTS cell 缩放视口不上桥,zoom / pan 留在 Compose 侧

4.4 ArkTS 与 Kotlin 的边界

const call = (req: object) => JSON.parse(dotKitRender(JSON.stringify(req)));
call({ action: 'init', width: 32, height: 32 });
call({ action: 'tool', name: 'brush', size: 2 });
call({ action: 'down', x: 3, y: 4 });
call({ action: 'move', x: 8, y: 9 });
const res = call({ action: 'up', x: 10, y: 12 });
// res.pngBase64 是当前合成结果;res.canUndo / res.layers 一并回传
@CName("DotKitRender")
fun dotKitRender(request: String): String = try {
    val json = MiniJson.parse(request)
    when (json.str("action")) {
        "init"  -> engine.init(json.int("width", 32), json.int("height", 32))
        "down"  -> engine.down(json.int("x", 0), json.int("y", 0))
        "move"  -> engine.move(json.int("x", 0), json.int("y", 0))
        "up"    -> engine.up(json.int("x", 0), json.int("y", 0))   // onUp → CanvasCommand → history
        "undo"  -> engine.undo()
        /* ...17 个 action... */
    }
} catch (t: Throwable) { /* {"ok":false,"error":"..."} */ }

两者之间只传输扁平 JSON 字符串与 Base64 PNG,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译纯 Kotlin core,Compose UI 整模块隔离

dotkit-core 天生就是引擎与 UI 分层的纯 Kotlin 模块,正确答案就是"加目标 + 补一个 PNG actual",core 算法一行不动—— flood fill、Bresenham、alpha 合成、命令模式历史栈全部是上游原件。dotkit-compose 相反:它站在整个 Compose 语义面上,社区 CMP 1.9.x 工具链对不齐,整模块不编 ohos(效果等价 qrose 的 composeMain 隔离,粒度是整模块),源码原样保留等工具链对齐后接回。两条处理互为表里:能复用的全部复用,编不了的干净隔离。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 降 8.14.1,与 KStore 的 2.4.20 线并存

工具链版本跟着依赖走:KStore 被 kotlinx ohos 制品线逼到 2.4.20-0.1.0-07;DotKit 的 core 收敛为零第三方依赖后没有制品逼版本,而 dotkit-compose / sample 要继续用 Compose Multiplatform 1.9.3(Kotlin 2.2.21 同代),所以钉社区 CMP 库基线 2.2.21-1.0.0。这条线按 Gradle 8.x 验证,上游 wrapper 9.2.0 降到 8.14.1(AGP 8.12.3 要求 ≥8.13,兼容)——两条工具链线的 Gradle 经验不能互相套用(2.4.20 线可留 9.x,2.2.21 线必须 8.14.1)。

决策 3:有状态桥,而不是 qrose 式无状态桥

绘图是交互语义:down / move / up 之间有 ToolState,笔画之间有历史栈,图层之间有 z-order。无状态桥一次一图,等价于把编辑器降级成导出器;历史栈在谁手里,撤销就只能在谁那做。引擎做 KN 侧进程级单例,ArkTS 只发增量指令,线程安全靠单 JS 线程串行(cmp_drawbox 同款契约)。

决策 4:渲染分工——KN 权威状态与导出位图,ArkUI 只画实时层

每次桥调用都返回全量合成 PNG(32×32 约 4 KB 量级,未压缩可接受),ArkUI Canvas 按棋盘格 → 位图 → 网格线三层绘制。KN 的 composite() 是唯一合成权威,ArkTS 不再叠加透明度;网格是 zoom>=4 的视图层行为,由 ArkUI 画在最上层,导出的 snapshot 永远不带网格——导出语义和上游一致。实时帧率靠 move 合并节流保住,不做流式增量渲染。

决策 5:StateJson 补齐上游缺失的 JSON 转换器

上游 README 宣称的 DotKitJsonConverter 在源码里并不存在(文档过度承诺)。桥内自带零依赖 StateJson(图层像素 Base64 编码,扁平 MiniJson 读不了的嵌套结构走转义字符串进桥),save 是它的生产端、load 是消费端——「AI 生成像素画 → JSON → 进编辑器继续改」这条上游卖点上声明的链路,在鸿蒙侧是闭环可用的。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin,org.gradle.java.home 钉版)、DevEco Studio 5.1.0 Release 与鸿蒙真机(华为 Mate 90 Pro,arm64)。

6.2 JVM 交叉验证

./gradlew :dotkit-core:compileKotlinDesktop
./gradlew :dotkit-compose:compileKotlinDesktop :sample:compileKotlinDesktop

三项全部通过。说明两点:其一,同一份 commonMain 在 JVM 与 ohos 各编一次,是"core 没漂"的编译层基线;其二,dotkit-compose / sample 在 HarmonyOS Kotlin 2.2.21-1.0.0 下照常编译,证明工具链替换没有伤及上游四平台构建。上游 core 没有自带单元测试(desktopTest 是空任务),行为级交叉验证用「同一操作序列在桌面 sample 与鸿蒙 Demo 各跑一遍、对比合成结果」补位。

6.3 HAP 验证

:dotkit-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64: arm64-v8a/libohosdotkit.so(4.35MB,含 KN 运行时)+ libc++_shared.so
linkDebugSharedOhosX64:   x86_64/libohosdotkit.so(3.89MB)+ libc++_shared.so
llvm-nm -D: DotKitRender 为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libdotkit_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

每个 ABI 目录三件套缺一不可:libdotkit_napi.so(CMake 产物)、libohosdotkit.so(KN 运行时在内)、libc++_shared.so(与 KN so 同 triple,copyKnSo 一起拷)。

6.4 功能用例(真机)

  1. 启动即 init:状态行出现 ok brush 32x32 …,画布区渲染棋盘格 + 网格线;
  2. 画笔连拖数笔:状态行 ok brush 32x32 png=4196B undo=1/0——32×32 画布未压缩 PNG 约 4 KB,与预期量级一致;
  3. 七工具逐个切换:line / rect / circle 有橡皮筋预览(move 阶段响应即带预览叠加),eraser 擦出透明(棋盘格露出),fill 点封闭区域整块变色,eyedropper 点笔迹回 picked;
  4. 撤销 / 重做:连画三笔 undo 三次回到空画布,canUndo / canRedo 与按钮灰置一致;
  5. 图层:新建两层、非活动层透明度拉到 0.5 看合成变透、关可见整层消失、锁定后落笔被拒、上移下移看遮挡关系、复制出拷贝;
  6. 取色回填:picked 确认后回填主色再画一笔,颜色一致;
  7. 导出 PNG:Base64 解码落沙箱 files/export/,是合法 PNG、尺寸等于画布尺寸、不带网格线;
  8. JSON 往返:save 拿 stateJson → 修改几笔 → load 粘回,画布、图层、颜色全部恢复。

6.5 验证结论

绘图引擎、图层合成、历史栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的像素编辑会话:三段式笔画、工具预览、撤销重做、图层合成、取色、PNG 导出与 JSON 往返(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/DotKit.git
cd DotKit
export JAVA_HOME="/path/to/jdk-21"
./gradlew :dotkit-core:compileKotlinDesktop                                          # JVM 交叉验证
./gradlew :dotkit-core:compileKotlinOhosArm64 :dotkit-core:compileKotlinOhosX64      # KLIB
./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.dotkit -m entry
hdc shell hilog | grep DotKit

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :dotkit-core 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 Gradle 9.2 wrapper 与 HarmonyOS Kotlin 2.2.21-1.0.0 插件线不兼容,wrapper 降到 8.14.1——工具链线的 Gradle 经验不能跨线套用(KStore 的 2.4.20 线可留 9.x);
  4. configuration cache 会让 KN link 与 copyKnSo 这类 finalizer 复用出问题,直接关掉;
  5. kotlinx-coroutines-core 在 Maven Central 没有 ohos 变体:core 源码零引用,直接从 commonMain 移除;业务侧确需协程时换 eazytec 仓库的 ohos 适配线,不要混官方版;
  6. 上游 README 宣称的 DotKitJsonConverter 在源码里并不存在——依赖上游文档先验证源码,桥内自带 StateJson 补齐;
  7. 上游 core 没有单元测试,desktopTest 是空任务:别指望"测试全绿"当验收,行为交叉验证要靠双端同序列操作对比;
  8. ArkTS 严格模式六连:fs 要用 fileIo 导入、Canvas 上下文用全局 RenderingContextSettings 单参构造、@State 不得叫 width / height(与 CustomComponent 撞名)、接口不能 cast 成 Record、导入失败会连锁产生 no-any-unknown、getContext 换 getUIContext().getHostContext()(见 5.3);
  9. HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS:必须 CMake NAPI 包装,nm_modname / ArkTS import 名 / index.d.ts 三个名字任何一个写错,表现都是"能安装、一动笔就崩";
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();napi_get_value_string_utf8 的缓冲区先量长度再开 std::vector<char>;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. ShapeTool 的 FILL 模式会在边界检查前物化整个点列表:桥对指针坐标先 coerce 到画布内,防止远界坐标巨量分配。

8.2 已知问题

  • 鸿蒙侧没有 Compose UI:DotKitCanvas / DotKitController / ZoomPanHandler 未编进 ohos,等社区 compose.ui ohos 制品与 CMP 工具链对齐后接回,ArkTS Demo 降级为 fallback;
  • 交互帧率受"每次调用全量合成 + PNG + Base64"带宽限制,move 已合并节流;更大画布或更高频采样需要增量渲染协议;
  • zoom / pan 视口变换(0.1x–32x)不上桥,是视图层概念,鸿蒙侧由 cell 缩放承担;
  • PNG 未压缩(zlib stored):16/32/64 三档画布 4 KB 到十几 KB 可接受,超大画布会变胖,真要瘦身只换 IDAT 内部实现,协议字段不动;
  • 上游 sample 的 iOS / Wasm 目标本仓未做运行验证(构建脚本未动,不在本仓验证范围);
  • CAdapter 生成代码仍有 clang 警告(api.cpp non-void),.so 可正常链接,等 HarmonyOS Kotlin 修生成器;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.dotkit 匹配的签名。

8.3 未来优化方向

  • 增量渲染:move 阶段只回脏区矩形或小图,up 后回全图,突破全量 PNG 的帧率天花板;
  • 补 ZoomPanHandler 等价物:把视口手势采样传给 KN 统一管理,或保持视图层实现并写进集成文档;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)验收矩阵和自动化截图脚本(uitest + dumpLayout)。

九、总结

9.1 核心难点回顾

上游 dotkit-core(纯 Kotlin 引擎)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1 降级,configuration cache 关闭)
    → ohosArm64 / ohosX64 KLIB + 唯一 ohosMain PNG actual
    → 有状态 C ABI(@CName × 1,17 action)+ 扁平 JSON
    → C++ N-API(DisposeString 纪律,三名一致性)
    → ArkTS JSON 解析 + PixelMap 解码 + 指针节流 + 严格模式适配
    → ArkUI 真机像素编辑器验收(笔画 / 图层 / 历史 / 取色 / 导出 / JSON 往返)

9.2 封装层次

  • 引擎层:上游原版 DotKitState、LayerManager alpha 合成、HistoryManager 命令模式、六工具;
  • 导出层:上游 expect/actual 体系补 ohos actual(零依赖 PNG + Data URL);
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON 与 PNG;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、桥调用封装、PixelMap 解码、节流、图层面板与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先盘点 core 的"纯净度"再定适配路线:dotkit-core 零 Compose 依赖、零第三方依赖,适配面就是"加目标 + 补 actual + 写桥";同样是绘图库,cmp_drawbox 没有现成 core 要先手洗,DotKit 四步走完——路线选择必须基于当天的源码依赖度盘点,而不是品类直觉;
  2. 工具链版本跟着依赖走,Gradle 经验跟着工具链线走:core 零第三方依赖时钉社区 CMP 基线 2.2.21-1.0.0(保住 Compose 1.9.3 四平台构建),依赖被 ohos 制品线逼版时才升 2.4.20-0.1.0-07(KStore 路线);2.2.21 线 Gradle 必须降 8.14.1,2.4.20 线可留 9.x;
  3. 交互式库的桥必须保住交互语义:有状态单例 + 三段式指针协议 + 全量状态回包,ArkTS 侧零业务状态;无状态桥能出图,但出不了编辑器——撤销、预览、图层这些语义都在调用间隙里,桥不接住它们就永远丢了。

9.4 适配成果

当前适配已完成:

  • DotKit 1.0.0-ohos.1(上游 main 快照)真实引擎的双 ABI Kotlin/Native 编译(commonMain 零重写,仅 ohosMain 一个 PNG actual);
  • dotkit-core 的 ohosArm64 / ohosX64 KLIB 与 JVM 交叉验证编译全绿,dotkit-compose / sample 四平台构建保持上游原样;
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号,17 个桥接 action 全覆盖);
  • DevEco Stage ArkUI 像素编辑器验收页面(七工具三段式笔画、工具预览、撤销重做、图层八类操作、取色、调色板、网格、PNG 导出、JSON 往返);
  • HAP 构建(双 ABI 六件套 so)、签名、设备安装和华为 Mate 90 Pro 真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐