开源鸿蒙平台 CMP 三方库「multiplot」适配全流程
本文记录
multiplot接入 OpenHarmony 的完整过程,覆盖图表库能力盘点、Compose 依赖隔离决策、纯 Kotlin 布局引擎抽取、ohosArm64/ohosX64目标、Kotlin/Native 软件光栅化、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
multiplot是一个 theme-agnostic 的 Compose Multiplatform 柱状图 / 折线图库:整个 API 围绕ChartDataset/ChartSeries建模,加第二条、第三条、第十条系列不需要换代码路径;柱状图支持分组(GROUPED,并排对比)与堆叠(STACKED,看总量与构成)两种模式,折线图支持贝塞尔平滑、渐变面积填充、白芯数据点与末点强调;所有颜色来自ChartStyle,库内部不读MaterialTheme。本次适配与 KStore 那批"制品线齐全、真编译整库"的路线有本质区别:multiplot是纯 UI 库,渲染的 100% 站在 ComposeCanvas/TextMeasurer/animateFloatAsState上(runtime / foundation / ui / material3 / animation-core 五件套),上游 Kotlin 2.3.10 / CMP 1.11.1 与社区鸿蒙 CMP 1.9.2 / Kotlin 2.2.21 对不齐,Compose 无法被编进 ohos。因此走 cmp_qrose 审核通过的"模型复用、渲染重写"路线:数据模型与校验、好看刻度数学、色板配色留在commonMain,几何计算(柱槽位、堆叠分段、贝塞尔控制点、标签抽稀)从 Canvas 代码里抽出来做成纯 Kotlin 的ChartLayout布局引擎并整体编译成 OpenHarmony KLIB;上游 6 个 Compose 渲染文件原样放进src/composeMain不参与编译;鸿蒙侧用 Kotlin/Native 软件光栅化 + PNG +@CNameC ABI 出图,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/multiplot.git
上游仓库: Jeremy-Gitau/multiplot
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
multiplot 为 Kotlin Multiplatform 应用提供"数据 → 图表"这一件事的完整答案:ChartDataset(categories, series) 一行建模,每条 ChartSeries 带 id / label / values / 可选覆盖色,构造期校验"每系列每类别一个值",派生 maxValue 与 maxStackedValue;ChartStyle 承载全部视觉旋钮——四套内置色板(Kotlin / Vibrant / Pastel / Monochrome)、ColorAssignment.ByIndex / ByStableHash 两种配色策略、网格 / 标签 / 背景三色、圆角、Y 轴步数、K/M 数值缩写格式化器。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套图表能力,但上游构建(Kotlin 2.3.10 / Gradle 9.3.0 / AGP 8.13.2)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮和图片,却无法证明图表几何引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证模型与布局引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.10 / Gradle 9.3.0 与鸿蒙工具链无法共存于一个 Gradle 构建;鸿蒙线钉 Kotlin 2.2.21-1.0.0,该线只支持 Gradle 8.x,wrapper 必须从 9.3.0 降到 8.14.1。 |
| 依赖矩阵 | 渲染依赖 compose.runtime / foundation / ui / material3 / animation-core 五件套,社区鸿蒙 CMP 只有 1.9.2(配 Kotlin 2.2.21),与上游 CMP 1.11.1(配 Kotlin 2.3.10)跨两代,元数据互读不了。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 渲染语义跨不了桥 | Canvas 画布回调、TextMeasurer 文本测量、animateFloatAsState 入场动画都是 Compose 运行时概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:模型与几何计算的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。图表的每一笔怎么画,由从上游 Canvas 代码里逐条溯源出来的 ChartLayout 布局引擎负责;鸿蒙侧只负责把布局图元光栅化成 PNG,ArkTS 只负责调用 Native、解码图片和展示结果。
1.2 库提供的能力
Demo 提供一个真实的图表渲染会话,对应桥接的全部入口:
- 分组柱状(GROUPED):每个类别下各系列并排成柱,柱宽 = 组内容宽 / 系列数 − 间隙,四角圆角 4dp,高度按
value / niceMax(maxValue)归一; - 堆叠柱状(STACKED):每类别一根整柱,各系列按序自底向上堆叠,高度和等于
maxStackedValue归一值; - 折线图(line):贝塞尔平滑(mid-x 控制点,每段 24 段细分)、垂直渐变面积填充(多系列 alpha 0.14 / 单系列 0.28)、白芯数据点(末点放大强调)、X 轴标签自动抽稀;
- 调色板 ×4:Kotlin / Vibrant / Pastel / Monochrome,与上游
MultiplotPalettes色值逐字一致; - 数据集 ×2:Weekly status(Mon–Sun × Completed / Active / Cancelled 三系列)与 Monthly split(Jan–Jun × North / South 两系列),系列数任意增减,配色与图例自动跟随;
- 图例开关:图例行占位随之增减,图表主体位置不变。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 渲染是否成功,失败时 error 带原文。 |
kind | 回显图表类型(bar / line)。 |
width / height | PNG 像素尺寸。 |
bytes | PNG 字节数。 |
pngBase64 | 图表 PNG 的 Base64 文本。 |
引擎侧完整能力(数据集校验、nice 刻度、色板解析、稳定哈希配色、数值格式化)全部在 commonMain 里随编译进入 .so,JVM 侧保留 7 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的几何逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 模型与几何公式全部来自上游:ChartMath 原样保留,ChartDataset / ChartStyle 重写为同构纯 Kotlin(Color→ARGB Long、Dp→Float),ChartLayout 的常量与公式逐条溯源上游 Canvas 代码;6 个 Compose 渲染文件隔离进 composeMain,保留源码不删除。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/(上游 Android 演示)留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 不引入任何 compose 的 ohos 坐标——compose 依赖被整体移出构建图,libs.versions.toml 只剩 HarmonyOS Kotlin 一个插件别名。 |
| UI 完整 | 单页覆盖全部桥接入口:图表类型、调色板、数据集、图例开关、渲染结果、尺寸统计和仓库链接。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
multiplot上游的全部渲染都是 Compose,适配面天然落在"模型 + 几何";ArkTS Demo 只是把桥暴露的渲染闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 Compose 依赖边界、确定"模型复用、渲染重写"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模 ── 图表请求 JSON 契约(类型/模式/色板/数据集/开关)、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 控件 chips、图例开关、PixelMap 解码和状态刷新
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
multiplot/ 图表库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
multiplot/src/commonMain/ ChartMath(上游原样)+ 纯 Kotlin 版 ChartDataset / ChartStyle + ChartLayout 布局引擎
multiplot/src/composeMain/ 上游 6 个 Compose 渲染文件(BarChart / LineChart / AxisRenderer / ChartLegend / ChartStyle / ChartDataset),保留不编译
multiplot/src/commonTest/ 7 项布局引擎交叉验证
example/nativeApp/ Kotlin/Native 动态库(Bridge + PlotRenderer + Font5x7 + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
sample/ 上游 Android 演示 app,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 本文章与效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :multiplot 与 :example:nativeApp,rootProject.name = "ohos_multiplot"。上游的 AGP / jetbrainsCompose / compose-compiler / vanniktech 发布插件、Maven Central 签名发布段全部随根构建替换剥离——本 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.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(从上游 9.3.0 降级;2.2.21 旧线只支持 Gradle 8.x) |
| Compose | 不使用——compose 依赖整体移出构建图,无任何 ohos 坐标 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.multiplot |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
上游 commonMain 共 7 个文件,按 Compose 依赖度分三类处置:
| 文件 | 依赖形态 | 处置 |
|---|---|---|
core/ChartMath.kt | 纯 Kotlin(只 import kotlin.math.*):niceMax / yAxisSteps / roundUpToMultiple | 原样保留 commonMain |
core/ChartDataset.kt | 仅依赖 Color(ChartSeries.color: Color?) | 重写为同构纯 Kotlin:color: Long?(ARGB),字段、默认值、校验、派生属性逐字一致 |
core/ChartStyle.kt | Color / Dp / dp | 重写为同构纯 Kotlin:色板与三色改 Long,cornerRadius 改 Float(dp),resolveColors 配色逻辑原样 |
bar/BarChart.kt | Canvas / 布局 / 动画 / TextMeasurer / 图例 | 移入 composeMain,几何公式抽进 ChartLayout.barChart |
line/LineChart.kt | 同上 + Brush / Path / clipRect | 移入 composeMain,几何抽进 ChartLayout.lineChart |
axis/AxisRenderer.kt | DrawScope / TextMeasurer / PathEffect | 移入 composeMain,网格与刻度几何抽进布局引擎 |
legend/ChartLegend.kt | FlowRow / material3.Text / 动画 | 移入 composeMain,图例改为渲染侧图元 |
composeMain 不是注册的 source set 名(不在默认层级模板里),Gradle 天然忽略它——源码完整保留、零编译,等社区 compose.ui ohos 制品与上游对齐后可直接接回。
1.4 布局引擎边界
输入 dataset + style + displayMode + widthPx + density(+ line 开关组)
输出 Frame(画布/图区边界/背景/圆角)
GridLine[](虚线网格 + 基线) YLabel[](右对齐刻度文本)
BarSpec[](x/y/w/h/色/圆角) XLabel[](居中类目标签,含末点强调色/加粗)
SeriesGeometry[](折线点列 + 面积多边形 + 点列) LegendEntry[](色点 + 标签 + 汇总值)
上游 Canvas 代码里的全部几何常量逐条搬进引擎:柱状图高 280dp、内边距 16/40/44/16 dp;折线图高 240dp、内边距 16/38/48/16 dp;组内边距 10%、柱圆角 4dp、折线宽 2dp、点半径 3dp / 末点 5dp、虚线 8/6 dp、网格 1dp、图例行高 22dp。渲染器只消费这些图元,不再自行计算任何业务几何。
1.5 页面边界
标题区 MULTILOT · OPENHARMONY / 副标题(CMP 图表库 · Kotlin/Native ohosArm64/ohosX64)
图表类型 分组柱状图 / 堆叠柱状图 / 折线图 三枚 chips
调色板 Kotlin / Vibrant / Pastel / Monochrome 四枚 chips
数据集 Weekly status / Monthly split 两枚 chips + 系列提示行
图例开关 Toggle(默认开)
图区 Image(PixelMap),100% 宽 300 高 Contain
状态行 最近一次渲染结果(如"生成成功 bar 1000x942 png=3769295B")
统计行 kind=size png=bytes
关于区 库简介与仓库链接
页面启动时 aboutToAppear 直接发起首次渲染,每个控件 onClick 都触发 generate() 重渲染。
第 2 阶段:目标与依赖打通
2.1 为什么钉 2.2.21-1.0.0、Gradle 降 8.14.1
multiplot 没有任何 kotlinx 依赖,不存在"制品线反推工具链"的问题(KStore 那条路线在这里不适用),直接采用社区标准钉法:HarmonyOS Kotlin 2.2.21-1.0.0——qrose / haze 同款,来自 eazytec Maven 双仓。该线只支持 Gradle 8.x,因此 wrapper 从上游 9.3.0 降到 8.14.1(wrapper 脚本与 jar 一并替换为审核通过的 8.14.1 版本),并在 gradle.properties 补三条 Kotlin/Native 开关:
version=0.1.0-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
kotlin.apple.deprecated.allowUsingOpenSourceCompiler=true
2.2 根构建替换
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()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_multiplot"
include(":multiplot")
include(":example:nativeApp")
根 build.gradle.kts 收敛为版本声明 + 测试纪律:
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}
allprojects {
group = "io.github.jeremygitau.multiplot"
version = providers.gradleProperty("version").orElse("0.1.0-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
gradle/libs.versions.toml 从 26 行瘦身到 4 行——agp、jetbrainsCompose、compose-compiler、vanniktech 与全部 compose 库坐标删除,只剩:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.3 编译模型与布局引擎
multiplot/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
./gradlew :multiplot:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :multiplot:compileKotlinOhosArm64 :multiplot:compileKotlinOhosX64 # BUILD SUCCESSFUL
模型与布局引擎的全部文件一个没动上游语义,composeMain 里的 6 个渲染文件不进任何 target。
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.jeremygitau.multiplot.core/**(模型 + ChartMath + ChartLayout,ohos 原样编译)
↓ ChartDataset / ChartStyle / BarSpec / SeriesGeometry
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API multiplotRender()
↓
ArkUI Index.ets(单页滚动)
请求与响应:
请求 : {"kind":"bar","displayMode":"grouped","palette":"kotlin",
"dataset":"weekly","showLegend":true,"width":1000,"density":3}
{"kind":"line","palette":"vibrant","dataset":"monthly",
"smooth":true,"showArea":true,"showDots":true,"emphasizeLastPoint":true}
响应 : {"ok":true,"kind":"bar","width":1000,"height":942,"bytes":3769295,"pngBase64":"..."}
{"ok":false,"error":"..."}
| 请求字段 | 类型 / 取值 | 默认 | 说明 |
|---|---|---|---|
kind | bar / line | bar | 图表类型 |
displayMode | grouped / stacked | grouped | 柱状图模式(line 时忽略) |
palette | kotlin / vibrant / pastel / monochrome | kotlin | 色板 |
dataset | weekly / monthly | weekly | 内置数据集 |
showLegend | bool | true | 图例行 |
width | int | 1000 | 输出宽度(320–2160 钳制) |
density | int | 3 | dp→px 换算(1–4 钳制) |
smooth / showArea / showDots / emphasizeLastPoint | bool | true | 折线开关组 |
categories / seriesCount / s0.id / s0.label / s0.values / s0.color | string / int | — | 自定义数据集覆盖(values 为逗号分隔串,MiniJson 扁平解析不碰嵌套数组) |
3.2 几何在 Kotlin 侧算完
桥内渲染这样走:
val layout = ChartLayout.barChart(dataset, style, mode, widthPx = width, density = density)
val image = PlotRenderer.renderBar(layout, density, style.labelColor)
ChartLayout 意味着桥不管像素怎么画——它只把上游的几何语义(槽位、归一高度、刻度、抽稀)展开成图元列表,渲染器与未来任何前端(包括接回 Compose 的 Canvas)消费的是同一份几何。density 参数承担上游 dp.toPx() 的角色,默认 3.0 对齐鸿蒙主流屏幕。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("MultiplotRender")
fun multiplotRender(request: String): String {
return try {
/* kind 分发 → 布局 → 光栅化 → PNG → Base64 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
}
非法 JSON、值数与类别数不匹配(ChartDataset 构造期 require 直接抛)、不支持的 kind,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libmultiplot_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosmultiplot.so)
│ MultiplotRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制几何语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("MultiplotRender")
fun multiplotRender(request: String): String // bar / line 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
MultiplotRender | multiplotRender(req) | 模型构造 + ChartLayout 布局 + 光栅化 + PNG |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"multiplotRender", nullptr, NapiMultiplotRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosmultiplot_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosmultiplot_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosmultiplot SHARED IMPORTED)
set_target_properties(ohosmultiplot PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(multiplot_napi SHARED napi_init.cpp)
target_link_libraries(multiplot_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosmultiplot)
OHOS_ARCH 为空或 libohosmultiplot.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const multiplotRender: (request: string) => string;
4.5 软件光栅化器(没有 Canvas / 没有系统字体的出图方案)
PlotRenderer 在纯 Kotlin 里实现全部图元:2x 超采样网格上硬边绘制、盒式降采样得到均匀抗锯齿;fillRoundRect(椭圆角判定)画圆角柱与背景卡片、drawDashedH(8/6 dp 虚线)画网格、drawThickSegment(点到线段距离判定,天然圆头)画折线、fillPolygonVerticalGradient(扫描线 even-odd + 按行 alpha 插值)画渐变面积、fillCircle 画白芯数据点、drawText 用 Font5x7(公有领域 5x7 点阵字体,覆盖 ASCII 可打印集,未知字符回落 ?)画轴标签与图例。PngEncoder 用 zlib stored(未压缩 deflate)+ CRC32 + Adler32 产出合法 PNG——与 cmp_qrose 同款实现。
第 5 阶段:ArkUI 宿主
5.1 触发渲染与解码图片
private generate(): void {
const kind: string = this.chartTab === 'line' ? 'line' : 'bar';
const displayMode: string = this.chartTab === 'stacked' ? 'stacked' : 'grouped';
const raw = multiplotRender(JSON.stringify({
kind: kind, displayMode: displayMode,
palette: this.palette, dataset: this.dataset, showLegend: this.showLegend
}));
const parsed = JSON.parse(raw) as RenderOk;
if (!parsed.ok || parsed.pngBase64 === undefined) { /* 状态行报错 */ return; }
this.decodePng(parsed.pngBase64);
}
private decodePng(b64: string): void {
const bytes: Uint8Array = new util.Base64Helper().decodeSync(b64);
const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const src = image.createImageSource(sliced);
src.createPixelMap().then((pm: image.PixelMap) => { this.pixelMap = pm; });
}
每个控件 onClick 都触发 generate(),@State pixelMap 变更驱动 Image 刷新——整页没有任何图表逻辑,只有控件状态到 JSON 请求的映射。
5.2 页面交互
单页自上而下:标题与副标题、图表类型三枚 chips(选中黑底)、调色板四枚 chips(选中紫底 #7F52FF)、数据集两枚 chips(选中青底 #0F766E)+ 系列提示行、图例 Toggle、图区卡片(白底描边)、状态行与统计行、关于区(库简介 + AtomGit 链接)。切换任一控件立即重渲染,状态行回显 kind / 尺寸 / PNG 字节数。
5.3 错误边界
ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并打 hilog。数据非法(如自定义系列值数与类别数不符)在 Kotlin 侧 require 拦截,不产生半张图。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosmultiplot)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName + MiniJson + 内置数据集(继承自 cmp_qrose 审核模式)
│ ├── PlotRenderer.kt 2x 超采样软件光栅化器
│ ├── Font5x7.kt 公有领域 5x7 点阵字体
│ └── PngEncoder.kt zlib stored PNG 编码
└── harmonyApp/
├── build-profile.json5 compatibleSdkVersion 5.1.0(18)、runtimeOS HarmonyOS
├── AppScope/app.json5 bundleName org.terminator.ohos.multiplot、versionName 0.1.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/libohosmultiplot_api.h
├── src/main/cpp/types/libmultiplot_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :multiplot:jvmTest
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 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.multiplot -m entry
四、完整代码对照
4.1 整体架构
io.github.jeremygitau.multiplot.core/**(模型 + 数学 + 布局引擎,ohos 原样编译)
│ ChartDataset / ChartStyle / ChartMath / ChartLayout
example/nativeapp(KN 侧)
│ MultiplotRender → PlotRenderer / Font5x7 / PngEncoder
libmultiplot_napi.so
│ multiplotRender
Index.ets
│ JSON parse + PixelMap 解码 + @State 更新
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_multiplot、sample 移出 |
gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 + kotlin-multiplatform 别名 |
build.gradle.kts(根) | 版本 0.1.0-ohos.1、全局禁用 Ohos*Test* 任务 |
multiplot/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64 |
multiplot/src/commonMain/.../core/*.kt | ChartMath(原样)、ChartDataset / ChartStyle(同构重写)、ChartLayout(新增布局引擎) |
multiplot/src/composeMain/** | 上游 6 个 Compose 渲染文件(保留不编译) |
multiplot/src/commonTest/.../ChartLayoutTest.kt | 7 项布局交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、内置数据集、MiniJson、错误通道 |
nativeApp/.../PlotRenderer.kt / Font5x7.kt / PngEncoder.kt | 软件光栅化 / 点阵字体 / PNG 编码 |
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 | 鸿蒙适配主文档(23 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 模型 | ChartDataset(categories, series) | 建模 + 构造期校验 + maxValue / maxStackedValue |
| 模型 | ChartStyle(palette, colorAssignment, …) | 全部视觉旋钮,resolveColors() 配色 |
| 数学 | ChartMath.niceMax / yAxisSteps | 1/2/5×10ⁿ 好看刻度(上游原样) |
| 布局 | ChartLayout.barChart(...) | 分组 / 堆叠柱全部图元(槽位、圆角、刻度、标签、图例) |
| 布局 | ChartLayout.lineChart(...) | 折线全部图元(平滑点列、面积多边形、点列、抽稀标签) |
| 布局 | ChartLayout.autoLabelStep(n) | ≤7→1、≤14→2、否则 5 的标签抽稀 |
| 渲染 | PlotRenderer.renderBar / renderLine | 图元 → 2x 超采样 ARGB → 降采样 → PNG |
| Native | MultiplotRender | C ABI JSON 导出 |
| N-API | multiplotRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | generate() | 控件状态 → 请求 → PixelMap 刷新 |
4.4 ArkTS 与 Kotlin 的边界
const raw = multiplotRender(JSON.stringify({
kind: 'bar', displayMode: 'stacked',
palette: 'monochrome', dataset: 'monthly', showLegend: true
}));
const res = JSON.parse(raw);
// res.ok === true;res.pngBase64 → Base64Helper 解码 → createPixelMap → Image 显示
val layout = ChartLayout.barChart(
dataset = dataset, style = style,
displayMode = ChartLayout.BarDisplayMode.STACKED,
widthPx = width, density = density,
)
val image = PlotRenderer.renderBar(layout, density, style.labelColor)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:隔离 Compose,而不是接社区 CMP 1.9.2
把上游从 CMP 1.11.1 / Kotlin 2.3.10 降到 1.9.2 / 2.2.21 理论上能"真编译",但要动上游源码里全部新 API 兼容层,且社区 CMP 制品本身仍在演进——渲染代码两头受气。隔离(composeMain 保留不编译)让上游源码零改动,模型与几何以纯 Kotlin 形态进 ohos,等社区工具链对齐后渲染器整体接回,光栅化降级为 fallback。
决策 2:新增 ChartLayout 布局引擎,而不是在 ArkTS 侧重写几何
图表库的核心资产不是画像素的代码,而是"每一笔画在哪"的几何语义。把几何抽成纯 Kotlin 引擎后:一份数学同时服务 JVM 测试、ohos 光栅化和未来的 Compose 接回;ArkTS 侧零几何逻辑,避免两处实现漂移。全部常量(280/240dp 高度、16/40/44/16 与 16/38/48/16 内边距、10% 组内边距、4dp 圆角、2dp 线宽、8/6 虚线)逐条溯源上游源码,不重新设计。
决策 3:颜色用 ARGB Long、尺寸用 Float dp——模型 API 与上游同构
ChartSeries.color: Color? 变 Long?、cornerRadius: Dp 变 Float,其余字段、默认值、校验消息逐字一致。这样 composeMain 里的上游渲染器将来接回时面对的是同构模型,适配成本最小;鸿蒙侧渲染只需在入口按 density 换算一次。
决策 4:KN 软件光栅化 + 2x 超采样 + 5x7 点阵字体
ohos 上没有 Compose Canvas、TextMeasurer 和系统字体度量可用。2x 超采样 + 盒式降采样用最少的代码换来全图元一致的抗锯齿;5x7 点阵字体是嵌入式领域数十年的公有领域方案,轴标签这种短文本完全够用;PNG 用 zlib stored 未压缩,实现只有百余行且已过社区审核(cmp_qrose 同款)。这是一套"不依赖任何平台图形栈"的完整出图管线。
决策 5:jvmTest 交叉验证 + 测试纪律
布局引擎在 commonMain,JVM 与 ohos 跑同一份代码;7 项 jvmTest 钉住刻度取整、数据集校验、分组柱边界、堆叠高度总和、折线点位与标签抽稀,是"几何没漂"的闸门。ohos 侧没有测试 runner,根构建全局禁用 Ohos*Test* 任务,测试统一 JVM 跑——与 KStore 的 turbine 处置同一条纪律。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :multiplot:jvmTest
7 项用例全部通过:
| 用例 | 钉住什么 |
|---|---|
niceMaxSnapsTo125Steps | 1/2/5×10ⁿ 取整:0→5、3→5、7→10、12→20、42→50、60→100 |
yAxisStepsWalkFromZeroToMax | 刻度从 0 到 max 等步进(10/5 → 0,2,4,6,8,10) |
datasetRejectsSeriesWithWrongValueCount | 构造期校验:值数 ≠ 类别数抛 IllegalArgumentException |
groupedBarsStayInsidePlotArea | 7 类 × 3 系 = 21 根柱,全部落在图区边界内,标签与图例数量正确 |
stackedSegmentsSumToCategoryTotal | 堆叠分段高度和 = 类别总值 / niceMax(maxStacked) × 图高(周一 4+2+1=7,yMax=10) |
lineChartProducesPointsPerSeriesInsidePlot | 每系列点列、面积基线闭合、点位全部在图区内 |
labelStepThinsLongCategoryAxes | 抽稀:7→1、14→2、30→5 |
其中堆叠用例在编写时先按错误的最大值假设断言失败,修正后与手算一致——交叉验证确实能抓住几何漂移。
6.3 编译与链接验证
:multiplot:compileKotlinJvm: BUILD SUCCESSFUL
:multiplot:jvmTest: BUILD SUCCESSFUL(7 项全绿)
:example:nativeApp:linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 example/harmonyApp/entry/libs/{arm64-v8a,x86_64}
二进制检查: libohosmultiplot.so 内 MultiplotRender 符号存在
KN so 体积: arm64 约 4.0MB、x86_64 约 3.7MB(debugShared 未 strip)
6.4 功能用例
- 启动即自动渲染默认组合(分组柱状 + Kotlin 色板 + Weekly 数据集 + 图例开),状态行显示
生成成功 bar 1000x942 png=…B; - 切"堆叠柱状图",三系列合并为每类一根整柱,色序与图例保持系列顺序;
- 切"折线图",出平滑曲线 + 渐变面积 + 白芯数据点,末点标签以系列一颜色加粗;
- 四枚色板切换,柱 / 线 / 图例颜色全部跟随,与上游
MultiplotPalettes色值一致; - 切 Monthly split,类别变 Jan–Jun、系列变 North / South 两系列,图例与配色自动收缩;
- 关闭图例开关,图例行消失,图表主体与状态行位置不变;
- 虚线网格、Y 轴刻度(0–niceMax 等步)、X 轴类目标签与上游 Canvas 渲染的视觉结构一致;
- 统计行回显
kind=size png=bytes,宽 1000 × 高 942(280dp 图高 + 12+22dp 图例行 × density 3)与布局引擎输出吻合。
6.5 验证结论
模型与布局引擎、Kotlin/Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的渲染会话:三种图表类型、四套色板、两个数据集与图例开关的全组合(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/multiplot.git
cd multiplot
export JAVA_HOME="/path/to/jdk-21"
./gradlew :multiplot:compileKotlinJvm :multiplot:jvmTest # 7 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp,配置签名后构建 HAP
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.multiplot -m entry
hdc shell hilog | grep Multiplot
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:multiplot真正编译成ohosArm64/ohosX64KLIB、example/nativeApp链出双 ABI 动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 钉
2.2.21-1.0.0旧线时 Gradle 必须降 8.14.1——上游 9.3.0 wrapper 起不来;这条与 KStore(2.4.20 线留 9.x)恰好相反,两条工具链线的经验不能互相套用; composeMain不是默认层级模板里的 source set 名,目录放进去即被忽略——但commonMain里新写的ChartDataset/ChartStyle与composeMain里的上游同名文件绝不能同时进编译,重接 Compose 时要先做合并而不是直接加 source set;- 上游分组柱间隙是 Canvas 里的裸
3fpx(不是 dp),直接照抄在高密度屏上几乎不可见,布局引擎里按3f * density放大为约 1dp——"照搬公式"也要区分 px 与 dp 语义; MiniJson是扁平键值解析,不支持嵌套数组:自定义系列的values用逗号分隔字符串(s0.values = "4,6,3")传入,桥内 split——省掉一个完整 JSON 解析器;- Kotlin 文件级
private const对同文件的顶层private class不可见(SS超采样因子最初声明在 object 里,PlotCanvas引用报 Unresolved)——移到文件顶层常量即可; parseColor里0xFF000000L or hex.toLongOrNull(16) ?: return null这种写法,or与?:优先级组合直接编译不过,nullable 要先解析再参与位运算;- 堆叠测试的期望值要手算类别和(Weekly 周一 4+2+1=7,maxStacked=10 → yMax=10)而不是拍脑袋——交叉验证的期望值本身也会写错,手算复核不可省;
OHOS_ARCH为空或entry/libs/<abi>/libohosmultiplot.so缺失时 CMake 配置期 FATAL_ERROR(提示先跑 link 任务)——这是刻意设计,避免 HAP 带着空 so 静默装上后运行期才炸;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();ArkTS 只 importlibmultiplot_napi.so,绝不能 importlibohosmultiplot.so——HarmonyOS Kotlin 的 CAdapter 不会把@CName业务符号挂到 ArkTS; - 上游
.gitignore忽略了gradle.properties,而鸿蒙构建的版本号与 JDK 路径恰恰写在里面——必须把这行从 ignore 里删掉并提交,否则换机器构建直接失败; - 签名材料由 DevEco 自动写入
build-profile.json5,提交前保护,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧出图是静态 PNG,上游的入场动画(
animateFloatAsState+clipRect揭示)是 Compose 运行时概念,不跨 C ABI;ArkTS 侧如需动画要自行驱动连续帧; - 轴标签 / 图例文本用 5x7 点阵字体,非系统字体,长标签观感一般(未知字符回落
?); - PNG 用 zlib stored 未压缩:1000×942 的图约 3.7MB(真机实测 3769295B),尺寸越大体积线性增长,UI 侧解码耗时随之上升;
- 上游
ChartStatPills统计卡片在 PNG 中与图例合并为一行「标签 + 汇总值」; Dp→ px 由density请求参数(默认 3)换算,不读设备真实密度;- android / ios 目标剥离(
sample/等源文件留在树里),发布、签名、POM 一并剥离,本 fork 不发 Maven Central; - HAP 安装必须使用与 bundle name
org.terminator.ohos.multiplot匹配的签名。
8.3 未来优化方向
- PNG 压缩升级为固定 Huffman 的 deflate(仍是纯 Kotlin,无 zlib 依赖),预计体积降一个数量级;
- 桥增加动画帧入口(
frameIndex/progress参数),ArkTS 侧用定时器驱动入场揭示动画; - 评估 SVG / 路径导出形态,让 ArkTS 侧用系统矢量管线渲染,摆脱位图字体;
ChartStatPills独立成图元,恢复上游三段式布局(pills / 图区 / 图例);- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 multiplot(7 文件:1 纯 Kotlin + 6 Compose)
→ 判定路线:compose 五件套无 ohos 制品 → 模型复用、渲染重写(cmp_qrose 模式)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 9.3.0 降 8.14.1)
→ 根构建替换 + catalog 瘦身(compose 依赖整体移出)
→ commonMain:ChartMath 原样 + 同构模型 + ChartLayout 布局引擎(常量逐条溯源上游)
→ ohosArm64 / ohosX64 KLIB + KN 软件光栅化(2x 超采样 + 5x7 字体 + PNG)
→ C ABI(@CName × 1)+ 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + PixelMap
→ ArkUI 真机单页验收(三类型 × 四色板 × 两数据集 × 图例开关)
9.2 封装层次
- 模型层:同构纯 Kotlin
ChartDataset/ChartStyle(校验、派生属性、色板、配色策略); - 数学层:上游原版
ChartMath(nice 刻度、刻度序列); - 布局层:
ChartLayout把上游 Canvas 几何展开为图元(Frame / BarSpec / SeriesGeometry / 标签 / 图例); - 渲染层:
PlotRenderer+Font5x7+PngEncoder生成双 ABI 动态库并通过 1 个 C ABI 输出 PNG Base64; - N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理控件状态、JSON 请求映射、PixelMap 解码与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配边界是"模型 + 几何",不是"渲染":图表库的价值在几何语义而不在画像素的框架。把几何抽成纯 Kotlin 引擎后,ohos 光栅化与未来 Compose 接回消费同一份真源,ArkTS 侧零几何逻辑——两处实现永远不会漂移;
- 路线选择基于当天的制品可达性:KStore 那批库因为 kotlinx ohos 制品线补齐而走"真编译整库";multiplot 的 compose 依赖没有 ohos 制品且跨两代版本,只能隔离重写。同一个社区、同一个季度、两条相反的路线,都是对的——先盘点依赖矩阵再定路线;
- 交叉验证是"几何没漂"的唯一可信证明:7 项
jvmTest从刻度取整到堆叠总和逐层钉住布局引擎,其中堆叠用例还真抓住了期望值的手算错误。静态 PNG 的视觉验收只能证明"能出图",几何正确性必须靠 JVM 测试兜底。
9.4 适配成果
当前适配已完成:
- multiplot
0.1.0模型与几何的双 ABI Kotlin/Native 编译(ChartMath原样、模型同构重写、布局引擎新增,上游 Compose 渲染器零改动隔离保留); - 1 个 C ABI 符号和 C++ N-API 包装层(二进制检查
MultiplotRender符号存在); - 纯 Kotlin 软件光栅化管线(2x 超采样抗锯齿、圆角柱、虚线网格、圆头粗线、扫描线渐变面积、白芯数据点、点阵字体、PNG 编码);
- DevEco Stage ArkUI 单页验收页面(三图表类型、四色板、两数据集、图例开关、尺寸统计);
- 7 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)、主文档(23 节中文)和本地效果图。
适配过程 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)