开源鸿蒙平台 CMP 三方库「Shapes」适配全流程
本文记录
Shapes接入 OpenHarmony 的完整过程,覆盖形状库盘点、G2 数学内核下沉、Compose 源码集物理隔离、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、扫描线光栅化、ArkUI 真机页面、HAP 构建和真机验收。
Shapes是 Kyant0 开发的 Compose Multiplatform iOS 风格 G2 连续曲率圆角形状库:RoundedRectangle(16.dp)画出的不是普通圆角矩形,而是曲率连续(角部无"折点")的 smooth corner / squircle——每个角用三段三次贝塞尔曲线逼近,控制点由解析求解三次 / 四次方程得到。本次适配与 KStore 的"真编译整库"路线有本质区别:Shapes 的公开 API 全部是androidx.compose.ui类型,社区 CMP 1.9.2 与上游 Compose 1.12 无法混编,因此走的是 cmp_qrose 审核模式——G2 角求解器一行不改地编进鸿蒙 KLIB,10 个 Compose 文件物理隔离到src/composeMain不参与鸿蒙构建,鸿蒙侧用 Kotlin/Native 扫描线光栅化器复刻roundedRectangleOutline的全部语义,PNG + Base64 过桥,ArkUI 四 Tab 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/Shapes.git
上游仓库: Kyant0/Shapes
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Shapes 为 Compose Multiplatform 应用提供"圆角"这一件事的高级答案:RoundedRectangle 等四角圆角、UnevenRoundedRectangle 四角不等、Capsule 胶囊、Rectangle 直角,RoundedCornerStyle.Circular / Continuous 两种角样式,lerp(start, stop, fraction) 让形状在矩形 ↔ 圆角 ↔ 胶囊之间连续变形。这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套视觉能力(卡片、弹窗、图标 squircle、和 iOS 对齐的设计系统),但上游构建(Kotlin 2.4.10 / Gradle 9.7.1 / AGP 9.3.2 / Compose 1.12.0)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 的 borderRadius,页面可以显示圆角,却无法证明 G2 求解器在 OpenHarmony ARM64 设备上完成编译、链接和执行——而且 ArkUI 的圆弧角在语义上就不等于连续曲率角。适配需要同时验证几何内核、Native 动态库、C ABI、N-API、光栅化正确性、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 整库都是 Compose API | 公开类型 RoundedRectangularShape : Shape 全部站在 androidx.compose.ui 上;社区 CMP 是 1.9.2,上游是 Compose 1.12 / Kotlin 2.4,二进制不兼容。 |
| 工具链不一致 | 上游 Gradle 9.7.1 / AGP 9.3.2 与鸿蒙工具链无法共存于一个 Gradle 构建;社区钉法 HarmonyOS Kotlin 2.2.21-1.0.0 线必须降 Gradle 8.14.1。 |
| 渲染语义跨不了桥 | Compose Outline / Path 依赖平台图形后端,鸿蒙侧需要自己把贝塞尔轮廓画成像素。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:G2 数学内核的 ohos 编译、Compose 源码集物理隔离(cmp_qrose 模式)和 C ABI/N-API 桥接层。圆角语义完全由上游求解器负责,鸿蒙侧光栅化只做"轮廓 → 抗锯齿像素 → PNG"这一层。
1.2 库提供的能力
Demo 提供一个真实的形状渲染会话,对应桥接的全部入口:
- 基础形状(rounded / capsule / rectangle):
RoundedRectangle半径滑条 0…max 实时重渲,Capsule半径自动取minDimension / 2,直角矩形零圆角直接填充; - 四角定制(uneven):
UnevenRoundedRectangle四角独立半径 + 预设(左侧双角 / 上侧双角 / 对角 / 全零); - 形状插值(lerp):起点 / 终点三选一(矩形 / 圆角 / 胶囊)+ 插值比例滑条,复刻上游
LerpRoundedRectangle.corners的逐角插值与 coerce 规则; - 样式对比(compare):同参数下 Circular 与 Continuous 并排渲染,直观展示 G2 连续曲率与普通圆弧角的差别;
- corners 回显:响应里
corners字段回显光栅化前解析出的四角半径,相当于上游corners(size, layoutDirection, density)的可观测面,供验收对表。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 渲染是否成功,失败时 error 带原文。 |
kind / style | 回显实际渲染的形状与角样式。 |
width / height / bytes | 画布尺寸与 PNG 字节数。 |
corners | 解析后的四角半径 "tl,tr,br,bl"。 |
pngBase64 | 抗锯齿 RGBA PNG 的 Base64 文本。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | G2 求解器 ContinuousCurvatureRoundedRectangleCornerBuilder 原样编译,不提取、不重写;库模块唯一新增源文件是 ShapesGeometry.kt(公共入口 + 轮廓组装)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);10 个 Compose 文件 git mv 到 src/composeMain(物理隔离、不编译);上游 androidApp 示例留盘不进构建。 |
| 渲染复刻 | 光栅化器逐分支复刻 roundedRectangleOutline:全零半径→矩形、Circular 或正方形满半径→解析圆弧、其余→G2 贝塞尔轮廓。 |
| 桥接稳定 | 单个 C ABI(@CName("ShapesRender"))+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 适配后 :shapes 零第三方依赖(Compose 依赖随 composeMain 隔离一并剥离);工具链钉社区已审的 2.2.21 线。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:形状 / 四角 / 插值 / 对比 + 尺寸滑条 + 色板 + 状态行。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。Shapes 本身就是 UI 形状库,适配面天然在"几何 + 渲染";ArkTS Demo 只是把桥暴露的形状语义画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、拆分 commonMain / composeMain、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模 ── 形状请求 JSON 契约、corners 解析规则、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、扫描线光栅化、手写 PNG、C++ N-API
第 5 阶段:ArkUI 宿主 ── 四 Tab、滑条、色板、状态行、PixelMap 解码
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
shapes/src/commonMain/ G2 求解器(上游原文件,一行未改)+ ShapesGeometry 公共入口(进 ohos 构建)
shapes/src/composeMain/ 上游 10 个 Compose 文件原样保留,不进编译(RoundedRectangularShape /
RoundedRectangle / UnevenRoundedRectangle / Capsule / Rectangle /
RectangleCornerRadii / RoundedCornerStyle / Copy / Lerp / RoundedRectangleOutline)
shapes/src/commonTest/ 7 项数学不变量测试(jvmTest 运行,Ohos 测试任务全局禁用)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + Rasterizer + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
androidApp/ 上游 Android 示例,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :shapes 与 :example:nativeApp,rootProject.name = "ohos_shapes"。上游的 AGP / Compose / vanniktech 发布插件与 mavenPublishing 块全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉死) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.7.1 必须降级:2.2.21 旧线不支持 Gradle 9.x) |
| 依赖 | :shapes 零第三方依赖(compose-ui 随 composeMain 隔离剥离) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.shapes |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 文件 | 依赖形态 | 处置 |
|---|---|---|
ContinuousCurvatureRoundedRectangleCornerBuilder.kt | 纯 kotlin.math(acos / cbrt / cos / sin / sqrt / tan),零 Compose | 留在 commonMain(编 ohos klib) |
RoundedRectangleOutline.kt | Outline / Path / RoundRect / fastCoerceIn | git mv → composeMain |
RoundedRectangularShape.kt 等 9 个 API 文件 | Shape / Dp / Density / LayoutDirection / @Immutable | git mv → composeMain |
上游 androidApp 示例 | com.android.application 全家桶 | 留盘不进构建 |
关键判断:全库 12 个源文件里只有求解器不碰 Compose,但渲染语义(roundedRectangleOutline 的三分支规则、四角镜像、tW/tH coerce)必须有一个非 Compose 副本才能在鸿蒙侧复刻。因此库模块新增唯一源文件 ShapesGeometry.kt:暴露角控制点公共入口(上游求解器是 internal,同 qrose 给 internal 编码器补 Qrose.encodeQr 的手法),并实现 continuousOutlineSegments——把四角贝塞尔链组装成像素级闭合轮廓,角间直边编码为退化三次贝塞尔,返回单条连续链。这份组装逻辑与 composeMain 里的 continuousCurvatureRoundedRectanglePath 逐行对应,且能被 JVM 测试覆盖。
1.4 页面边界
标题区 Shapes · OpenHarmony / 副标题(G2 连续曲率圆角 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 基础形状 / 四角定制 / 形状插值 / 样式对比
基础形状 矩形 / 圆角矩形 / 胶囊 chips + 角样式 chips(Continuous / Circular)+ 圆角滑条
四角定制 四角预设 chips + 四条角半径滑条 + 角样式
形状插值 起点 / 终点 chips + 插值比例滑条 + 端点圆角滑条
样式对比 圆角滑条 + 左右并排双图(Circular | Continuous)
公共区 宽 / 高滑条、五组色板、生成按钮、渲染图(ImageFit.Contain)
状态行 生成结果(kind / style / size / png 字节数 / corners 回显)
页面启动时 aboutToAppear 直接发起默认参数渲染(420x420、r=120、continuous),真机装上即出图。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
Shapes 的价值全在自研几何算法,不依赖任何 kotlinx 制品——不需要像 KStore 那样先验证 coroutines / serialization 的 ohos 制品线,直接套用社区已审的钉法:HarmonyOS Kotlin 2.2.21-1.0.0。这条旧线的要求是 Gradle 8.14.1(上游 wrapper 是 9.7.1,必须降级)、JDK 21、三个 native 开关,并删除上游的 gradle-daemon-jvm.properties(daemon toolchain 钉法与 org.gradle.java.home 钉法二选一):
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=1.2.1-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
kotlin.apple.deprecated.allowUsingOpenSourceCompiler=true
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_shapes"
include(":shapes")
include(":example:nativeApp")
version catalog 裁剪到只剩一个插件条目:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译几何内核
shapes/build.gradle.kts 收敛为纯 KMP 模块,commonMain.dependencies 为空——这是源码集隔离的直接收益:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
.\gradlew.bat :shapes:compileKotlinJvm :shapes:compileKotlinOhosArm64 :shapes:compileKotlinOhosX64 # BUILD SUCCESSFUL
求解器本体一行未动;composeMain 只是一个不被编译的普通目录(物理隔离,将来 CMP 官方支持 ohos 时可整体接回)。
2.3 测试任务的处置
根构建全局禁用 ohos 测试任务(社区工具链测试任务不可用),测试统一在 JVM 跑:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
2.4 sharedLib 只放桥模块
库模块不开 binaries.sharedLib(避免每个模块都带一份 Kotlin/Native 运行时),example/nativeApp 独占动态库出口,baseName = "ohosshapes",并用 Copy 任务把链接产物连同对应 triple 的 libc++_shared.so 一起回填 harmonyApp/entry/libs/<abi>/,finalizedBy 挂在 linkDebugSharedOhos* 上自动触发。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.kyant.shapes/**(上游 G2 求解器原样编译 + ShapesGeometry 轮廓组装)
↓ cornerBezierPoints(tW, tH) / continuousOutlineSegments
Bridge.kt(@CName × 1,扁平 JSON 进)
↓ UTF-8 JSON
Rasterizer(扫描线抗锯齿)→ PngEncoder(zlib stored PNG)
↓ Base64
N-API shapesRender()
↓
ArkUI Index.ets(四 Tab 滚动页)
请求与响应:
请求 : {"kind":"rounded","width":420,"height":420,"cornerRadius":120,
"style":"continuous","fg":"#F59E0B","bg":"#FFFBEB"}
{"kind":"uneven","radii":"[160,0,0,160]", ...}
{"kind":"lerp","lerpFrom":"rectangle","lerpTo":"capsule","fraction":0.5, ...}
响应 : {"ok":true,"kind":"rounded","style":"continuous","width":420,"height":420,
"bytes":706138,"corners":"120,120,120,120","pngBase64":"iVBORw0KG..."}
{"ok":false,"error":"..."}
三个设计点:radii 是四元数组的字符串形式("[tl,tr,br,bl]",MiniJson 解析后手工切分);响应的 pngBase64 直接可喂给 util.Base64Helper;corners 回显光栅化前 coerce 的结果——圆角滑条顶满时四角会被 minDimension / 2 截住,回显让页面能对表验证语义。
3.2 corners 解析规则(与上游一一对应)
rectangle -> 全 0
capsule -> 全部 min(w, h) / 2
rounded -> cornerRadius 逐角 coerceIn(0, min(w, h) / 2)
uneven -> radii 逐角 coerceIn(0, min(w, h) / 2)
lerp -> 端点先各自解析,再逐角线性插值,最后再 coerce // 对应 LerpRoundedRectangle
角求解的输入参数复刻上游内部公式:
tW = ((width * 0.5 - r) / r).coerceIn(0, 1)
tH = ((height * 0.5 - r) / r).coerceIn(0, 1)
p = ShapesGeometry.cornerBezierPoints(tW, tH) // 20 个数:10 个控制点
tW / tH 是"剩余空间分数":1 是独立角,0 是胶囊类退化角(四角已顶满短边)。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ShapesRender")
fun shapesRender(request: String): String = try {
/* 解析请求 → 解析四角 → 光栅化 → PNG → Base64 拼响应 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、字段越界(尺寸 coerce 到 16…1024)、颜色格式错、数组残缺,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libshapes_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosshapes.so)
│ ShapesRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又表达不了轮廓,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("ShapesRender")
fun shapesRender(request: String): String // 全部形状一口进
| C ABI 符号 | ArkTS 方法 | 覆盖语义 |
|---|---|---|
ShapesRender | shapesRender(req) | 五种形状 + 两种角样式 + 尺寸 / 色板 / 插值全参数渲染 |
4.3 扫描线光栅化:把 Compose Outline 语义画回像素
Rasterizer.shape 逐分支复刻上游 roundedRectangleOutline:
val polygon = when {
// 全零半径 → 矩形
topLeft == 0.0 && topRight == 0.0 && bottomRight == 0.0 && bottomLeft == 0.0 ->
listOf(doubleArrayOf(0.0, 0.0), doubleArrayOf(w, 0.0), doubleArrayOf(w, h), doubleArrayOf(0.0, h))
// Circular,或正方形且四角 ≥ maxRadius → 四分之一圆弧(RoundRect 语义)
style == CornerStyle.CIRCULAR || (w == h && 全部半径 >= maxRadius) ->
circularPolygon(w, h, topLeft, topRight, bottomRight, bottomLeft)
// 其余 → G2 贝塞尔轮廓(ShapesGeometry.continuousOutlineSegments)
else ->
flatten(ShapesGeometry.continuousOutlineSegments(w, h, topLeft, topRight, bottomRight, bottomLeft))
}
三种轮廓汇入同一条抗锯齿管线:
- 展平:每段三次贝塞尔按控制多边形长度以 ~2px 步长自适应细分(6…48 段);圆弧按
r / 2步长(8…48 段); - 扫描线:每像素行取 4 条子行,子行与多边形求交(半开区间规则保证奇偶一致性),交点排序后逐 span 填充;像素覆盖率 = span 与像素的重叠长度——行内是精确面积采样,不是 0/1 抖动;
- 合成:
out = bg · (1 − c · α) + fg · c · α逐通道线性混合(fg 支持#AARRGGBB透明度)。
一个必须理解的设计细节:角链最后一个控制点 y9 = 1 + k(k = 2/3 · 剩余分数)有意落在直边延长线上——这正是 G2 曲率无折点并入直边的机制。光栅化对超出单位框的控制点无需特判,展平后的曲线本身在包围盒内(y9 · r ≤ 5/3 · minDim/2 < minDim 恒成立)。
4.4 手写 PNG 编码器
PngEncoder 用 zlib stored(未压缩)DEFLATE 块 + adler32 / crc32 + IHDR/IDAT/IEND 组块,零第三方依赖。420x420 RGBA 的输出约 690KB(真机状态行实测 png=706138B),Base64 后约 941KB,N-API 字符串传输无压力。C ABI 侧不引入任何压缩库,签名永远稳定。
4.5 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"shapesRender", nullptr, NapiShapesRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosshapes_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosshapes_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.6 CMake 和类型声明
add_library(ohosshapes SHARED IMPORTED)
set_target_properties(ohosshapes PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(shapes_napi SHARED napi_init.cpp)
target_link_libraries(shapes_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosshapes)
OHOS_ARCH 为空或 libohosshapes.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const shapesRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 组装请求和触发渲染
private buildBody(styleOverride: string): string {
let kind: string = this.shape;
if (this.tab === 'uneven') {
kind = 'uneven';
} else if (this.tab === 'lerp') {
kind = 'lerp';
}
const body: Record<string, Object> = {
'kind': kind,
'style': styleOverride.length > 0 ? styleOverride : this.style,
'width': this.boxWidth,
'height': this.boxHeight,
'fg': this.fg,
'bg': this.bg
};
if (this.tab === 'uneven') {
body['radii'] = `[${this.rTL},${this.rTR},${this.rBR},${this.rBL}]`;
} else if (this.tab === 'lerp') {
body['lerpFrom'] = this.lerpFrom;
body['lerpTo'] = this.lerpTo;
body['fraction'] = this.fraction;
body['cornerRadius'] = this.radius;
} else {
body['cornerRadius'] = this.radius;
}
return JSON.stringify(body);
}
chips / 色板 / Tab 切换即时重渲;滑条(圆角、四角、插值、宽高)只更新 @State,点"生成"按钮统一触发——避免拖动过程中每帧过一次 N-API。样式对比 Tab 一次发两个请求(circular 与 continuous 强制覆写),左右并排显示。
5.2 Base64 到 PixelMap
private decodePng(b64: string, slot: number): void {
const helper = new util.Base64Helper();
const bytes: Uint8Array = helper.decodeSync(b64);
const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const src = image.createImageSource(sliced);
src.createPixelMap().then((pm: image.PixelMap) => {
/* 按 slot 写入 pixelMap / cmpLeft / cmpRight,刷新状态行 */
});
}
注意 bytes.buffer.slice(...) 一步是为了拿纯净 ArrayBuffer(decodeSync 返回的 Uint8Array 可能是视图)。
5.3 状态行与错误边界
渲染成功后状态行显示 生成成功 kind=rounded style=continuous 420x420 png=706138B 与 corners(tl,tr,br,bl)=120,120,120,120;ok:false 时错误原文进状态行;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行并打 hilog。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosshapes)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName + MiniJson(带括号深度追踪)
│ ├── Rasterizer.kt 扫描线抗锯齿光栅化
│ └── PngEncoder.kt zlib stored PNG
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.shapes、versionName 1.2.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/libohosshapes_api.h
├── src/main/cpp/types/libshapes_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
.\gradlew.bat :shapes:jvmTest # 7 项数学不变量
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example\harmonyApp
hvigorw assembleHap --mode module -p product=default
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
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.shapes -m entry
四、完整代码对照
4.1 整体架构
com.kyant.shapes(上游 G2 求解器原样编译)
│ solveCubicSingle / solveDepressedQuarticSingle(三次 / 四次方程闭式解 + 缓存)
ShapesGeometry(唯一新增库源文件)
│ cornerBezierPoints / continuousOutlineSegments(四角镜像 + 闭合链)
Bridge.kt + MiniJson
│ ShapesRender:resolveRadii(coerce / lerp 规则)→ JSON 分发
Rasterizer + PngEncoder
│ 扫描线抗锯齿 → zlib stored PNG → Base64
libshapes_napi.so
│ shapesRender
Index.ets
│ JSON parse + Base64Helper + createPixelMap
▼
ArkUI 真机页面(四 Tab 滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_shapes、androidApp 移出 |
gradle/libs.versions.toml | 只剩 HarmonyOS Kotlin 2.2.21-1.0.0 一个插件条目 |
build.gradle.kts(根) | 版本 1.2.1-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 21 钉死、native 三开关、删 configuration-cache |
shapes/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
shapes/src/commonMain/.../ShapesGeometry.kt | 唯一新增库源文件(公共入口 + 轮廓组装) |
shapes/src/commonTest/.../ShapesGeometryTest.kt | 7 项数学不变量测试 |
shapes/src/composeMain/(10 个文件) | 上游 Compose API 物理隔离,内容零改动 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt / Rasterizer.kt / PngEncoder.kt | 1 个 C ABI、渲染规则复刻、错误通道 |
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 | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 求解器 | getCornerBezierPoints(tH, tV) | 单角 10 控制点(上游 internal,经 ShapesGeometry 对外) |
| 求解器 | solveCubicSingle / solveDepressedQuarticSingle | G2 角闭式解(等角 / 不等角两条路) |
| 几何 | ShapesGeometry.cornerBezierPoints(w, h, r) | 剩余空间分数 coerce 后取控制点 |
| 几何 | ShapesGeometry.continuousOutlineSegments(...) | 四角镜像 + 角间直边 → 像素级闭合贝塞尔链 |
| 渲染 | Rasterizer.shape(w, h, radii, style, fg, bg) | 三分支轮廓 → 扫描线 AA → PNG |
| Native | ShapesRender | C ABI JSON 导出(唯一符号) |
| N-API | shapesRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | generate() / buildBody() | 组装请求、分发渲染、刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const raw: string = shapesRender(JSON.stringify({
kind: 'rounded', width: 420, height: 420, cornerRadius: 120,
style: 'continuous', fg: '#F59E0B', bg: '#FFFBEB'
}));
const res = JSON.parse(raw);
// res.ok === true; res.corners === '120,120,120,120'; res.pngBase64 → PixelMap
val radii = resolveRadii(kind, json, width, height) // coerce / lerp 规则
val image = Rasterizer.shape(width, height, radii, style, fg, bg)
// image.png → Base64 → {"ok":true, "corners":"120,120,120,120", "pngBase64":"..."}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、轮廓数组或位图裸数据。
五、关键决策说明
决策 1:数学内核下沉 + Compose 物理隔离,而不是等 CMP 或重写
整库都是 Compose API 是本次适配的最大障碍,也是路线选择的依据:硬接社区 CMP 1.9.2 等于重写;ArkTS 翻译数学是负价值(查重视角)。下沉求解器(唯一不碰 Compose 的文件)+ git mv 物理隔离其余 10 个文件,既保住"上游算法原样复用"的底线,又让 composeMain 在将来 CMP 官方支持 ohos 时可整体接回。
决策 2:工具链钉社区已审的 2.2.21 线,Gradle 降 8.14.1
Shapes 零第三方依赖,不需要像 KStore 那样被 kotlinx 制品线反推到 2.4.20;直接复用 qrose / haze 批准过的钉法(Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21 + native 三开关),审核成本最低。两条工具链线的经验不能互相套用:2.4.20 线可留 Gradle 9.x,2.2.21 线必须降 8.14.1。
决策 3:渲染语义在 KN 侧复刻,而不是 ArkTS 侧画
把 roundedRectangleOutline 的三分支规则、四角镜像、tW/tH coerce 复刻在 Rasterizer + ShapesGeometry 里,ArkTS 只拿到最终 PNG。好处有三:几何复用上游求解器(不是重写)、渲染正确性可以在 JVM 上交叉验证(同一份代码)、ArkTS 集成面最小(一个函数一个 Base64)。
决策 4:单 C ABI + corners 回显
一个 ShapesRender 覆盖全部形状参数,加字段不用改 N-API / 头文件;响应回显 corners 把 coerce 后的真实半径透出,让"语义没漂"在页面上可验证——相当于把上游 corners(...) 变成可观测面。
决策 5:jvmTest 数学不变量 + JVM 渲染等价交叉验证
上游 Shapes 没有测试目录,"内核没漂"的闸门要自己造:常驻 7 项数学不变量(边界 / 单调 / 闭合链 / 退化角),加上一次性 JVM 渲染等价验证(把 Rasterizer / PngEncoder 临时复制进 commonTest,用 ImageIO 解码 PNG 断言像素:中心前景、远角背景、角部斜线存在混合像素、胶囊端盖、圆弧内外点)——ohos 与 JVM 跑的是同一份渲染代码,JVM 绿即功能层绿。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64,Git Bash / PowerShell)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与 HarmonyOS 真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :shapes:jvmTest
7 项用例全部通过:
| 用例 | 钉住什么 |
|---|---|
cornerPointsHaveTwentyFiniteValues | 四种剩余分数组合下控制点均为 20 个有限值 |
cornerPointsStayWithinEdgeOvershoot | x ∈ [−1, 1]、y ∈ [0, 5/3](含 y9 = 1 + k 设计性越界)、链单调 |
oversizedRadiusIsCoercedToDegenerateRoom | 半径超界时 coerce 不产生 NaN / Inf |
capsuleRoomFractionProducesFiniteCorners | 胶囊退化角(tH = 0)有限 |
outlineSegmentsFormOneClosedChain | 四种半径组合下轮廓链连续且闭合 |
outlineStaysInsideTheBoundingBox | 混合半径轮廓全部点在包围盒内 |
zeroRadiiCollapseToPlainRectangleEdges | 全零半径退化为 4 条矩形边 |
渲染等价验证(方法存档于 README.OpenHarmony_CN.md 第 22 节):临时把 Rasterizer.kt / PngEncoder.kt 复制进 commonTest,jvmTest 里 ImageIO.read 解码并断言 5 组像素语义全数通过后移除副本。
6.3 产物与符号验证
:shapes:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ShapesRender / libohosshapes_symbols 均为全局 T 符号(双 ABI)
| 产物 | 位置 | 大小 |
|---|---|---|
libohosshapes.so (arm64) | entry/libs/arm64-v8a/ | 3,798,408 B |
libohosshapes.so (x86_64) | entry/libs/x86_64/ | 3,500,440 B |
libc++_shared.so | 同级目录 | 1.85 MB / 1.73 MB |
6.4 真机功能用例
- DevEco 自动签名后安装 HAP,启动即渲染默认参数:基础形状 Tab、圆角矩形、Continuous 样式、420x420、r=120(见第七节截图);
- 状态行显示
生成成功 kind=rounded style=continuous 420x420 png=706138B、corners(tl,tr,br,bl)=120,120,120,120——PNG 字节数与 zlib stored 体积量级吻合,corners 回显与滑条一致; - 圆角滑条 0 → 210(max):0 时为直角矩形,顶满时四角被 coerce 到 210(
minDimension / 2),corners 回显随之变化; - 切换 Circular 样式:角部出现折点;切回 Continuous 折点消失——G2 语义在真机可感知;
- 胶囊:半径自动取
minDimension / 2,端盖饱满无棱; - 四角定制:预设"左侧双角"仅左侧两角圆,四滑条独立生效;
- 形状插值:fraction = 0 与端点形状一致、= 1 与终点一致,中间值逐角平滑变形;
- 样式对比:同参数左右并排,右图(Continuous)角部更鼓、无折点;
- 色板五组切换,前景 / 背景即时重渲;
- 边缘放大观察无锯齿(4 子行精确面积采样的抗锯齿坡连续)。
6.5 验证结论
G2 求解器、轮廓组装、光栅化、PNG 编码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 全部功能可用,默认参数渲染与状态行回显符合预期(见下节效果图)。
七、运行效果
7.1 真机截图
7.2 命令速查
git clone https://atomgit.com/oh-tpc/Shapes.git
cd Shapes
export JAVA_HOME="/path/to/jdk-21" # Windows: set JAVA_HOME=C:\...
./gradlew.bat :shapes:compileKotlinOhosArm64 :shapes:compileKotlinOhosX64 # klib
./gradlew.bat :shapes:jvmTest # 7 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap --mode module -p product=default
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.shapes -m entry
hdc shell hilog | grep Shapes
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:shapes真正编译成ohosArm64/ohosX64并链接出动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin/Native 没有
java.lang.Math:光栅化器里写Math.PI在 JVM 能过、ohos 编译直接Unresolved reference 'Math'——统一用kotlin.math.PI顶层常量; - 角链控制点
y9 = 1 + k落在单位框外是 G2 设计(曲率并入直边),不是 bug:数学测试的边界断言必须写到 5/3,第一次按 [0, 1] 写就误报了y out of range: 1.096...; - qrose 的 MiniJson 平铺解析只对字符串豁免逗号,
radii: "[160,0,0,160]"这类数组值会被切碎——本仓加了[/{深度追踪后数组才能整体作为值; org.gradle.java.home只管 daemon,不管gradlew.bat启动器:命令行跑构建前必须先设JAVA_HOME,否则启动器直接报 JAVA_HOME is not set;- 2.2.21 旧线必须 Gradle 8.14.1:上游 wrapper 9.7.1 原样沿用会配置期失败;同时删掉
gradle-daemon-jvm.properties(daemon toolchain 与 java.home 钉法冲突)并关掉 configuration-cache; - DevEco 打开工程后会在
example/harmonyApp下生成.idea/、.clangd、.clang-tidy,上游根.gitignore的/build/、.idea/是根锚定模式盖不住嵌套目录——必须单独追加 gitignore,否则混进提交(本仓提交前实际拦下了这两个文件); build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不强引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); entry/libs/下的 so 是构建产物,走 gitignore 不入库——换机器克隆后必须先跑 link 任务再开 DevEco,CMake 里专门写了 FATAL_ERROR 防呆提示。
8.2 已知问题
- 鸿蒙侧输出是位图(PNG),不参与 ArkUI 主题 / 深色模式 / 无障碍的响应式重排;
UnevenRoundedRectangle的 RTL 镜像(上游layoutDirection分支)在桥协议按 LTR 处理,需要 RTL 时由调用方交换radii前两个元素;- 上游
copy(cornerRadius = ...)修饰符族未纳入桥协议(语义可由radii+cornerRadius组合表达); - PNG 为 zlib stored 未压缩,4 位数百万像素级画布会到 MB 级,需要小图时控制
width/height; fg支持透明度(#AARRGGBB),bg按不透明处理;- android / js / wasmJs / apple 目标剥离(源文件留在树里),发布插件一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.shapes匹配的签名。
8.3 未来优化方向
- 把扫描线行级并行化(当前单线程 512px 级别毫秒级,真机实测无感,先留数据再决定);
- 桥协议补
copy(...)修饰符语义与描边(stroke)参数; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和
hdc uitest自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 shapes(1 个纯数学文件 + 10 个 Compose 文件)
→ 源码集拆分:求解器留 commonMain,Compose API git mv → composeMain
→ 工具链钉 2.2.21-1.0.0 线(Gradle 降 8.14.1 + JDK 21 + native 三开关)
→ ShapesGeometry 公共入口 + 轮廓组装(唯一新增库源文件)
→ ohosArm64 / ohosX64 KLIB + nativeApp 双 ABI 动态库
→ C ABI(@CName × 1)+ corners / lerp 语义解析
→ 扫描线抗锯齿光栅化 + 手写 PNG(zlib stored)
→ C++ N-API(DisposeString 纪律)
→ ArkTS Base64 → PixelMap
→ ArkUI 真机四 Tab 验收(形状 / 四角 / 插值 / 对比)
9.2 封装层次
- 求解器层:上游原版三次 / 四次方程闭式解、控制点缓存、等角 / 不等角两条路;
- 几何层:
ShapesGeometry负责剩余分数 coerce、四角镜像与闭合轮廓组装; - 渲染层:
Rasterizer复刻roundedRectangleOutline三分支语义,扫描线精确面积抗锯齿; - 编码层:
PngEncoder零依赖输出 RGBA PNG; - Native 层:双 ABI 动态库通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、请求组装、Base64 解码与展示;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线由库的依赖形态决定:KStore 全库 commonMain 且 kotlinx 制品线齐备,正确答案是"真编译整库 + 补 8 行 actual";Shapes 整库是 Compose API,正确答案是"数学内核下沉 + Compose 物理隔离 + 渲染复刻"。同一条 cmp_qrose 六步骨架(查重 → 钉工具链 → 拆源码集 → 加目标 → 桥接 → Demo)在两种形态下都成立,但每一步的内涵完全不同——路线必须基于库本身的结构,不能拿上一仓的结论直接套。
- 工具链版本跟着钉法线走:零第三方依赖的库直接复用社区已审钉法(2.2.21 线 = Gradle 8.14.1 + JDK 21 + 三开关),依赖重的库才需要被制品线反推(KStore 的 2.4.20 线)。两条线的 Gradle 兼容区间不同,经验不能互相套用。
- 没有上游测试就要自己造交叉验证:7 项数学不变量钉住求解器行为,JVM 渲染等价(ImageIO 解码断言像素)钉住光栅化行为——ohos 与 JVM 跑同一份代码,JVM 绿即功能层绿。渲染类库的验收重点是"角部语义与边缘抗锯齿",与存储类的"重启后数据还在"相对,闸门要造在各自的风险面上。
9.4 适配成果
当前适配已完成:
- Shapes
1.2.1G2 求解器的双 ABI Kotlin/Native 编译(上游求解器零改动,库模块唯一新增ShapesGeometry.kt); - 10 个 Compose 文件的物理隔离(
src/composeMain,内容零改动,可随 CMP 演进整体接回); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - 扫描线精确面积抗锯齿光栅化 + 零依赖 PNG 编码(
roundedRectangleOutline三分支语义逐行复刻); - DevEco Stage ArkUI 四 Tab 验收页面(基础形状 / 四角定制 / 形状插值 / 样式对比 + 状态行回显);
- 7 项 JVM 数学不变量 + JVM 渲染等价交叉验证全绿 + HAP 构建、签名、真机安装与运行验证(默认参数渲染 420x420、
png=706138B、corners=120,120,120,120,见效果图); - 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)