本文记录 Compose Multiplatform 画板库 DrawBox 接入 OpenHarmony 的完整过程,覆盖上游源码集盘点、composeMain 隔离、零依赖纯 Kotlin 核心提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、有状态 C ABI/N-API 桥接、ArkUI 双层渲染页面、HAP 构建和真机验收。

DrawBox 是一个完全用 Compose Multiplatform 编写的多平台画板库:给定一个 DrawController,DrawBox Composable 提供触摸绘制、撤销 / 重做 / 清空、纯色 / 图片背景、笔迹样式订阅与 getBitmap 离屏导出,内部用双栈(已画 / 已撤销)管理笔迹、用画布边长归一化坐标、收笔时按当时样式固化笔画。本次适配与 KStore 那类"整库直编"路线有本质区别:上游没有纯 Kotlin 的 commonMain——连 PathWrapper 里装的都是 Offset 和 Color,而社区 CMP 是 1.9.2、HarmonyOS Kotlin 是 2.2.21-1.0.0,上游是 Compose 1.5.10 / Kotlin 1.9.20,compose.runtime / compose.foundation 没有 ohos 制品。因此控制器状态机被逐条对照上游 1.3.1 语义提取为零依赖纯 Kotlin 核心(归一化协议、双栈撤销、样式固化时机一条不差),上游 Compose 源码 100% 原样保留在 composeMain 不参与编译,鸿蒙侧以 Kotlin/Native 光栅化 + @CName C ABI + CMake N-API 落地;又因为画板是交互式库而非"一次出图"的条码库,桥做成有状态单例,ArkUI Canvas 画实时层、每次触摸事件镜像进 Kotlin/Native 控制器,导出时由 KN 权威渲染。

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

上游仓库: MarkYav/DrawBox

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

DrawBox 为 Kotlin Multiplatform 应用提供"画板"这一件事的完整答案:DrawController 一行建控制器,insertNewPath / updateLatestPath / finalizePath 承接一次完整笔画,undo / redo / reset 全生命周期覆盖,undoCount / redoCount 让 UI 订阅撤销能力,open / background 提供背景图与背景色,getBitmap(size, subscription) 把当前画布离屏渲染成位图。渲染语义明确:先画中心裁剪的背景图,再按圆头圆角逐笔画路径;触摸像素坐标除以方形画布边长归一化到 [0..1],线宽同空间归一化。KMP 项目把这些能力放在 commonMain,由 Android、桌面共享。OpenHarmony 应用同样需要这套绘制能力(签名板、手写批注、涂鸦社交都是直接场景),但上游构建(Kotlin 1.9.20 / Compose 1.5.10 / Gradle 8.4 / AGP 8.1.4)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示画布,却无法证明公共控制器状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机核心、归一化协议、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 1.9.20 / Compose 1.5.10 / AGP 8.1.4 与鸿蒙工具链无法共存于一个 Gradle 构建;社区 CMP 是 1.9.2,compose.runtime / compose.foundation 没有 ohos 变体。
没有纯 Kotlin 核心上游全部 11 个源文件都 import androidx.compose.*,没有可直接编译的 commonMain——这是 DrawBox 与 qrose(编码器本来就是纯 Kotlin)最大的差别。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
绘制状态跨不了桥撤销双栈、进行中的一笔、StateFlow 订阅是 Kotlin 侧概念,C ABI 只能同步一来一回;且画板天然跨调用有状态,与 qrose 无状态桥不同。
交互式渲染不可过桥逐帧把画布过桥渲染成 PNG 撑不住触摸帧率,实时层必须在 ArkUI 侧。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:控制器状态机的零依赖提取与语义对照、根构建替换(cmp_qrose 模式)和有状态 C ABI/N-API 桥接层。渲染语义完全由"上游 getBitmap 渲染顺序的软件复现"负责,ArkTS 只负责实时笔迹、调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的绘制会话,对应桥接的全部入口:

  • 绘制(begin / move / end):触摸按下开新笔并清空重做栈,移动追加归一化采样点,抬手按当时颜色 / 透明度 / 线宽固化笔画;
  • 撤销 / 重做 / 清空:双栈搬移语义与上游一致;清空只清笔迹,不清背景图(只有 open 会重置);
  • 样式(style):颜色 #RRGGBB(AA)、笔宽、笔迹不透明度、画布层不透明度四个字段即时生效;
  • 背景(background):无 / 纯色(含 alpha)/ 图片(原始像素 + 宽高过桥);按上游 open 语义,设置图片背景先清空画布;
  • 导出(snapshot):任意尺寸方形 PNG,按上游 getBitmap 渲染顺序执行(透明底 → 可选背景 → 中心裁剪背景图 → 圆头圆角笔迹),undoCount / redoCount 实时回传。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文(Kotlin 异常不跨 FFI)。
