开源鸿蒙平台 CMP 三方库「DrawBox」适配全流程
本文记录 Compose Multiplatform 画板库
DrawBox接入 OpenHarmony 的完整过程,覆盖上游源码集盘点、composeMain隔离、零依赖纯 Kotlin 核心提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、有状态 C ABI/N-API 桥接、ArkUI 双层渲染页面、HAP 构建和真机验收。
DrawBox是一个完全用 Compose Multiplatform 编写的多平台画板库:给定一个DrawController,DrawBoxComposable 提供触摸绘制、撤销 / 重做 / 清空、纯色 / 图片背景、笔迹样式订阅与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 光栅化 +@CNameC 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.uiohos 制品与 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作对照验证) |
| Gradle | 8.14.1(wrapper 从上游 8.4 升级;2.2.21 线的经验钉法) |
| 外部依赖 | 零(核心不引 coroutines / serialization / 任何三方库) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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) 存入 PathWrapper | DrawPoint.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 方法 | 覆盖内核 |
|---|---|---|
DrawBoxRender | drawBoxRender(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.toml | Kotlin 2.2.21-1.0.0 单插件钉版(AGP / Compose / androidx 全部剥离) |
build.gradle.kts(根) | 版本 1.3.1-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 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.kt | C ABI、有状态单例、MiniJson、错误通道(无尾逗号) |
nativeApp/.../Rasterizer.kt | getBitmap 顺序复现、圆刷冲压、中心裁剪、source-over 混合 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(双层渲染) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(15 节) |
4.3 关键 API 对照
| 层次 | 上游 API | 核心 / 桥 | 作用 |
|---|---|---|---|
| 控制器 | DrawController() + 五个 internal 事件入口 | 同名 public 方法 | 一次笔画的完整生命周期 |
| 控制器 | undo / redo / reset / open | 同名方法 | 双栈与背景图语义 |
| 控制器 | undoCount / redoCount(StateFlow) | 同名只读属性(栈深度) | 撤销能力订阅改为同步读 |
| 控制器 | getBitmap(size, subscription) | Rasterizer.render(...) | 离屏导出(留在 nativeApp,不进库模块) |
| 模型 | PathWrapper / DrawBoxBackground / OpenedImage | StrokePath / 同名 sealed 类 | 逐字段对应,Offset→DrawPoint、Color→ARGB Int |
| Native | — | DrawBoxRender | C 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 功能用例
- Canvas
onReady自动连接,状态行出现已连接 320x320 画布(connect握手); - 触摸绘制多笔,实时层与状态行计数同步增长(
begin/move×N/end,真机 12 笔后撤销=12 重做=0); - 点撤销,最后一笔消失,
撤销=11 重做=1,按钮计数与高亮联动;点重做恢复; - 样式 Tab 切色板 / 调笔宽 / 调不透明度,后续笔画即时生效;
- 背景 Tab 选"蓝色 15%",画布出现半透明纯色底,导出含同样背景;
- 背景 Tab 选"图标图片",画布按上游
open语义清空笔迹并铺上中心裁剪的背景图; - 导出 Tab 选 512 + 打开"导出包含背景",返回
导出成功 512x512 png=1049236B,PNG 内容 = 背景图 + 圆头笔迹,与画布所见一致;撤销后导出即少一笔; - 非法操作(未连接就
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP/CMP 适配,必须让状态机真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- ArkCompiler 的
JSON.parse不接受对象尾逗号(报Unexpected Object Prop in JSON,V8 新版能容忍)——手写 JSON 拼接的桥必须保证最后一个字段后无逗号,本仓第一版桥在真机上被抓到; - 这个 SDK 上
PixelMap.readPixelsToBuffer实际吐 RGBA 字节,与"默认 BGRA_8888"的常见认知相反,按 BGRA 交换会红蓝反转(导出图标蓝色变橙色);桥的format字段同时支持rgba/bgra,Demo 按实测填rgba; - 对已卸载 Canvas 的
CanvasRenderingContext2D调用会卡住 ArkUI 渲染管线(逻辑还在跑、画面停更)——Tab 切换卸载画布后redraw()必须加守卫,onReady重挂载时补画; - 命令行打 HAP 时 hvigor 从 PATH 找
java,JAVA_HOME不管用,报spawn java ENOENT——PATH 前置 DevEco 自带 JBR 的bin; getMediaContent/encodeToString只有 Promise 版,同步版是getMediaContentSync/encodeToStringSync,按 Promise 版声明类型会编译报错;- 自定义组件成员名不能撞 ArkUI 内置属性方法(
opacity、onTouch都是通用属性),否则报Property 'x' in type 'Index' is not assignable to the same property in base type 'CustomComponent'——改名strokeOpacity/handleTouch; - 根
.gitignore的.idea/*与/build/是锚定模式,盖不住嵌套目录——DevEco 生成的example/harmonyApp/.idea/(含 clangd 缓存库)要单独追加 gitignore,本仓首次提交前抓出并清理; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();CAdapter 生成代码的api.cpp:448clang 警告为社区已知,可忽略; - 上游文件用
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.uiohos 制品与 CMP 对齐后,把composeMain接回编译,实时层换回 ComposeDrawBox,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 三条经验
- 适配路线由依赖矩阵决定,不由偏好决定:同一个社区里,KStore"整库直编"和 DrawBox"核心提取"都是正确答案——前者依赖的 kotlinx 制品线齐了,后者站在
compose.ui这个没有 ohos 制品的依赖上;动手前先盘上游每个 import 的可达性; - 交互式库和无状态出图库的桥形状不同:条码 / 存储可以一口进一口出无状态,画板必须跨调用维护"进行中的一笔 + 撤销栈",有状态单例 + 实时层本地渲染 + 事件镜像是这类库的最小可行形状;
- 上游没有测试时,语义对照表就是交叉验证本身:把归一化、双栈、固化时机、渲染顺序逐条写成表、落进代码注释和文档,改坏任何一条都可审计;
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。
参考文档
更多推荐

所有评论(0)