开源鸿蒙平台 CMP 三方库「compose-multiplatform-chart」适配全流程
本文记录
compose-multiplatform-chart接入 OpenHarmony 的完整过程,覆盖图表库源码依赖度盘点、Compose 源码集隔离、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、软件光栅化复现 Compose 绘制语义、ArkUI 真机页面、HAP 构建和真机验收。
compose-multiplatform-chart是基于 Compose Multiplatform 的图表库:LineChart面向大数据集(内部先做 LTTB 降采样再绘制)、LiveLineChart面向实时数据、BarChart提供柱状图,全部通过 ComposeCanvas渲染,支持平滑曲线、区域渐变填充、数据点、带标签的坐标轴与捏合缩放手势。本次适配走的是社区早中期路线(qrose / Vico / Reorderable 的"算法内核真编译 + Compose UI 隔离"):上游工具链(Kotlin 1.9.20 / CMP 1.5.10)与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)对不齐,@Composable组件无法编进 ohos,因此数据模型与 LTTB 降采样算法原样编译成 OpenHarmony KLIB,Compose 组件隔离到不参与编译的composeMain,鸿蒙侧用 Kotlin/Native 软件光栅化复现上游的绘制语义(轴标签位置、辅助网格线、拐角平滑、渐变填充),一个能画 200 万点级折线图与柱状图的完整管线运行在真机上,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/compose-multiplatform-chart.git
上游仓库: maximilianproell/compose-multiplatform-chart
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-multiplatform-chart 为 Compose Multiplatform 应用提供"数据 → 图表"这一件事的完整答案:LineChart 接收 List<LineDataSet>,数据多于 250 点时内部自动走 LTTB(Largest-Triangle-Three-Buckets)降采样,这正是它能流畅绘制上游 Demo 中 200 万行心率 CSV 的原因;LiveLineChart 砍掉降采样换实时性;BarChart 的 BarChartEntry(xValue, yValue) 用字符串标签建模柱体。样式面覆盖 LineConfig(平滑曲线 / 数据点 / 区域填充 / 线宽)与 LabeledXAxisConfig / LabeledYAxisConfig(标签数量、格式化器、辅助线开关)。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套图表能力,但上游构建(Kotlin 1.9.20 / CMP 1.5.10 / AGP 8.2.0 / Gradle 8.2.1,Groovy 根脚本)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 画个折线,页面可以显示,却无法证明公共的 LTTB 算法与数据模型在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证算法内核、光栅化输出、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 1.9.20 / CMP 1.5.10 / AGP 8.2.0(Groovy 根构建)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| Compose 依赖全面渗透 | 库的 18 个源文件里 10 个直接 import androidx.compose(组件、配置、模型里都有 Color / Dp),ohos 编译器看不到这些符号。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 绘制语义跨不了桥 | Canvas / Path / PathEffect.cornerPathEffect / TextMeasurer 是 Compose 运行时概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:数据模型与 LTTB 算法的 ohos 编译、Compose 源码集隔离(cmp_qrose 模式)、软件光栅化复现绘制语义和 C ABI/N-API 桥接层。降采样行为完全由上游原版算法负责,ArkTS 只负责调用 Native、解析 JSON 和展示 PNG。
1.2 库提供的能力
Demo 提供一个真实的图表会话,对应桥接的全部入口:
- 折线图(line):指定波形(正弦 / 心率)与点数(100…5000),数据在 Kotlin/Native 侧由可复现 LCG 生成器产出;多于 250 点自动 LTTB 降采样——与上游
LineChart内部行为一致; - 大数据 LTTB(bigdata):点数 1 万…200 万,降采样档位 LTTB 250 / LTTB 1000 / 关闭,状态行直接显示
in → out点数对比,把上游"200 万行 CSV 也能画"的核心卖点变成可验收项; - 柱状图(bar):
values+labels逗号串入参,三套预设(工作日 / 季度 / 六项)一键切换,对应上游BarChartEntry模型; - 样式参数:平滑曲线 / 数据点 / 区域填充三开关、线宽 1-8px、五套前景背景色板、画布尺寸与轴标签数量。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
kind / width / height | 回显图表类型与 PNG 尺寸。 |
inCount / outCount / buckets | LTTB 前后点数与桶数——验证降采样发生的直接证据。 |
minY / maxY | 数据值域,用于 JVM 交叉验证对账。 |
bytes / pngBase64 | PNG 字节数与 Base64 载荷。 |
引擎侧完整能力(LTTB 分桶、三角形面积选点、边界点保留、DataPoint / BarChartEntry 模型)全部在上游 commonMain 里原样随编译进入 .so,一行未改;JVM 侧保留 jvm() 目标做交叉验证,证明 ohos .so 里执行的降采样逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | commonMain 保留 8 个纯 Kotlin 文件(DataPoint、BarChartEntry、LTTB 采样全套 6 个)原样编译,不提取、不重写;唯一新增源文件是给桥用的 40 行入口 MultiplatformChart.kt。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建从 Groovy 替换为极简 ohos Kotlin DSL 构建(cmp_qrose 模式);上游 app(Android demo)、shared(iOS demo)、iosApp 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI(ChartRender)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 库模块零第三方依赖(纯 Kotlin),不存在制品线问题;Kotlin 钉 2.2.21-1.0.0(qrose 同款已审钉法)。 |
| UI 完整 | 单页三 Tab 覆盖全部桥接入口:折线样式、大数据降采样对比、柱状图预设与自定义。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙侧没有 Compose
Canvas,图表由 Kotlin/Native 软件光栅化输出 PNG,ArkTS Demo 只是把桥暴露的绘制管线画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与源码 Compose 依赖度、确定交付物
第 2 阶段:目标与构建打通 ── 根构建替换(Groovy → KTS)、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:源码集拆分 ── 10 个 Compose 文件迁 composeMain、8 个纯 Kotlin 留 commonMain、新增 MultiplatformChart 入口
第 4 阶段:原生桥接与光栅化 ── @CName C ABI、Rasterizer 复现绘制语义、5x7 像素字体、PNG 编码、LCG 数据生成
第 5 阶段:ArkUI 宿主 ── 三 Tab 页面、参数状态机、Base64 → PixelMap 显示
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
multiplatformchart/ 上游图表库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
multiplatformchart/src/commonMain/ 数据模型 + LTTB 采样全套 + MultiplatformChart 入口(8+1 个文件)
multiplatformchart/src/composeMain/ 上游 Compose 组件 ×10(LineChart/LiveLineChart/BarChart/Axis/Config)
app/ 上游 Android demo(AGP + Hilt),留盘不进 settings
shared/ + iosApp/ 上游 iOS demo,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + Rasterizer + Font5x7 + PngEncoder + ChartData)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/img/ 真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :multiplatformchart 与 :example:nativeApp,rootProject.name = "ohos_multiplatformchart"。上游的 maven-publish / vanniktech / signing 发布插件、AGP classpath、Android namespace 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Eclipse Temurin,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 整体替换;2.2.21 线要求 Gradle 8.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.chart |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
对库的 18 个源文件逐个 grep import,按"是否触碰 androidx.compose"分成两堆:
| 分类 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| 纯 Kotlin ×8 | DataPoint、BarChartEntry、sampling/{Point, Area, Bucket, Triangle, OnePassBucketizer, LTThreeBuckets} | 零依赖、零 UI、零 expect | 留在 commonMain 原样编译 |
| Compose ×10 | LineChart、BarChart、Axis、XaxisLabel、Calculations、LabeledAxisConfig、LineConfig、BarConfig、LineDataSet、LineChartCalculations | androidx.compose.{runtime, foundation, ui, material3, animation} | 迁入 composeMain,ohos 不编 |
这个分布说明图表库与存储库(KStore)的适配面完全不同:存储库的引擎全在纯 Kotlin 里,真编译整库即可;图表库的价值大头(绘制)恰恰在 Compose 侧。好在"能算"的部分——LTTB 降采样、数据模型——恰好是纯 Kotlin,而且是上游的核心卖点(200 万点能画)。拆分的红线是:留下的代码不引用移走代码的任何类型。逐文件核对后确认 DataPoint / BarChartEntry / sampling 六件套自洽,拆分是干净的;反向依赖(composeMain → commonMain)无所谓,因为它们本来就不参与 ohos 编译。
1.4 页面边界
标题区 multiplatformchart · OPENHARMONY / 副标题(CMP 图表库 · Kotlin/Native ohosArm64/ohosX64 · LTTB 降采样)
Tab 切换 折线图 / 大数据 LTTB / 柱状图
折线图 Tab 波形(正弦/心率)、点数(100/300/1000/5000)、平滑曲线/数据点/区域填充开关、strokeWidth 滑条
大数据 Tab 点数(1万/10万/100万/200万)、LTTB 档位(250/1000/关闭)、波形切换
柱状图 Tab 预设(工作日/季度/六项)、values/labels 可编辑文本域
公共区 五套色板、生成按钮、Image(PixelMap) 图表区、状态行、元信息行(kind/size/png/耗时 + LTTB in→out)
页面启动时 aboutToAppear 自动生成一次默认折线图,作为 L3 运行层的第一张图。
第 2 阶段:目标与构建打通
2.1 根构建替换(Groovy → Kotlin DSL)
上游根脚本是 Groovy 的 settings.gradle / build.gradle(AGP + Hilt 插件声明、Android 仓库配置),鸿蒙工具链下配置期直接死。整体替换:
// settings.gradle.kts
pluginManagement {
repositories {
// HarmonyOS Kotlin 工具链仓库必须排在最前,
// 否则解析到官方 Kotlin 插件,ohosArm64()/ohosX64() 配置期 Unresolved
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 { /* 同上 */ }
}
rootProject.name = "ohos_multiplatformchart"
include(":multiplatformchart")
include(":example:nativeApp")
// build.gradle.kts(根)
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}
allprojects {
group = "io.github.maximilianproell" // 沿用上游 group
version = providers.gradleProperty("version").orElse("2.1.0-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false } // 测试统一 JVM 跑
}
gradle.properties 写入 JDK 21 路径、version=2.1.0-ohos.1 与 kotlin.native.ignoreDisabledTargets=true;libs.versions.toml 收敛为只钉 kotlin = "2.2.21-1.0.0" 一个版本;wrapper 整体替换为 8.14.1(gradle-wrapper.jar + gradlew.bat 一并换掉,版本不配套的 wrapper jar 会校验失败)。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已审钉法(qrose / haze):HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21。本库零第三方依赖,不需要追新制品线(对比 KStore 为读 coroutines 2.4 元数据升到 2.4.20 线)——没有依赖推着升级时,严格沿用已审核的旧线,审核与复现成本最低。上游源码是 Kotlin 1.9.20 编的,2.2.21 向后兼容无压力。
2.3 编译算法内核
multiplatformchart/build.gradle.kts 从"AGP + CMP + 发布三件套"收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 纯 Kotlin,零依赖
}
}
实测:
.\gradlew.bat :multiplatformchart:compileKotlinJvm
.\gradlew.bat :multiplatformchart:compileKotlinOhosArm64 :multiplatformchart:compileKotlinOhosX64
# 全部 BUILD SUCCESSFUL
LTTB 六件套与数据模型一个没动,ohos 编译器看不到任何 androidx.compose 符号。
2.4 上游遗留模块的处置
app(Android demo,Hilt/kapt/AGP 8.2)、shared(iOS framework)、iosApp(Xcode 工程)留盘不进 settings.gradle.kts——它们引用的 libs.plugins.android.* / compose-multiplatform 别名已从版本目录移除,拿来 sync 只会报别名找不到。与 KStore 处置 kstore-storage 同款:存在方式只有"留盘不进构建"这一种。
第 3 阶段:桥协议建模
3.1 JSON 契约
at.maximilianproell.multiplatformchart/**(上游模型 + LTTB,原样编译)
↓ DataPoint / BarChartEntry / LTThreeBuckets.sorted
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ 生成数据 / 降采样 / 光栅化 / PNG
Rasterizer + Font5x7 + PngEncoder
↓ UTF-8 JSON(含 pngBase64)
N-API chartRender()
↓
ArkUI Index.ets(三 Tab)
请求与响应:
请求 : {"kind":"line","gen":"sine","count":1000,"downsample":-1,
"smooth":true,"dots":true,"fill":false,"strokeWidth":3,
"fg":"#1D4ED8","bg":"#FFFFFF","yLabels":5,"xLabels":5}
{"kind":"bar","values":"12,31,7,25,18","labels":"MO,DI,MI,DO,FR"}
响应 : {"ok":true,"kind":"line","width":960,"height":520,
"inCount":1000,"outCount":252,"buckets":250,
"minY":22.3,"maxY":80.4,"bytes":12345,"pngBase64":"..."}
{"ok":false,"error":"..."}
三个刻意设计:其一,大数据不过 JSON——百万点在 KN 侧由 LCG 生成器产出,ArkTS 只传 gen/count/seed 三个数,既避免巨型字符串也测出 LTTB 真实收益;其二,downsample 默认 -1(自动:点数 >250 就降 250 桶),逐字对齐上游 LineChart 的内部行为,0 模拟 LiveLineChart 的不降采样路径;其三,seed 默认 42,同 seed 必出同序列,JVM 与真机可对账。
3.2 入口封装为什么需要 MultiplatformChart
上游 LTThreeBuckets.sorted 操作的是采样专用的 Point(x: Double, y: Double),图表 API 用的是 DataPoint(xValue: Float, yValue: Float),上游 LineChart 在两者间手动 map(commonMain → map(Point) → sorted → map(DataPoint))。桥不能碰 @Composable 内部,这段转换收进 commonMain 的公开入口:
public object MultiplatformChart {
public fun downsample(data: List<DataPoint>, desiredBuckets: Int): List<DataPoint> {
if (data.size <= desiredBuckets + 2) return data
val sampled = LTThreeBuckets.sorted(
input = data.map { Point(x = it.xValue.toDouble(), y = it.yValue.toDouble()) },
inputSize = data.size,
desiredBuckets = desiredBuckets,
)
return sampled.map { DataPoint(xValue = it.x.toFloat(), yValue = it.y.toFloat()) }
}
}
这是全仓唯一新增的库源文件(对应 KStore 的 8 行 dispatcher actual 位置),算法本体零改动。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ChartRender")
fun chartRender(request: String): String = try {
/* kind 分发 + 参数钳制 + 光栅化 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 points 串、空 values、越界参数(钳制兜底)全部走同一个 ok:false 通道,HAP 不崩。
第 4 阶段:原生桥接与光栅化(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libchart_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoschart.so)
│ ChartRender
▼
UTF-8 JSON + DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS 模块——libohoschart.so 里能 nm 到 ChartRender,但 ArkTS import 'libohoschart.so' 看不到它。所以 CMake 必须再包一层 NAPI so,ArkTS 只 import libchart_napi.so。
4.2 Kotlin/Native 导出函数
@CName("ChartRender")
fun chartRender(request: String): String // line / bar 一口进,JSON 含 PNG Base64
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ChartRender | chartRender(req) | 数据生成 + MultiplatformChart.downsample + 光栅化 + PNG |
单符号设计:图表间差异全在 JSON 字段(kind / gen / values),不像存储库需要按 op 分发,一个入口足够,以后加图表类型只加字段不改签名。
4.3 光栅化:把 Compose 绘制语义翻译成像素
Rasterizer.kt 逐条复现上游的绘制语义,这是本次适配技术含量最高的部分:
上游(Compose Canvas) | 光栅化实现 | 语义对齐点 |
|---|---|---|
drawYAxisWithLabels(Axis.kt) | drawYAxis() | 标签值 = min + step*(n-1-i),画在 plotH/(n-1)*i 高度;最顶部不画辅助线;标签位于辅助线左上方、贴图内左侧 |
drawLine(alpha = 0.4F) 辅助线 | fillRect(alpha = 0.4) | 横贯绘图区、40% 透明 |
drawXLabel(clipOnBorder = false) | drawXLabels() | 图表下方、按值居中、越界收回画布(左 <0 拉回 0,右溢出拉回 width-textWidth) |
PathEffect.cornerPathEffect(strokeWidth) | cornerSmooth() | 每个内部顶点沿入/出边各退 min(r, 边长/2),画 a → 顶点 → b 二次贝塞尔(8 段采样) |
Brush.verticalGradient(lineColor → Transparent) | fillPolygonVerticalGradient() | 扫描线逐行填充,alpha 从折线顶部到图底线性衰减(上限 0.45) |
drawPoints + StrokeCap.Round(半径 4.dp) | fillCircle(r = 4.0) | 点数 >600 自动关闭(250 桶 LTTB 后最多 252 点,恰好在阈值内) |
BarChart 柱宽 12.dp + 剩余均分间距 | 自适应柱宽 | plotW/n*0.55(clamp 3…64),间距公式保持上游形状;柱标签居中收回 |
坐标变换同样复刻 LineChartCalculations.dataToOffSet 的语义:x = left + (x-minX)/(maxX-minX)*plotW,y = bottom - (y-minY)/ySpan*plotH。
文字是图表轴标签的硬需求,而 Kotlin/Native 没有系统字体。Font5x7.kt 内置经典 5x7 点阵字体(数字、大写字母、- . : % / + 共 40 字形,列字节位 0 为顶行),2 倍缩放绘制——轴标签以数字为主,字形足够;实测真机轴刻度清晰可读。
PNG 编码(PngEncoder.kt)走 zlib stored 块(不压缩、零依赖):ARGB 像素 → 滤波 0 行 → stored DEFLATE 块 + adler32 → IHDR/IDAT/IEND + crc32。常规图表几十 KB,ArkTS Base64Helper.decodeSync + image.createImageSource 一次出 PixelMap。
数据生成(ChartData.kt)用 Numerical Recipes LCG:sine 是双正弦叠加微噪声、heartrate 是 70bpm 随机游走 + 偶发尖峰,对应上游 Demo 的两类数据形态(基础折线 / 心率 CSV)。
4.4 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"chartRender", nullptr, NapiChartRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};
回调统一三步:napi_typeof 校验 string、OH_LOG_INFO 打请求字节数、调 ChartRender 并包装返回。KN 字符串必须经 libohoschart_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。API 26 的 NAPI 头把 napi_get_value_string_utf8 缓冲区标成非 const——先量长度再开 std::vector<char> 拷贝,规避 std::string::data() 的 const 限制。
add_library(ohoschart SHARED IMPORTED)
set_target_properties(ohoschart PROPERTIES IMPORTED_LOCATION ${KN_SO} IMPORTED_NO_SONAME TRUE)
add_library(chart_napi SHARED napi_init.cpp)
target_link_libraries(chart_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohoschart)
OHOS_ARCH 为空或 libohoschart.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath + POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 互相可见。
export const chartRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 参数状态机与触发
private buildRequest(): string {
if (this.tab === 'bar') {
return JSON.stringify({ kind: 'bar', values: this.barValues,
labels: this.barLabels, fg: this.fg, bg: this.bg });
}
if (this.tab === 'bigdata') {
return JSON.stringify({ kind: 'line', gen: this.gen, count: this.bigCount,
downsample: this.buckets, dots: false, smooth: true, fg: this.fg, bg: this.bg });
}
return JSON.stringify({ kind: 'line', gen: this.gen, count: this.count,
downsample: -1, smooth: this.smooth, dots: this.dots, fill: this.fill,
strokeWidth: this.strokeWidth, fg: this.fg, bg: this.bg });
}
private generate(): void {
const started: number = Date.now();
const raw: string = chartRender(this.buildRequest());
this.lastMillis = Date.now() - started; // 耗时入状态行
const parsed: RenderOk = JSON.parse(raw) as RenderOk;
/* inCount/outCount/buckets 入元信息行 → decodePng */
}
PNG 解码三步:Base64Helper.decodeSync → buffer.slice → createImageSource(...).createPixelMap(),Image(pixelMap) 全宽显示。大数据 Tab 固定 dots: false——百万点画圆点既慢又没意义,桥侧 >600 点也会自动忽略。
5.2 页面交互
三 Tab 自上而下:Tab 胶囊行 → 参数面板(随 Tab 切换)→ 五套色板(双色块预览 + 选中黑框)→ 生成按钮(防重入)→ 图表 Image 区 → 状态行 → 元信息行。每个参数按钮点击即重新生成,所见即所得;大数据 Tab 额外显示 LTTB: {in} 点 → {out} 点 (buckets={n}) 对账行和耗时毫秒数。
5.3 对比度和错误边界
选中态深色白字、未选中浅灰深字;ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)被 try/catch 收进状态行与 hilog。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoschart)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName("ChartRender") + MiniJson(继承自 cmp_qrose 审核模式)
│ ├── Rasterizer.kt 绘制语义复现 + PxCanvas
│ ├── Font5x7.kt 5x7 点阵字体
│ ├── PngEncoder.kt zlib stored PNG
│ └── ChartData.kt LCG 演示数据生成
└── harmonyApp/
├── build-profile.json5 signingConfig=default(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.chart、versionName 2.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/libohoschart_api.h
├── src/main/cpp/types/libchart_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :multiplatformchart:compileKotlinJvm `
:example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64
用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),File → Project Structure → Signing Configs 勾选自动签名,构建 HAP:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.chart -m entry
四、完整代码对照
4.1 整体架构
at.maximilianproell.multiplatformchart/**(上游模型 + LTTB 原样编译)
│ DataPoint / BarChartEntry / LTThreeBuckets
MultiplatformChart.downsample(唯一新增入口)
│ DataPoint ↔ Point 转换 + LTTB
Bridge.kt + MiniJson
│ ChartRender
Rasterizer + Font5x7 + PngEncoder
│ PNG Base64
libchart_napi.so
│ chartRender
Index.ets
│ JSON parse + PixelMap
▼
ArkUI 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_multiplatformchart、app/shared 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单版本钉版 |
build.gradle.kts(根) | 版本 2.1.0-ohos.1、全局禁用 Ohos*Test* 任务 |
multiplatformchart/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64 |
multiplatformchart/src/composeMain/(10 文件) | 上游 Compose 组件隔离(ohos 不编) |
multiplatformchart/.../MultiplatformChart.kt | 唯一新增库源文件(downsample 入口) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、参数钳制、MiniJson、错误通道 |
nativeApp/.../Rasterizer.kt | 绘制语义复现 + PxCanvas 像素画布 |
nativeApp/.../Font5x7.kt / PngEncoder.kt / ChartData.kt | 点阵字体 / 零依赖 PNG / LCG 数据 |
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 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 模型 | DataPoint(xValue, yValue) | 折线数据点(commonMain 原样编译) |
| 模型 | BarChartEntry(xValue: String, yValue: Float) | 柱体 + 字符串标签 |
| 算法 | LTThreeBuckets.sorted(input, size, buckets) | LTTB 主入口(上游原件) |
| 算法 | OnePassBucketizer.bucketize | 分桶(余数均匀摊到前几个桶) |
| 算法 | Triangle.result | 三桶滑动窗口里选最大三角形面积的点 |
| 入口 | MultiplatformChart.downsample(data, buckets) | 新增桥用入口(DataPoint ↔ Point 换装) |
| Native | ChartRender | C ABI JSON 导出(line / bar 一口进) |
| N-API | chartRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | buildRequest() / generate() | 参数拼装与触发 |
4.4 ArkTS 与 Kotlin 的边界
const raw = chartRender(JSON.stringify({
kind: 'line', gen: 'heartrate', count: 1000000,
downsample: 250, smooth: true, dots: false, strokeWidth: 3,
fg: '#1D4ED8', bg: '#FFFFFF'
}));
// JSON.parse(raw) → { ok:true, inCount:1000000, outCount:252, pngBase64:'...' }
val raw = /* 生成 100 万点 */
val buckets = if (raw.size > 250) 250 else 0
val points = if (buckets > 0) MultiplatformChart.downsample(raw, buckets) else raw
val image = Rasterizer.line(points, width, height, fg, bg, grid, text,
smooth, dots, fill, strokeWidth, yLabels, xLabels, minY, maxY)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:composeMain 隔离而不是砍掉 Compose 组件
上游 10 个 Compose 文件不是死代码:它们是 Android / iOS / 未来桌面端的正主。整目录迁入 src/composeMain(非 Kotlin 源集,ohos 编译不可见),既让 ohos 构建干净,又保留对照与回迁能力——等社区 compose.ui ohos 制品与上游 API 对齐,把目录改回 commonMain 即可接回,光栅化降级为 fallback。砍掉它们等于放弃 fork 的上游同步能力。
决策 2:光栅化放 Kotlin/Native,而不是返回坐标让 ArkTS 画 Canvas
社区里 Vico 走的是"数据过桥、ArkTS Canvas 绘制"。本仓选择 KN 光栅化(qrose 同构):绘制语义(标签位置、平滑算法、渐变规则)全部在 Kotlin 侧与 JVM 对照,ArkTS 只做 Image(pixelMap),验收面最小。代价是快照无交互;收益是绘制语义零复制、单端可对账。需要交互时再补 ArkTS Canvas 路径,桥协议不用变(加 kind=line_coords 返回点集即可)。
决策 3:单 C ABI + 扁平 JSON,大数据不过桥
ChartRender 一个符号覆盖折线与柱状,图表差异全在 JSON 字段。百万点数据在 KN 侧生成,ArkTS 只传 gen/count/seed——把 200 万点塞进 JSON 既慢又测不出 LTTB 真实收益。seed 保证可复现,JVM 与真机可逐位对账。
决策 4:严格沿用已审工具链旧线(2.2.21-1.0.0)
库零第三方依赖,没有制品线推着升级(对比 KStore 为 coroutines 元数据升 2.4.20 线)。没有理由时不动钉法:HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21 与 qrose / haze 完全一致,审核与排错经验直接复用。
决策 5:保留 jvm() 做交叉验证
上游 commonTest 是空的(没有现成测试套件),交叉验证改用对账法:JVM 与 ohos 跑同一份 MultiplatformChart.downsample,同 seed 生成同序列,比较 outCount / minY / maxY。真机 1000 点正弦实测 outCount=252(1000 > 250 → 默认 250 桶 → 首尾 + 250 结果点),与算法推导逐位一致。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11 x64、JDK 21(Eclipse Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :multiplatformchart:compileKotlinJvm # BUILD SUCCESSFUL
上游 commonTest 为空,验证按对账法进行(方法详见仓库 README.OpenHarmony_CN.md 第 12 节):JVM 与 ohos 执行同一 MultiplatformChart.downsample,同 seed 序列输出应逐位一致。真机侧 1000 点正弦波(默认自动降采样)返回 inCount=1000、outCount=252、buckets=250,与 LTTB 输出规模推导(首尾点 + 每桶一个代表点 = 桶数 + 2)完全吻合;minY=22.3、maxY=80.4 与生成器公式(50 ± 20 主正弦 + 8 副正弦 + 微噪声)值域一致。
6.3 HAP 验证
:multiplatformchart:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosArm64: BUILD SUCCESSFUL(arm64-v8a/libohoschart.so ≈3.9MB)
:example:nativeApp:linkDebugSharedOhosX64: BUILD SUCCESSFUL(x86_64/libohoschart.so ≈3.6MB)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/<abi>/(含 libc++_shared.so ≈1.8/1.7MB)
llvm-nm -D: ChartRender 为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libchart_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
CAdapter 链接期有一条已知 clang 警告(api.cpp:448: non-void function does not return a value in all control paths),属 HarmonyOS Kotlin 生成的 napi 胶水,非业务代码,.so 正常链接——与 qrose 仓记录一致。
6.4 功能用例
- 启动即自动生成默认折线图(正弦 1000 点、平滑 + 数据点开、填充关、线宽 3、蓝色板),无需点击;
- 折线图 Tab:Y 轴五档刻度(22.3 / 36.8 / 51.4 / 65.9 / 80.4)自上而下递减、辅助网格线贯穿、X 轴 0…999 五档标签居中——与上游
drawYAxisWithLabels/drawXLabel语义一致; - 切波形为"心率",曲线变为 70bpm 随机游走形态,偶发尖峰可见;
- 关闭"平滑曲线"后拐角变直角;打开"区域填充"后线下出现上深下浅渐变;开关"数据点"圆点随之显隐;
- 大数据 Tab:100 万点 + LTTB 250 秒级返回,
in→out行显示1000000 → 252;切"LTTB 1000"变1000000 → 1002;切"关闭降采样"耗时与 PNG 字节数显著增大——LTTB 的价值直接可见; - 柱状图 Tab:默认工作日五柱(12/31/7/25/18,MO…FR 标签居中),切季度/六项预设即换数据,编辑 values 文本域自定义数值即刻生效;
- 五套色板切换,图表前景背景同步变化,反色板下轴标签仍清晰;
- 元信息行显示
kind=line size=960x520 png=…B …ms,与状态行互为印证。
6.5 验证结论
算法内核、数据模型、光栅化输出、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的图表会话:LTTB 降采样对账、绘制语义复现与柱状图模型闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-multiplatform-chart.git
cd compose-multiplatform-chart
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :multiplatformchart:compileKotlinJvm # JVM 交叉验证目标
.\gradlew.bat :multiplatformchart:compileKotlinOhosArm64 :multiplatformchart:compileKotlinOhosX64 # klib
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so
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.chart -m entry
hdc shell hilog | findstr Chart
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:multiplatformchart真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Gradle wrapper 升级要整体替换(
gradle-wrapper.jar+ properties + 脚本),只改 properties 里的版本号会因 jar 不配套校验失败; vararg Int在函数体内已是IntArray,对其再调toIntArray()编译不过——点阵字体表注册函数的实际返回类型要写对;- 局部辅助函数命名不能与
MutableMap.put撞名:buildMap里写fun put(...) = put(...)会造成类型检查递归报错,改名glyph即可; - alpha 混合公式里的除法必须是浮点:
(dst - src) * a / 255(Int 除法)会截断为 0,渐变与辅助线全部消失,要写成/ 255.0; - 根构建的
/build/是根锚定模式,盖不住嵌套目录——example/harmonyApp的oh_modules/.hvigor/entry/build/entry/libs要单独追加 gitignore,否则 so 与构建产物混进提交; - 上游
app/shared引用的libs.plugins.android.*别名已从版本目录移除,任何把它们 include 进来的尝试都会在配置期报别名找不到——留盘不进 settings 是唯一处置; build-profile.json5里signingConfig引用default而signingConfigs留空数组,由 DevEco 勾选自动签名后回填完整material配置——证书绝不进仓库;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;DevEco 模拟器是 x86_64,只编 arm64 会栽在这; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 打包 JDK 必须是 DevEco 自带 JBR,系统 JDK 8 会让
hvigorw assembleHap中途报不相干的错。
8.2 已知问题
- 鸿蒙侧没有 Compose
Canvas,出图是 KN 光栅化近似;轴标签用 5x7 点阵字体(数字 + 大写字母),中文与小写不支持,需要富文本标签时在 ArkTS 侧叠加Text; - 上游
LineChart的捏合缩放 / 拖动手势(detectTransformGestures)、LiveLineChart的实时流式重组、BarChart的入场动画是 Compose 运行时能力,不在 PNG 快照路径上; - 多数据集(
List<LineDataSet>一图多线)暂未桥接:桥一次画一条线,多线需多次调用叠加或扩展fg2/points2字段; - PNG 为 zlib stored 未压缩,常规图表几十 KB,关闭降采样的百万点图会到 MB 级;
- Android / iOS 目标与 Maven 发布剥离(源文件留在树里),本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.chart匹配的签名。
8.3 未来优化方向
- 补
kind=line_coords返回坐标点集的 ArkTS Canvas 路径,恢复捏合缩放与实时更新交互; - 多
LineDataSet桥接(一图多线 + 图例),对齐上游lineDataSets参数; - x 轴时间文本格式化(
xFormatter字段),标签显示HH:mm; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵与 uitest 自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 multiplatformchart(18 源文件,10 个 Compose)
→ 源码依赖度盘点(grep import 逐文件分类)
→ 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(已审旧线)
→ 根构建替换(Groovy → KTS,cmp_qrose 模式)
→ composeMain 隔离 ×10 / commonMain 保留 ×8 + MultiplatformChart 入口
→ C ABI(@CName × 1)+ 扁平 JSON + KN 侧大数据生成
→ 软件光栅化复现 Compose 绘制语义 + 5x7 字体 + 零依赖 PNG
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON/PNG 解析
→ ArkUI 真机三 Tab 验收(折线 / 大数据 LTTB / 柱状图)
9.2 封装层次
- 模型层:上游原版
DataPoint、BarChartEntry; - 算法层:上游原版 LTTB 六件套 + 新增
MultiplatformChart.downsample换装入; - 光栅化层:
Rasterizer/Font5x7/PngEncoder把绘制语义翻译成像素与 PNG; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理三 Tab 参数状态机、JSON 拼装、PixelMap 解码与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线由库的"纯 Kotlin 含金量"决定:存储库(KStore)引擎全在纯 Kotlin,真编译整库 + 补一个 actual 即可;图表库的价值大头在 Compose 侧,必须做源码集隔离 + 绘制语义复刻。先逐文件盘点依赖度,再定"真编译 / 隔离 / 光栅化"的边界,顺序不能反;
- 绘制类 CMP 库的鸿蒙验收锚点是"语义对账"而不是"像不像":Y 轴标签在辅助线左上、X 轴标签越界收回、平滑半径等于线宽——每条规则都能在上游源码里指到出处,Demo 出的图才不是"另一个图表控件"而是"同一个库的鸿蒙渲染";
- 大数据能力要在桥的协议层就设计好:百万点不过 JSON、生成器进 KN、
downsample档位透出、in→out进状态行——上游的核心卖点(200 万点能画)在鸿蒙 Demo 里一条链路全可验收,靠的是协议设计而不是事后补 Demo。
9.4 适配成果
当前适配已完成:
- compose-multiplatform-chart
2.1.0算法内核的双 ABI Kotlin/Native 编译(上游commonMain8 个纯 Kotlin 文件零重写,唯一新增 40 行入口); - Compose 组件 ×10 的
composeMain隔离与回迁预留; - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - 复现上游绘制语义的软件光栅化器(轴标签 / 网格线 / 拐角平滑 / 渐变填充 / 数据点 / 自适应柱宽)+ 5x7 点阵字体 + 零依赖 PNG 编码;
- DevEco Stage ArkUI 三 Tab 验收页面(折线样式 / 100 万点 LTTB 对比 / 柱状图预设);
- JVM 交叉验证目标 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(提交
6a658df已推送); - 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)