connected / canvasSize控制器与方形画布的连接状态与边长。
undoCount / redoCount已画 / 已撤销栈深度,实时驱动按钮计数与高亮。
width / height / bytes / pngBase64仅 snapshot:导出尺寸、PNG 字节数与 Base64 内容。

核心侧完整能力(归一化、双栈撤销、样式固化时机、DynamicUpdate / FinishDrawingUpdate 两种订阅语义、中心裁剪几何)全部随编译进入 .so,逐条对照上游 1.3.1 实现,证明 ohos .so 里执行的绘制逻辑没有漂移。

1.3 实现适配

维度要求
代码复用状态机逐条对照上游 DrawController 提取为纯 Kotlin 核心(归一化、双栈、固化时机、方形约束一条不差);上游 Compose 源码 git mv 进 composeMain,内容零改动。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作对照验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/android、sample/desktop 留盘不进构建(引用的是 composeMain API)。
依赖纪律核心零外部依赖——连 kotlinx.coroutines 都不拉(官方 coroutines 没有 ohos klib),MutableStateFlow 用十几行的 DrawState 等价替代。
桥接稳定单一 C ABI + UTF-8 JSON,有状态单例(单 JS 线程串行调用),异常永远不跨 FFI。
UI 完整单页四 Tab 覆盖全部桥接入口:画板、样式、背景、导出,实时层与导出双层渲染。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。实时笔迹由 ArkUI Canvas 用与核心同源的中点平滑算法绘制,每次触摸事件同时镜像进 KN 控制器;导出走 KN 光栅化。composeMain 里的上游 Compose 源码等社区 compose.ui ohos 制品与 CMP 对齐后可接回,届时 KN 光栅化降级为导出后端。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点上游源码集与 Compose 依赖面、确定"核心提取 + composeMain 隔离"路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、零依赖核心编译
第 3 阶段:桥协议建模     ── 有状态单例控制器、10 个 action 的扁平 JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和 DisposeString
第 5 阶段:ArkUI 宿主     ── 双层渲染(ArkUI Canvas 实时层 + KN 导出层)、四 Tab、撤销计数联动
第 6 阶段:示例与验证     ── HAP、签名、真机与真机安装、全功能验收和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
drawbox/src/commonMain/               新增:零依赖纯 Kotlin 核心(io.github.markyav.drawbox.core,4 个文件)
drawbox/src/composeMain/              上游 11 个 Compose 源文件 git mv 而入(100% rename,不参与编译)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + Rasterizer + PngEncoder)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
sample/android、sample/desktop        上游示例,引用 composeMain API,留盘不进 settings
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :drawbox 与 :example:nativeApp,rootProject.name = "ohos_drawbox"。上游的 AGP classpath、Compose 插件、androidx 依赖、convention-publication 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作对照验证)
Gradle8.14.1(wrapper 从上游 8.4 升级;2.2.21 线的经验钉法)
外部依赖零(核心不引 coroutines / serialization / 任何三方库)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.drawbox、versionName 1.3.1-ohos.1
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
源码文件数依赖形态处置
上游 box/ + controller/ + model/ + util/11全部 import androidx.compose.*(Offset/Color/Path/ImageBitmap/Modifier)git mv 进 composeMain,内容零改动,不参与任何编译
新增 core/(DrawState / DrawBoxModel / Geometry / DrawController)4纯 Kotlin,零 expect、零外部依赖新增编译(jvm + 双 ohos)

这里与 KStore 的"整库直编"正好相反,原因是依赖矩阵而不是偏好:DrawController 连数据类型都是 Compose 的(PathWrapper.points: List<Offset>),compose.ui 没有 ohos 制品,expect/actual 补类型等于把 Compose 几何体系复制进库。所以本 fork 的答案是把状态机手洗成库自己的纯 Kotlin 类型——DrawPoint(x, y)(归一化坐标)、StrokePath(points, strokeWidth, strokeColor: Int ARGB, alpha)(与 PathWrapper 逐字段对应,points 与 strokeWidth 同处归一化空间)、DrawBoxBackground / BackgroundImage / OpenedImage(中心裁剪几何与上游 srcSize=min(w,h)、srcOffset 取整方式一致)。唯一一处类型降级:上游的 MutableStateFlow 在核心里换成十几行的 DrawState<T> 可读写值容器(官方 coroutines 没有 ohos klib),undoCount / redoCount 改为同步只读属性。

