本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 钉死)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 9.7.1 必须降级:2.2.21 旧线不支持 Gradle 9.x)
依赖:shapes 零第三方依赖(compose-ui 随 composeMain 隔离剥离)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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.ktOutline / Path / RoundRect / fastCoerceIngit mv → composeMain
RoundedRectangularShape.kt 等 9 个 API 文件Shape / Dp / Density / LayoutDirection / @Immutablegit 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 方法覆盖语义
ShapesRendershapesRender(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.propertiesJDK 21 钉死、native 三开关、删 configuration-cache
shapes/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零依赖
shapes/src/commonMain/.../ShapesGeometry.kt唯一新增库源文件(公共入口 + 轮廓组装)
shapes/src/commonTest/.../ShapesGeometryTest.kt7 项数学不变量测试
shapes/src/composeMain/(10 个文件)上游 Compose API 物理隔离,内容零改动
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt / Rasterizer.kt / PngEncoder.kt1 个 C ABI、渲染规则复刻、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(24 节)

4.3 关键 API 对照

层次API作用
求解器getCornerBezierPoints(tH, tV)单角 10 控制点(上游 internal,经 ShapesGeometry 对外)
求解器solveCubicSingle / solveDepressedQuarticSingleG2 角闭式解(等角 / 不等角两条路)
几何ShapesGeometry.cornerBezierPoints(w, h, r)剩余空间分数 coerce 后取控制点
几何ShapesGeometry.continuousOutlineSegments(...)四角镜像 + 角间直边 → 像素级闭合贝塞尔链
渲染Rasterizer.shape(w, h, radii, style, fg, bg)三分支轮廓 → 扫描线 AA → PNG
NativeShapesRenderC ABI JSON 导出(唯一符号)
N-APIshapesRender(req)向 ArkTS 暴露能力
ArkTSgenerate() / 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 个有限值
cornerPointsStayWithinEdgeOvershootx ∈ [−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 真机功能用例

  1. DevEco 自动签名后安装 HAP,启动即渲染默认参数:基础形状 Tab、圆角矩形、Continuous 样式、420x420、r=120(见第七节截图);
  2. 状态行显示 生成成功 kind=rounded style=continuous 420x420 png=706138B、corners(tl,tr,br,bl)=120,120,120,120——PNG 字节数与 zlib stored 体积量级吻合,corners 回显与滑条一致;
  3. 圆角滑条 0 → 210(max):0 时为直角矩形,顶满时四角被 coerce 到 210(minDimension / 2),corners 回显随之变化;
  4. 切换 Circular 样式:角部出现折点;切回 Continuous 折点消失——G2 语义在真机可感知;
  5. 胶囊:半径自动取 minDimension / 2,端盖饱满无棱;
  6. 四角定制:预设"左侧双角"仅左侧两角圆,四滑条独立生效;
  7. 形状插值:fraction = 0 与端点形状一致、= 1 与终点一致,中间值逐角平滑变形;
  8. 样式对比:同参数左右并排,右图(Continuous)角部更鼓、无折点;
  9. 色板五组切换,前景 / 背景即时重渲;
  10. 边缘放大观察无锯齿(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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :shapes 真正编译成 ohosArm64 / ohosX64 并链接出动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin/Native 没有 java.lang.Math:光栅化器里写 Math.PI 在 JVM 能过、ohos 编译直接 Unresolved reference 'Math'——统一用 kotlin.math.PI 顶层常量;
  4. 角链控制点 y9 = 1 + k 落在单位框外是 G2 设计(曲率并入直边),不是 bug:数学测试的边界断言必须写到 5/3,第一次按 [0, 1] 写就误报了 y out of range: 1.096...;
  5. qrose 的 MiniJson 平铺解析只对字符串豁免逗号,radii: "[160,0,0,160]" 这类数组值会被切碎——本仓加了 [ / { 深度追踪后数组才能整体作为值;
  6. org.gradle.java.home 只管 daemon,不管 gradlew.bat 启动器:命令行跑构建前必须先设 JAVA_HOME,否则启动器直接报 JAVA_HOME is not set;
  7. 2.2.21 旧线必须 Gradle 8.14.1:上游 wrapper 9.7.1 原样沿用会配置期失败;同时删掉 gradle-daemon-jvm.properties(daemon toolchain 与 java.home 钉法冲突)并关掉 configuration-cache;
  8. DevEco 打开工程后会在 example/harmonyApp 下生成 .idea/、.clangd、.clang-tidy,上游根 .gitignore 的 /build/、.idea/ 是根锚定模式盖不住嵌套目录——必须单独追加 gitignore,否则混进提交(本仓提交前实际拦下了这两个文件);
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不强引用签名,勾选自动签名后由 IDE 回填完整配置;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 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 三条经验

  1. 适配路线由库的依赖形态决定:KStore 全库 commonMain 且 kotlinx 制品线齐备,正确答案是"真编译整库 + 补 8 行 actual";Shapes 整库是 Compose API,正确答案是"数学内核下沉 + Compose 物理隔离 + 渲染复刻"。同一条 cmp_qrose 六步骨架(查重 → 钉工具链 → 拆源码集 → 加目标 → 桥接 → Demo)在两种形态下都成立,但每一步的内涵完全不同——路线必须基于库本身的结构,不能拿上一仓的结论直接套。
  2. 工具链版本跟着钉法线走:零第三方依赖的库直接复用社区已审钉法(2.2.21 线 = Gradle 8.14.1 + JDK 21 + 三开关),依赖重的库才需要被制品线反推(KStore 的 2.4.20 线)。两条线的 Gradle 兼容区间不同,经验不能互相套用。
  3. 没有上游测试就要自己造交叉验证:7 项数学不变量钉住求解器行为,JVM 渲染等价(ImageIO 解码断言像素)钉住光栅化行为——ohos 与 JVM 跑同一份代码,JVM 绿即功能层绿。渲染类库的验收重点是"角部语义与边缘抗锯齿",与存储类的"重启后数据还在"相对,闸门要造在各自的风险面上。

9.4 适配成果

当前适配已完成:

  • Shapes 1.2.1 G2 求解器的双 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