有两处对上游可见性的调整,性质与 qrose 新增 Qrose.encodeQr 相同——桥是 headless 消费者,不是同模块的 Composable:上游 internal 的五个指针事件入口(connectToDrawBox / insertNewPath / updateLatestPath / finalizePath / onTap)改为 public;新增只读属性 isConnected / canvasSize。

1.4 页面边界
标题区          DrawBox · HarmonyOS / 副标题(Compose Multiplatform 画板 · Kotlin/Native 桥 libdrawbox_napi.so)
Tab 切换        画板 / 样式 / 背景 / 导出
画板 Tab        撤销(计数) / 重做(计数) / 清空 按钮 + 320vp 方形画布(onReady 自动 connect)
样式 Tab        五色色板、笔宽滑条(1..50)、笔迹不透明度、画布层不透明度(标注仅实时层)
背景 Tab        无 / 蓝色 15% / 图标图片 三态
导出 Tab        尺寸 256/512/1024、"导出包含背景"开关、导出按钮、结果 Image、状态行
状态行          最近一次桥响应(如 "end ok · 连接=true 画布=320 撤销=12 重做=0")

页面启动即预载图标像素(背景图片能力的数据源),画布 onReady 自动发起 connect 握手。

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

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

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。DrawBox 核心提取后零外部依赖,没有 KStore 那种"coroutines 制品线反推 Kotlin 版本"的约束,直接落在社区最成熟的钉法上。Gradle 按 2.2.21 线的经验钉 8.14.1(上游 8.4 的 wrapper 不能驱动该线 Kotlin 插件)——工具链经验按线区分:2.4.20 线留 Gradle 9.x(KStore 先例),2.2.21 线降 8.14.1(本仓与 qrose 一致),两条线不能互相套用。

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_drawbox"

include(":drawbox")
include(":example:nativeApp")

版本目录收敛到只剩 KMP 插件:

[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译纯 Kotlin 核心

drawbox/build.gradle.kts 收敛为纯 KMP 模块(与 qrose 模块同款):

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()            // 对照验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}   // 零依赖
    }
}

gradle.properties 钉 JDK 21 并补 KN 三项(照 qrose):

org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=1.3.1-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true

实测:

./gradlew :drawbox:compileKotlinJvm                                        # BUILD SUCCESSFUL
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

核心的 4 个文件全部是新增,上游 11 个 Compose 文件一个字节没动——git status 里它们全部以 100% rename 呈现,这是"隔离而不篡改"的直接证据。

2.3 状态机提取的语义对照表(本适配的成败手)

提取不是重写,每一行都有上游对照物:

上游(Compose 版)核心(纯 Kotlin 版)语义锚点
point.div(size) 存入 PathWrapperDrawPoint.div(size) 存入 StrokePath触摸像素 ÷ 方形边长归一化,线宽同空间
insertNewPath 清空 canceledPaths同名方法同序开新笔清重做栈
undo / redo 双栈 removeLast + add逐行同构栈搬移顺序
reset() 清两栈,不清 openedImage同只有 open 先 reset
finalizePath 用当时 color/opacity/strokeWidth.div(size) 固化同样式快照时机
createPath 中点平滑(首段 lineTo 中点,之后 quadratic,末尾补 lineTo)flattenPath 摊平为稠密折线(供圆刷冲压)少于两点返回空 → 单点不渲染
connectToDrawBox 要求 width==height>0单 size 参数(方形边长)方形画布约束
getBitmap 渲染顺序Rasterizer.render 同序透明底 → 背景图(中心裁剪拉伸)→ 圆头圆角笔迹

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.markyav.drawbox.core/**(零依赖核心,原样编译)
        ↓ DrawController / 双栈撤销 / 归一化
Bridge.kt(@CName × 1,扁平 JSON 进出,有状态单例)
        ↓ UTF-8 JSON
N-API drawBoxRender()
        ↓
ArkUI Index.ets(四 Tab,双层渲染)

请求(全部扁平字段,一层):

{"action":"connect","size":320}                                  连接方形画布
{"action":"begin","x":40,"y":60} / move / end / tap              一次笔画
{"action":"undo"} / {"action":"redo"} / {"action":"reset"}
{"action":"style","color":"#EF4444","opacity":1,"strokeWidth":6,"canvasOpacity":1}
{"action":"background","type":"color","color":"#1D4ED8","alpha":0.15}
{"action":"background","type":"image","width":114,"height":114,"format":"rgba","pixels":"<base64>"}
{"action":"snapshot","size":512,"background":true}
响应 : {"ok":true,"action":"...","connected":true,"canvasSize":320,"undoCount":12,"redoCount":0}
       snapshot 额外 : "width":512,"height":512,"bytes":1049236,"pngBase64":"..."
       {"ok":false,"error":"..."}
3.2 有状态桥:与 qrose / KStore 的本质差别

条码(qrose)与存储(KStore)的桥都可以无状态:条码编码是纯函数,KStore 每次按路径现场开店。画板不行——"正在进行的一笔"和"两条撤销栈"就是库的状态本身,把状态搬到 ArkTS 等于把库重写一遍。所以桥内持有一个进程级全局 DrawController,跨调用维护。线程安全由 ArkTS 单 JS 线程的串行调用保证,桥内不加锁,这一约定写进了类注释。

snapshot 取 DrawBoxSubscription.DynamicUpdate 语义(含进行中的一笔);上游的另一种订阅 FinishDrawingUpdate 在核心的 getDrawPath 里同样保留,只是快照固定走前者。

3.3 错误通道

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

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

非法 action、未连接就 begin(上游 require 语义)、背景图片像素尺寸不符,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。响应拼接不允许尾逗号:ArkCompiler 的 JSON.parse 不接受(V8 新版能容忍),报 Unexpected Object Prop in JSON——这是本适配在真机上抓到的第一只虫,桥的拼接函数因此保证最后一个字段后无逗号。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libdrawbox_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdrawbox.so)
  │ DrawBoxRender
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出像素数组又会让 ArkTS 复制渲染语义,所以选择 C ABI + JSON,导出物是 PNG Base64。

4.2 Kotlin/Native 导出函数
@CName("DrawBoxRender")
fun drawBoxRender(request: String): String   // 10 个 action 一口进,有状态
C ABI 符号ArkTS 方法覆盖内核
DrawBoxRenderdrawBoxRender(req)connect / begin / move / end / tap / undo / redo / reset / style / background / snapshot

llvm-nm -D libohosdrawbox.so 验证 DrawBoxRender 为全局 T 符号;llvm-readelf -d 的 NEEDED 全部是 libc++_shared.so 等短名,无本机盘符。

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

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosdrawbox_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdrawbox_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。

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

add_library(drawbox_napi SHARED napi_init.cpp)
target_link_libraries(drawbox_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdrawbox)

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

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

KN so 由 example/nativeApp 的 linkDebugSharedOhosArm64/X64 产出,copyKnSo* 任务(finalizedBy)自动把 libohosdrawbox.so 与对应 triple 的 libc++_shared.so 回填进 entry/libs/<abi>/,不手工借 so。

第 5 阶段:ArkUI 宿主

5.1 双层渲染模型(交互式库的关键设计)

逐帧过桥渲染撑不住触摸帧率,Demo 把渲染拆成两层:

// 实时层:ArkUI Canvas,与核心同源的中点平滑算法
private paintStroke(stroke: LiveStroke): void {
  const pts = stroke.points;
  if (pts.length < 4) return;          // 单点 = 上游空 Path,不渲染
  ctx.beginPath();
  ctx.moveTo(pts[0], pts[1]);
  let ox = pts[0], oy = pts[1];
  for (let i = 2; i + 1 < pts.length; i += 2) {
    const mx = (ox + pts[i]) / 2, my = (oy + pts[i + 1]) / 2;
    if (i === 2) ctx.lineTo(mx, my); else ctx.quadraticCurveTo(ox, oy, mx, my);
    ox = pts[i]; oy = pts[i + 1];
  }
  ctx.lineTo(ox, oy);
  ctx.strokeStyle = stroke.color; ctx.lineWidth = stroke.width;
  ctx.globalAlpha = stroke.alpha; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
  ctx.stroke();
}

// 权威层:每次触摸镜像进 KN 控制器
onTouch(event) → Down: drawBoxRender({"action":"begin",...})
               Move:  drawBoxRender({"action":"move",...})
               Up/Cancel: drawBoxRender({"action":"end"})   // Cancel 也走 end,与上游 onDragCancel 一致

两层算法同源,导出与实时所见一致。撤销 / 重做 / 清空调桥后同步维护本地镜像栈并整体重绘;导出层调用 snapshot,KN 按权威状态光栅化,返回 Base64 经 Base64Helper → createImageSource → Image 显示。

5.2 页面交互

画板 Tab 自上而下:撤销 / 重做 / 清空按钮行(计数来自 KN 响应,可撤销时黑底白字高亮)、320vp 方形画布(背景层 + Canvas 实时层叠放)。样式 Tab 五色色板 + 三个滑条,对应 style 动作四字段,画布层不透明度明确标注"仅实时层,导出不应用"(与上游 getBitmap 行为一致)。背景 Tab 三态切换;选图标图片时把 resourceManager → createImageSource → readPixelsToBuffer 的原始像素 Base64 过桥,并按上游 open 语义同步清空本地镜像。导出 Tab 三档尺寸 + "导出包含背景"开关(关闭即严格的 getBitmap 透明底行为)。

5.3 对比度和错误边界

主操作深绿按钮白字,撤销 / 重做在可用时黑底白字、不可用时灰底,语义与可用性对齐;ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并打 hilog。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdrawbox)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName + 有状态单例控制器 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
│       ├── Rasterizer.kt                     上游 getBitmap 顺序的软件复现(圆刷冲压 + 软边抗锯齿)
│       └── PngEncoder.kt                     zlib stored + adler32,零依赖 PNG(色型 6 RGBA)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.drawbox、versionName 1.3.1-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosdrawbox_api.h
        ├── src/main/cpp/types/libdrawbox_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(四 Tab,双层渲染)
6.2 构建、签名和安装
./gradlew :drawbox:compileKotlinJvm                                        # 核心对照编译
./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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,product 不引用签名,勾选自动签名后由 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.drawbox -m entry

四、完整代码对照

4.1 整体架构

io.github.markyav.drawbox.core/**(新增零依赖核心)
    │ DrawController(双栈撤销 / 归一化 / 样式固化) / DrawPoint / StrokePath / flattenPath
drawbox/src/composeMain/**(上游 11 文件,100% rename 保留,不编译)
    │ 上游 DrawBox Composable / DrawController(StateFlow 版)/ PathWrapper
Bridge.kt + Rasterizer.kt + PngEncoder.kt
    │ DrawBoxRender(有状态 C ABI) / getBitmap 顺序复现 / 零依赖 PNG
libdrawbox_napi.so
    │ drawBoxRender
Index.ets
    │ 双层渲染 + JSON parse + @State 更新
    ▼
ArkUI 真机页面(四 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_drawbox、sample 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单插件钉版(AGP / Compose / androidx 全部剥离)
build.gradle.kts(根)版本 1.3.1-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21 钉版 + kotlin.native.* 三项
drawbox/build.gradle.kts纯 KMP:jvm() + ohosArm64 + ohosX64,commonMain 零依赖
drawbox/src/commonMain/**/core/*新增核心 4 文件(DrawState / 模型 / 几何 / 控制器)
drawbox/src/composeMain/**上游 11 文件 100% rename 保留
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.ktC ABI、有状态单例、MiniJson、错误通道(无尾逗号)
nativeApp/.../Rasterizer.ktgetBitmap 顺序复现、圆刷冲压、中心裁剪、source-over 混合
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鸿蒙适配主文档(15 节)

4.3 关键 API 对照

层次上游 API核心 / 桥作用
控制器DrawController() + 五个 internal 事件入口同名 public 方法一次笔画的完整生命周期
控制器undo / redo / reset / open同名方法双栈与背景图语义
控制器undoCount / redoCount(StateFlow)同名只读属性(栈深度)撤销能力订阅改为同步读
控制器getBitmap(size, subscription)Rasterizer.render(...)离屏导出(留在 nativeApp,不进库模块)
模型PathWrapper / DrawBoxBackground / OpenedImageStrokePath / 同名 sealed 类逐字段对应,Offset→DrawPoint、Color→ARGB Int
Native—DrawBoxRenderC ABI JSON 导出(有状态单例)
N-API—drawBoxRender(req)向 ArkTS 暴露能力
ArkTS—handleTouch / applyStyle / applyBackground / exportPng触发操作并刷新双层 UI

4.4 ArkTS 与 Kotlin 的边界

// 一次完整笔画(实时层同步绘制,事件镜像进 KN)
drawBoxRender(JSON.stringify({ action: 'begin', x: 40, y: 60 }));
drawBoxRender(JSON.stringify({ action: 'move', x: 90, y: 120 }));
drawBoxRender(JSON.stringify({ action: 'end' }));

// 导出:KN 按权威状态光栅化
const res = JSON.parse(drawBoxRender(JSON.stringify(
  { action: 'snapshot', size: 512, background: true })));
// res.pngBase64 → Base64Helper.decodeSync → createImageSource → PixelMap
// KN 侧:控制器状态机 + 光栅化
when (action) {
    "begin" -> controller.insertNewPath(point(json))   // 开新笔并清重做栈
    "move"  -> controller.updateLatestPath(point(json)) // 归一化追加采样点
    "end"   -> controller.finalizePath()                // 按当时样式固化
    "undo"  -> controller.undo()
    "snapshot" -> Rasterizer.render(size, controller.getDrawPath(DynamicUpdate),
                                    controller.openedImage.value,
                                    if (withBackground) controller.background.value else null)
}

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

五、关键决策说明

决策 1:手工提取零依赖核心,而不是硬编 Compose 或 expect/actual 补类型

KStore 整库直编的前提是"依赖的 kotlinx 制品线在 ohos 上齐了";DrawBox 的依赖是 compose.runtime + compose.foundation,compose.ui 没有 ohos 制品,这条路对它不成立。expect/actual 给 ohos 补一套 Offset / Color actual 等于把 Compose 几何与颜色体系复制进库,公开 API 语义也被迫分叉。所以答案是状态机手洗:库自己的纯 Kotlin 类型 + 逐条语义对照(见 2.3 表),上游 Compose 源码 100% 保留 composeMain 等工具链对齐后接回。

决策 2:零依赖纪律——连 kotlinx.coroutines 都不拉

官方 coroutines 没有 ohos klib,为 MutableStateFlow 拉一条不存在的依赖线是死路。桥对状态的消费是同步读写(.value 进、.value 出),DrawState<T> 十几行就覆盖;订阅式消费在鸿蒙侧的唯一用户是 Compose,而 Compose 本来就没编进来。核心因此 commonMain.dependencies {} 为空,KLIB 只有 stdlib。

决策 3:根构建替换(cmp_qrose 模式),sample 留盘

上游根构建(AGP / Compose 插件 / androidx 版本目录 / convention-publication 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/android 与 sample/desktop 引用 composeMain API,编译无从谈起,留盘不进 settings.gradle.kts——它们的存在方式只有这一种,与 KStore 对 kstore-storage 的处置同理。

决策 4:有状态桥 + ArkUI 实时层(交互式库的形状)

条码库的桥是无状态纯函数;画板的语义就是跨调用维护"进行中的一笔 + 双栈",所以桥持进程级单例控制器,单 JS 线程串行调用保证安全。实时笔迹不能逐帧过桥(全画布重算 + PNG 编码撑不住帧率),由 ArkUI Canvas 用同源算法本地绘制;每次触摸事件镜像进 KN,权威状态始终在 Kotlin 侧,导出与实时所见一致。多实例需求出现时再把 C ABI 句柄化,当前不为它设计。

决策 5:语义逐条对照就是交叉验证

上游没有测试文件,KStore 那种"51 项 desktopTest 跑上游原用例"的闸门在这里不存在。本仓的替代是 2.3 节那张语义对照表:归一化协议、双栈搬移、固化时机、单点不渲染、方形约束、getBitmap 渲染顺序,每条都在核心与 Rasterizer 里显式对应并写进文档——改坏任何一条就是回归,不是"鸿蒙特性"。jvm() 目标保留,就是为了让这张表可以被同一个源码集在 JVM 上反复验证。

六、测试与验证

6.1 测试环境

本次验证使用 Windows x64、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release(含 hvigor / ohpm / hdc)、DevEco 真机(x86_64)与鸿蒙真机(arm64)。

6.2 JVM 编译与语义对照

./gradlew :drawbox:compileKotlinJvm

jvm() 与 ohos 跑同一份核心源码集,编译全绿。语义层逐条对照上游 1.3.1(归一化、双栈、固化时机、reset 不清背景图、open 清笔迹、单点不渲染、方形约束、getBitmap 渲染顺序),验收底线是:同一操作序列在 Android/Desktop 上游实现与鸿蒙核心上,drawnPaths / canceledPaths 的演化完全一致。

6.3 HAP 验证

:drawbox:compileKotlinJvm: BUILD SUCCESSFUL
:example:nativeApp:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填三件套)
llvm-nm -D: DrawBoxRender 为全局 T 符号
llvm-readelf -d: NEEDED 全部短名,无本机盘符
BuildNativeWithCmake: finished(双 ABI 编出 libdrawbox_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.9 MB,内含双 ABI 各三件套(libdrawbox_napi.so / libohosdrawbox.so / libc++_shared.so);KN so arm64 约 3.8MB、x86_64 约 3.5MB(含 Kotlin/Native 运行时,零三方依赖让体积明显小于带 coroutines 的库)。CAdapter 生成代码保留那条已知 clang 警告(api.cpp:448),与社区记录一致,可忽略。

6.4 功能用例

  1. Canvas onReady 自动连接,状态行出现 已连接 320x320 画布(connect 握手);
  2. 触摸绘制多笔,实时层与状态行计数同步增长(begin/move×N/end,真机 12 笔后 撤销=12 重做=0);
  3. 点撤销,最后一笔消失,撤销=11 重做=1,按钮计数与高亮联动;点重做恢复;
  4. 样式 Tab 切色板 / 调笔宽 / 调不透明度,后续笔画即时生效;
  5. 背景 Tab 选"蓝色 15%",画布出现半透明纯色底,导出含同样背景;
  6. 背景 Tab 选"图标图片",画布按上游 open 语义清空笔迹并铺上中心裁剪的背景图;
  7. 导出 Tab 选 512 + 打开"导出包含背景",返回 导出成功 512x512 png=1049236B,PNG 内容 = 背景图 + 圆头笔迹,与画布所见一致;撤销后导出即少一笔;
  8. 非法操作(未连接就 begin、像素尺寸不符)返回 ok:false 与原文错误,应用不崩溃。

6.5 验证结论

绘制状态机、归一化协议、Native 动态库、C ABI、N-API、ArkUI 双层渲染页面和 HAP 已完成链路验证。应用在 DevEco 真机(x86_64)完成全功能验收后,在鸿蒙真机(arm64)上成功安装并进入前台:触摸绘制、撤销 / 重做、背景与导出闭环全部可用(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.3 命令速查

git clone https://atomgit.com/oh-tpc/DrawBox.git
cd DrawBox
export JAVA_HOME="/path/to/jdk-21"
./gradlew :drawbox:compileKotlinJvm                                            # 核心 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.drawbox -m entry
hdc shell hilog | grep DrawBoxNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP/CMP 适配,必须让状态机真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. ArkCompiler 的 JSON.parse 不接受对象尾逗号(报 Unexpected Object Prop in JSON,V8 新版能容忍)——手写 JSON 拼接的桥必须保证最后一个字段后无逗号,本仓第一版桥在真机上被抓到;
  4. 这个 SDK 上 PixelMap.readPixelsToBuffer 实际吐 RGBA 字节,与"默认 BGRA_8888"的常见认知相反,按 BGRA 交换会红蓝反转(导出图标蓝色变橙色);桥的 format 字段同时支持 rgba / bgra,Demo 按实测填 rgba;
  5. 对已卸载 Canvas 的 CanvasRenderingContext2D 调用会卡住 ArkUI 渲染管线(逻辑还在跑、画面停更)——Tab 切换卸载画布后 redraw() 必须加守卫,onReady 重挂载时补画;
  6. 命令行打 HAP 时 hvigor 从 PATH 找 java,JAVA_HOME 不管用,报 spawn java ENOENT——PATH 前置 DevEco 自带 JBR 的 bin;
  7. getMediaContent / encodeToString 只有 Promise 版,同步版是 getMediaContentSync / encodeToStringSync,按 Promise 版声明类型会编译报错;
  8. 自定义组件成员名不能撞 ArkUI 内置属性方法(opacity、onTouch 都是通用属性),否则报 Property 'x' in type 'Index' is not assignable to the same property in base type 'CustomComponent'——改名 strokeOpacity / handleTouch;
  9. 根 .gitignore 的 .idea/* 与 /build/ 是锚定模式,盖不住嵌套目录——DevEco 生成的 example/harmonyApp/.idea/(含 clangd 缓存库)要单独追加 gitignore,本仓首次提交前抓出并清理;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();CAdapter 生成代码的 api.cpp:448 clang 警告为社区已知,可忽略;
  12. 上游文件用 git mv 迁入 composeMain,让 git 以 100% rename 追踪——"隔离而不篡改"在版本史里可审计。

8.2 已知问题

  • canvasOpacity 只影响实时层,不进导出——这与上游一致(getBitmap 同样不吸收画布层 alpha),桥照常接收并存进控制器;
  • 背景alpha(ColourBackground.alpha / ImageBackground.alpha)只作用于 UI 合成层,导出路径按全透明度画;Demo 的"导出包含背景"开关让两种行为都可观察;
  • PNG 用 zlib stored 未压缩,512² 约 1 MB、1024² 约 4 MB;要瘦身只换 IDAT 内部实现,协议字段不动;
  • 桥为进程级单例控制器,一进程一画布;多画布需把 C ABI 句柄化(create/destroy + handle);
  • DrawBoxSubscription 的映射是语义级的:核心 getDrawPath 是同步快照,DynamicUpdate 含进行中的一笔、FinishDrawingUpdate 不含,快照固定取前者;
  • KN 光栅化是软件渲染的近似(圆刷冲压 + 软边抗锯齿),无渐变 Brush、无 PathEffect;上游本身只用实色描边,可见语义全覆盖;
  • 上游 android / desktop 示例未接入本工程(引用 composeMain API),文件保留但不在 settings.gradle.kts。

8.3 未来优化方向

  • 把 C ABI 句柄化,支持一进程多控制器(多画布 / 分屏场景);
  • PNG 换可压缩 IDAT 实现,压掉 stored 块的水分;
  • 社区 compose.ui ohos 制品与 CMP 对齐后,把 composeMain 接回编译,实时层换回 Compose DrawBox,KN 光栅化降级为导出后端;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充 1024 档导出的耗时 / 内存基线与自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 DrawBox 1.3.1(全部源码 Compose 化,无纯 Kotlin commonMain)
    → composeMain 隔离(git mv,100% rename,零改动)
    → 零依赖纯 Kotlin 核心提取(语义逐条对照,DrawState 替代 StateFlow)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(cmp_qrose 钉法)
    → ohosArm64 / ohosX64 KLIB + jvm() 对照
    → 有状态 C ABI(@CName × 1)+ 10 action JSON 契约
    → C++ N-API(DisposeString 纪律)
    → ArkTS 双层渲染(ArkUI Canvas 实时层 + KN 导出层)
    → ArkUI 真机四 Tab 验收(绘制 / 撤销 / 背景 / 导出)

9.2 封装层次

  • 核心层:纯 Kotlin DrawController(双栈撤销、归一化、样式固化)、flattenPath 中点平滑、零外部依赖;
  • 渲染层:Rasterizer 按上游 getBitmap 顺序软件复现(圆刷冲压、中心裁剪、source-over 混合)+ 零依赖 PNG 编码;
  • Native 层:生成双 ABI 动态库并通过单一有状态 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:双层渲染、页面状态、桥调用分发、错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线由依赖矩阵决定,不由偏好决定:同一个社区里,KStore"整库直编"和 DrawBox"核心提取"都是正确答案——前者依赖的 kotlinx 制品线齐了,后者站在 compose.ui 这个没有 ohos 制品的依赖上;动手前先盘上游每个 import 的可达性;
  2. 交互式库和无状态出图库的桥形状不同:条码 / 存储可以一口进一口出无状态,画板必须跨调用维护"进行中的一笔 + 撤销栈",有状态单例 + 实时层本地渲染 + 事件镜像是这类库的最小可行形状;
  3. 上游没有测试时,语义对照表就是交叉验证本身:把归一化、双栈、固化时机、渲染顺序逐条写成表、落进代码注释和文档,改坏任何一条都可审计;git mv 保留上游文件让"隔离而不篡改"在版本史里自证。

9.4 适配成果

当前适配已完成:

  • DrawBox 1.3.1(1.3.1-ohos.1)控制器状态机的双 ABI Kotlin/Native 编译(零依赖核心 4 文件新增,上游 11 文件 100% rename 保留);
  • 单一有状态 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号,llvm-readelf 验证依赖干净);
  • 上游 getBitmap 渲染顺序的 KN 软件复现(圆刷冲压 + 软边抗锯齿 + 零依赖 PNG);
  • DevEco Stage ArkUI 四 Tab 验收页面(绘制、撤销 / 重做 / 清空、样式、背景三态、三档导出,双层渲染);
  • HAP 构建(双 ABI 六件套 so,约 6.9MB)、真机全功能验收、签名、真机安装与真机绘制验证(12 笔连续绘制、撤销栈正确);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