本文记录 SaltUI 接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的内核盘点、鸿蒙工具链钉版、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI / N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

SaltUI 是从音乐播放器 Salt Player 孵化出的 Compose Multiplatform UI 组件库(上游 3.0.0-beta02,Apache-2.0),覆盖主题系统(SaltTheme / SaltColors / SaltPalette / SaltDimens)、Item / Button / Dialog / TitleBar 等组件、lazy 列表 / 网格 / 瀑布流 / Pager,以及一套完整的 Sheet 底部弹层(detent 档位、拖拽吸附、独立关闭入口、状态保留)。它不是存储库那样的"零 UI 内核"——主库 ui2 的 216 个 commonMain 文件里 196 个深依赖 Compose。因此本次适配与 KStore 的"真编译整库"是相反方向的同一套方法论:把不依赖 UI 框架的那部分代码先编译到鸿蒙上跑起来——OS 探测、主题 token、Sheet detent 几何与拖拽 settle 规则,与 Android / iOS / 桌面保持同一份源码;ArkUI 页面作为验收宿主。Compose 组件本体冻结在非编译源码集 composeMain 中,等待鸿蒙侧 Compose 工具链与上游 CMP 1.12 线对齐后整体接回。

项目地址: AtomGit/oh-tpc/SaltUI.git

上游仓库: Moriafly/SaltUI

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

SaltUI 为 Kotlin Multiplatform 应用提供一整套视觉语言:SaltTheme 给出 11 个颜色 token 与明暗动态配对,SaltDimens 按平台分档(Android 56/24dp、iOS 52/22dp、桌面 32/20dp),core 模块的 OS.current 是这些分档的唯一开关;Sheet 组件在 Compose 之上实现了 detent 档位模型(Content / Medium / Large / Height / Fraction)、几何解析(inset 钳制、最小高度、等高档去重、就近偏高选择)与拖拽松手规则(位移关闭阈值、甩动关闭速度、方向越级、就近吸附)。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套设计语言,但上游构建(Kotlin 2.4.20 / Gradle 9.3.1 / CMP 1.12.1 / AGP 9.1.1)不会生成 OpenHarmony 变体。

把一个 UI 库带到鸿蒙,比存储库多出一堵墙:渲染层整体不可编译。鸿蒙社区 CMP 停在 1.9.2 线,与上游 CMP 1.12 / Kotlin 2.4 无法共存——这正是 cmp_qrose 适配条码库时遇到并记录过的同一堵墙。如果只把示例页面改成 ArkTS,页面可以显示数值,却无法证明库的逻辑模型在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证内核逻辑、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
渲染层断层196/216 个 commonMain 文件深依赖 Compose(组件、SaltTheme、Foundation 级 lazy 拷贝),鸿蒙侧没有 1.12 线 CMP,任何 androidx.compose import 都编不进 ohos target。
类型绑定 UI 框架主题 token 是 Color、尺寸是 Dp、几何解析吃 Density——都住在 androidx.compose.ui 里。
AGP / 发布插件拖累上游根构建挂 AGP 9.1.1、vanniktech 发布、kover,与鸿蒙工具链无法共存于一个 Gradle 构建。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交互语义跨不了桥拖拽速度、档位吸附是 Kotlin 侧算法,C ABI 只能同步一来一回。

因此,本项目把边界放在:不依赖 Compose 的内核(OS 探测、主题 token、Sheet 几何与 settle 规则)的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI / N-API 桥接层。数值与算法语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和渲染。

1.2 库提供的能力

Demo 提供一个真实的组件内核会话,对应桥接的全部入口:

  • 版本握手:version op 返回 fork 版本 3.0.0-beta02-ohos.1 与上游版本 3.0.0-beta02,来自真实 Native 调用而非静态文字;
  • 平台探测:os op 返回 OS.current 序列化,ohos 上是 {"kind":"harmonyos","apiVersion":0}(纯 Kotlin/Native 无法不经 NAPI 查询 API 版本,宿主可用系统 API 自行展示真实值);
  • 主题 token:theme op 按明暗与平台(鸿蒙 / Android / iOS / 桌面)返回 11 个颜色 token(#AARRGGBB,ArkTS 可直用)、12 个语义色与 8 项尺寸 token(dp 值 + density 换算 px);
  • Sheet 几何:sheet/geometry op 把 detent 声明解析成吸附点列表(px + dp 双显)与初始选中档;
  • 拖拽 settle:sheet/settle op 按上游 SheetTransitionState.settle 的同一套规则判定松手后关闭或落档;
  • 显示模式:sheet/display-mode op 以 600x480dp 阈值判定 Auto 模式下底部吸附还是居中浮层。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op 相关载荷每个操作各自的返回体(见第三节桥协议)。
统一错误包任意异常都以 {"ok":false,"error":"..."} 返回,Kotlin 异常永不跨 FFI。

内核侧完整能力(11 token 的明暗动态配对、sRGB 源覆盖混合、四平台尺寸分档、detent 几何解析、settle 四规则)全部在上游源码的抽取件里随编译进入 .so,JVM 侧保留 23 项 desktopTest 交叉验证用例,证明 ohos .so 里执行的逻辑没有漂移。

1.3 实现适配

维度要求
代码复用内核 19 个纯 Kotlin 文件与 Android / iOS / 桌面同一份源码:只做类型替换(Color→Long ARGB、Dp→Float)与注解剥离,数值与算法语义零改动,由 desktopTest 钉死。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 文件、平台 actual、资源与原测试整体迁入 composeMain 存档不编译;上游示例 app 与 ui-navigation、salt-adaptive-window 留盘不进构建。
桥接稳定单个 @CName("SaltUiDispatch") C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版);桥零依赖(手写 JSON 解析器)。
UI 完整单页覆盖全部桥接入口:三 Tab(主题 token 跨平台对比、按内核 settle 规则吸附的可拖拽 Sheet、版本握手与分层说明)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。SaltUI 的价值九成在 Compose 渲染层,那部分在鸿蒙侧没有对应物可编译;ArkTS Demo 呈现的是与 Android / iOS / 桌面同一份源码编译出的内核数值。

二、实现路线图

第 1 阶段:项目初始化     ── 模块与依赖边界盘点、216 文件 Compose 依赖度分类、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版(换线不换版)
第 3 阶段:源码集手术     ── 197 个 Compose 文件 git mv 进 composeMain 存档,19 个内核文件留 commonMain
第 4 阶段:内核抽取       ── Color→Long、Dp→Float、Density→Float、settle 纯函数化、internal→public
第 5 阶段:桥接           ── @CName C ABI、手写 MiniJson、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 三 Tab 页面、真实手势接 settle 判定、HAP、签名、设备安装和真机运行

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
core/                               上游 OS 探测(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
core/src/ohosMain/                  新增:os() actual,返回 OS.HarmonyOS(apiVersion = 0)
ui2/src/commonMain/                 19 个纯 Kotlin 内核文件(tokens + sheet 数学,进 ohos 构建)
ui2/src/composeMain/                197 个 Compose 文件 + androidMain/desktopMain/iosMain 平台 actual
                                    + composeResources 资源 + 原 commonTest/desktopTest 测试
                                    (非源码集,不参与任何 target 编译,等待工具链接回)
example/nativeApp/                  Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                 DevEco Stage、CMake N-API 和 ArkTS 页面
shared/ androidApp/ desktopApp/ iosApp/ ui-navigation/ salt-adaptive-window/
                                    上游示例与深 Compose 模块,留盘不进 settings
README.OpenHarmony.md / _CN.md      鸿蒙适配说明(中英双份)
README.upstream.md                  上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core、:ui2 与 :example:nativeApp,rootProject.name = "ohos_saltui"。上游的 AGP classpath、vanniktech / kover / dokka 发布插件、compose 编译器插件全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Temurin 21.0.12.101-hotspot,org.gradle.java.home 写入 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle9.6.1(上游 wrapper 为 9.3.1,本机下载停滞数小时,换用本地已缓存且 KStore 已验证的 9.6.1)
依赖零外部依赖:内核只用 Kotlin 标准库 + core 的 JNA(仅 desktop target)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.saltui
1.3 源码依赖度盘点

对 ui2 的 216 个 commonMain 文件逐个检查 import(脚本扫描 androidx.compose.* / org.jetbrains.compose.*,非肉眼):

分类数量处置
深依赖 Compose(@Composable 组件、SaltTheme、lazy/** 的 Foundation 拷贝、Sheet 渲染层)196git mv 进 composeMain 原样存档
浅依赖(CompositionLocal 声明等)1拆分:纯枚举留 commonMain,组合本地部分进 composeMain
纯 Kotlin(数据类、接口、算法)19留 commonMain 构成内核,做类型替换后进 ohos 构建

内核 19 文件的功能域:OS 探测(core 整体)、颜色 token(SaltColors / SaltPalette)、尺寸 token(SaltDimens)、Sheet 模型(SheetProperties / SheetDetent)、Sheet 几何(SheetGeometry / SheetDefaults)、settle 规则(新增 resolveSheetSettle)、枚举与纯数据结构(ControlSize、LazyStaggeredGridLaneInfo 等 232 行交错网格 lane 数据结构)。

两个"反向动作"值得一提:ControlSize.kt 是浅依赖文件,按 cmp_qrose 抽 Code128Type 的先例拆成 commonMain 的枚举与 composeMain 的 LocalControlSize / ProvideControlSize;SheetDefaults 的弹簧 MotionSpec 与 Dp 视图拆进 composeMain/SheetMotion.kt,dp 常量表与 settle 阈值留在内核。

1.4 页面边界
标题区          SaltUI · OpenHarmony / 副标题(Compose Multiplatform 组件内核 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        主题 / Sheet / 关于
主题页          深色开关、平台单选(鸿蒙/安卓/iOS/桌面)、11 颜色 token(色块+hex)、
                12 语义色板网格、8 项尺寸 token(dp/px 双显)、8 个上游 PNG 图标
Sheet 页        detent 复选(Content/Medium/Large/Height320dp/Fraction0.4)、可关闭开关、内容高度滑杆、
                吸附点列表(px+dp)、Auto 显示模式判定、可拖拽 Sheet 弹层、时间戳操作日志
关于页          版本握手、OS.current、架构分层图、边界声明、AtomGit 链接

页面启动时先做版本握手,再读屏幕(display.getDefaultDisplaySync())与避让区(window.getLastWindow)注入 density / 视口高 / 底部 inset。

第 2 阶段:目标与依赖打通

2.1 工具链钉版(为什么是 2.4.20-0.1.0-07)

KStore 验证过的"换线不换版"在本库零成本复用:上游源码本来就是 Kotlin 2.4.20 编的,HarmonyOS Kotlin 2.4.20-0.1.0-07 与之同代,commonMain 内核一行不改即可编译。ohosArm64() / ohosX64() 目标只存在于这套发行线,从 eazytec 社区 Maven 拉取。

Gradle 方向上游 wrapper 是 9.3.1,但本机从 services.gradle.org 下载该发行版停滞了两个多小时(每小时约 40MB);改用本地已缓存、且 KStore 在同一工具链线上验证过的 9.6.1,构建脚本同步适配(val x by getting 在 9.6 已是错误用法,改 getByName(...))。

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_saltui"

include(":core")
include(":ui2")
include(":example:nativeApp")
2.2 编译内核

:core 收敛为纯 KMP 模块:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    // 交叉验证用:上游 desktopMain 的 JNA actual 原样编译
    jvm("desktop")
    ohosArm64()
    ohosX64()

    sourceSets {
        commonTest.dependencies {
            implementation(libs.kotlin.test)
        }
        getByName("desktopMain") {
            dependencies {
                implementation(libs.jna)
                implementation(libs.jna.platform)
            }
        }
    }
}

:ui2 同构,commonMain 依赖 api(project(":core"))。库模块不开 sharedLib——so 产物只在 example/nativeApp 生成,否则每个模块都会吐一份带运行时的 so,HAP 体积和符号都会乱。实测:

./gradlew :core:compileKotlinDesktop :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :ui2:compileKotlinDesktop  :ui2:compileKotlinOhosArm64  :ui2:compileKotlinOhosX64    # BUILD SUCCESSFUL
2.3 内核抽取:换表示不换语义

内核对上游只做四类动作,全部可审计:

动作例子不变的东西
类型替换Color(0xFF0470E6) → 0xFF0470E6L;Dp → Float(dp 数值);Density 参数 → Float density全部 token 数值、阈值常量
算法移植compositeOver / copyAlpha 以 Long 数学实现 sRGB 源覆盖;resolveSheetSettle 纯函数化 SheetTransitionState.settle混合公式、四条落点规则逐字对应
注解剥离@Immutable / @Stable / @JvmField / mutableStateOf(HarmonyOS Kotlin 不认 JVM 向注解;状态原语属 Compose)属性名与默认值
桥面公开sheet 内核 API 从 internal 升 public 并统一标注 @UnstableSaltUiApi(SaltUI 自己的不稳定标记)签名语义

resolveSheetSettle 是本次最有含量的抽取——上游 settle 藏在持有 AnchoredDraggableState 的状态机里,规则却全是纯数值判断:

internal fun resolveSheetSettle(
    density: Float,
    viewportHeight: Float,
    points: List<Pair<SheetDetent, Float>>,   // 解析后的吸附点(px,升序)
    currentHeight: Float,                     // 松手时可见高度(px)
    velocity: Float,                          // px/s,向下为正
    dismissible: Boolean
): SheetSettleDecision

四条规则与上游一一对应:位移关闭阈值 min(48dp, 最低档 25%);低于最低档且下抛速度 > 1000dp/s 判关;|速度| ≥ 600dp/s 按方向越级选档(可跳过中间档);其余就近吸附。OS 侧则新增 HarmonyOS(apiVersion) 变体与 isHarmonyOS / ifHarmonyOS,SaltDimens 借 defaultFor(os) 补齐 HarmonyOS 移动档(52/22dp,与 iOS 同档)。

2.4 测试的处置:上游测试随 Compose 归档,内核测试重写为行为验收

ui2 的原 commonTest / desktopTest(4 + 19 个文件)测的是 Compose 组件(JustifiedRow 覆盖率门禁等),随源码集手术一起进 composeMain 存档——它们在内核构建里没有对应物。内核另建 23 项 desktopTest(2 项 core + 21 项 ui2),全部断言用户可观察行为:解析出的吸附点高度、settle 在每个阈值边界两侧的判定、token 混合值、四平台尺寸。根构建同样全局禁用 Ohos*Test* 任务,测试统一 JVM 跑。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.moriafly.salt.core / com.moriafly.salt.ui(内核,原源码抽取件)
        ↓ OS.current / SaltColors / SaltDimens / resolveSheetGeometry / resolveSheetSettle
Bridge.kt(@CName × 1,JSON 进出)
        ↓ UTF-8 JSON
N-API saltUiDispatch(request)
        ↓
ArkUI Index.ets(三 Tab 单页滚动)

六个 op 一口进:

请求 : {"op":"version"}
       {"op":"os"}
       {"op":"theme","dark":false,"os":"harmonyos"}
       {"op":"sheet/geometry","density":3.0,"height":2229,"chromeHeight":192,
        "bottomInset":132,"contentHeight":690.0,
        "properties":{"detents":[{"kind":"content"},{"kind":"medium"},{"kind":"large"}],...},
        "preferred":{"kind":"content"}}
       {"op":"sheet/settle","density":3.0,"viewportHeight":2229.0,
        "points":[{"detent":{...},"height":690.0},...],
        "currentHeight":900.0,"velocity":1900.0,"dismissible":true}
       {"op":"sheet/display-mode","mode":"auto","windowWidthPx":1080.0,
        "windowHeightPx":2229.0,"density":3.0}
响应 : {"ok":true,...各 op 载荷...}
       {"ok":false,"error":"..."}

两个设计继承自已审核仓库:detent 以 {"kind":"content"|"medium"|"large"|"height"|"fraction","value":...} 建模,sheet/geometry 的输出(吸附点 + detent 身份)原样作为 sheet/settle 的输入回传,桥不维护会话状态;颜色以 #AARRGGBB 字符串返回,ArkUI 的 ResourceColor 直接可用。

3.2 手写 MiniJson 为什么不上 kotlinx.serialization

桥载荷含嵌套数组与对象(detents、points),手写递归下降解析器约 300 行(含完整字符串转义)。这与 qrose / KStore 是同一决策:序列化运行时会通过依赖链拖进 so,而 C ABI 面要的是"一个 const char* 进、一个 const char* 出"的最小稳定面;载荷小,解析成本可忽略。

3.3 错误通道

导出整体 try/catch,非法 JSON、缺字段、不支持的 op / detent kind 全部走同一个 ok:false 通道,ArkTS 侧统一渲染:

@CName("SaltUiDispatch")
fun saltUiDispatch(request: String): String = try {
    MiniJson.write(dispatch(MiniJson.parse(request) as Jv.Obj))
} catch (t: Throwable) {
    MiniJson.write(Jv.obj("ok" to Jv.bool(false), "error" to Jv.str(t.message ?: t.toString())))
}

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libsaltui_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohossaltui.so)
  │ SaltUiDispatch
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;ArkTS 又不能调 @CName 符号,所以 N-API 包装层是必经之路。

4.2 Kotlin/Native 导出函数
@CName("SaltUiDispatch")
fun saltUiDispatch(request: String): String
C ABI 符号ArkTS 方法覆盖内核
SaltUiDispatchsaltUiDispatch(req)版本握手 / OS 探测 / 主题 token / Sheet 几何 / settle / 显示模式 六 op 分发
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"saltUiDispatch", nullptr, NapiSaltUiDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossaltui_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossaltui_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号)。三个名字严格一致:nm_modname=saltui_napi、ArkTS import { saltUiDispatch } from 'libsaltui_napi.so'、index.d.ts 导出名。

4.4 CMake 和类型声明
add_library(ohossaltui SHARED IMPORTED)
set_target_properties(ohossaltui PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(saltui_napi SHARED napi_init.cpp)
target_link_libraries(saltui_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohossaltui)

OHOS_ARCH 为空或 libohossaltui.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。链接任务 finalizedBy 拷贝任务把 KN so 与匹配 triple 的 libc++_shared.so 一并送进 entry/libs/<abi>/。

export const saltUiDispatch: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 启动注入和数值拉取
aboutToAppear(): void {
  const d = display.getDefaultDisplaySync();
  this.density = d.densityPixels > 0 ? d.densityPixels : 1;
  this.viewportPx = d.height;
  window.getLastWindow(getContext(this)).then((mainWindow: window.Window) => {
    const area = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
    this.bottomInsetPx = area.bottomRect.height;
  });
  this.refreshVersion();   // 版本握手:来自真实 Native 调用
  this.refreshOs();
  this.refreshTheme();
  this.refreshDisplayMode();
}

真机实测注入值:视口 1080x2229px(360x743dp)、density=3、底部避让 132px。

5.2 页面交互

主题 Tab 自上而下:深色开关与平台单选(切平台即重拉 theme op,四套 token 并排对比)、11 个颜色 token(色块 + 名称 + hex)、12 语义色板网格、8 项尺寸 token(dp 与 px 双显)、8 个上游 PNG 图标。Sheet Tab 是交互核心:

  • 勾选 / 去选 detent、拖内容高度滑杆,实时重解析吸附点列表(px + dp)与初始选中;
  • 点「显示 Sheet」打开自绘弹层:拖把手实时变高,松手调 sheet/settle 得目标档或关闭,animateTo 吸附;
  • 遮罩透明度 = visibleHeight / 参考档高度 × 0.32(SCRIM_OPACITY 来自内核常量),拖拽全程连续变化;
  • 每步操作进带时间戳的日志(最新在前,上限 50 条),→ 请求 ← 响应成对留痕。

拖拽速度没有用系统手势的 velocity 字段,而是 onActionUpdate 里以 offsetY / 时间差自算(每 5ms 采样一次),松手值喂给 settle——这是 PanGesture 速度类型在当前 SDK 上的稳妥替代。

5.3 对比度和错误边界

整页配色全部来自 theme op 的 token(背景 / 卡片 / 主文 / 次文 / 强调),深浅色自动成立;ok:false 时错误原文进日志区,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志,页面永不白屏。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohossaltui)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 1,六 op 分发
│       └── MiniJson.kt                       递归下降 JSON 解析 / 序列化(零依赖)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.saltui、versionName 3.0.0-beta02-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/libohossaltui_api.h
        ├── src/main/cpp/types/libsaltui_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
./gradlew :core:desktopTest :ui2:desktopTest                                 # 23 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

hvigor 命令行有两个环境前提:PATH 必须同时含 DevEco 自带的 node 与 jbr/bin(缺 java 会 spawn java ENOENT),并设 DEVECO_SDK_HOME。签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机不进仓库(仓库内 signingConfigs 保持空数组)。

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.saltui -m entry

四、完整代码对照

4.1 整体架构

com.moriafly.salt.core.os/**(OS 探测内核 + OS.HarmonyOS 变体)
    │ OS.current / isHarmonyOS / ifHarmonyOS
com.moriafly.salt.ui/**(19 文件内核)
    │ SaltColors / SaltPalette(Long ARGB + sRGB 混合)
    │ SaltDimens(Float dp + defaultFor(os) 四平台分档)
    │ SheetProperties / SheetDetent / SheetDefaults
    │ resolveSheetGeometry(detent → px 吸附点)
    │ resolveSheetSettle(松手落点四规则)
Bridge.kt + MiniJson
    │ SaltUiDispatch(六 op JSON 分发)
libsaltui_napi.so
    │ saltUiDispatch
Index.ets
    │ JSON parse + @State 更新 + 真实手势
    ▼
ArkUI 真机页面(三 Tab 单页滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_saltui、只 include 三模块
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 插件钉版 + kotlin-test / jna 库
gradle.properties版本 3.0.0-beta02-ohos.1、JDK 21 路径、kotlin.native 两个开关
build.gradle.kts(根)版本注入、全局禁用 Ohos*Test* 任务
core/build.gradle.kts / ui2/build.gradle.kts纯 KMP:jvm(“desktop”) + ohosArm64 + ohosX64,不开 sharedLib
core/.../OS.kt + OS.ohos.ktHarmonyOS 平台变体与 ohos actual(全库唯一新增 core 源文件)
ui2/src/commonMain 19 文件内核:tokens + sheet 数学(8 文件改写、其余原样)
ui2/src/composeMain197 个 Compose 文件 + 平台 actual + 资源 + 原测试,存档不编译
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt / MiniJson.kt1 个 C ABI、六 op、零依赖 JSON
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鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
内核SaltColors.defaultLight(os) / defaultDark(os)11 token 明暗配对(Long ARGB),含 compositeOver / copyAlpha 混合
内核SaltDimens.defaultFor(os)四平台尺寸分档(HarmonyOS 52/22dp)
内核resolveSheetGeometry(density, ...)detent → px 吸附点:inset 钳制、最小高度、去重、就近偏高选择
内核resolveSheetSettle(density, ...)松手落点:48dp/25% 关闭、1000dp/s 甩关、600dp/s 越级、就近吸附
内核SheetDisplayMode.resolveIsBottom(w, h)600x480dp 阈值判定底部 / 浮层
NativeSaltUiDispatchC ABI JSON 导出(六 op 一口进)
N-APIsaltUiDispatch(req)向 ArkTS 暴露能力
ArkTSdispatch(op, args)触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const response = JSON.parse(saltUiDispatch(JSON.stringify({
  op: 'sheet/settle',
  density: this.density, viewportHeight: this.viewportPx,
  points: this.points,                    // sheet/geometry 回传结构原样回传
  currentHeight: this.sheetHeightPx,
  velocity: this.dragVelocity, dismissible: this.dismissible
}))) as Record<string, Object>;
// response.close === true → 关闭;否则按 response.target 回查吸附点并 animateTo 吸附
val decision = resolveSheetSettle(
    density = density, viewportHeight = viewportHeight,
    points = points, currentHeight = currentHeight,
    velocity = velocity, dismissible = dismissible
)

两者之间只传输 JSON 字符串;吸附点表由桥解析、ArkTS 回传,桥不维护任何会话状态。

五、关键决策说明

决策 1:抽内核而不是真编译整库——与 KStore 相反方向的同一方法论

KStore 真编译整库,前提是 kotlinx 制品线齐了、引擎零 UI;SaltUI 的 216 个 commonMain 文件里 196 个是 Compose,“整库编译"这条路在工具链对齐前不存在。判断标准只有一条:该库的价值有多少不在 UI 框架里。SaltUI 的答案是主题 token + Sheet 数学 + OS 探测——恰好是设计语言的"数据层”,抽出来与 Android / iOS / 桌面同源;渲染层整体冻结,接回路径完整保留。

决策 2:Color→Long、Dp→Float——换表示不换语义

不引 Compose 也不引 Skia:Color 是 ARGB 打包值,Dp 是 Float 数值,内核需要的全部运算(sRGB 源覆盖混合、alpha 替换、dp×density)用 Long / Float 数学即可覆盖,desktopTest 用手工独立计算的期望值钉死(0x80FFFFFF over 0xFFF3F3F3 = #FFF9F9F9 等)。引图形运行时会把整棵依赖树拖进 C ABI 面,违背桥最小化原则。

决策 3:settle 规则纯函数化,状态机留在 Compose 侧

SheetTransitionState.settle 的四条规则是纯数值判断,跨桥价值最高;而 AnchoredDraggable / mutableStateOf 那层是 Compose 运行时语义,硬抽等于重写。落点判定进内核、动画与中断状态由 ArkTS 持有——真机上拖把手松手后的吸附行为与 Android 端走的是同一份规则源码。

决策 4:工具链换线不换版,Gradle 用本地已验证组合

Kotlin 钉 2.4.20-0.1.0-07 与上游同代、源码级零适配;Gradle 9.3.1 wrapper 在本机下载停滞,换成 KStore 在同一工具链线上验证过的 9.6.1——工具链经验按线传递,不按版本号想当然。

决策 5:composeMain 存档而不是删除

197 个文件、平台 actual、composeResources、原测试 git mv 进 composeMain(非源码集),提交历史保留改名轨迹。工具链对齐后的接回动作是"整体迁回 + 类型合流(Long↔Color、Float↔Dp)",而不是从上游重新对表——历史可审计,接回有清单。

决策 6:单 JSON 入口 + 零依赖桥

六个 op 共用一个 @CName 符号,未来加 op 不动 ABI;MiniJson 手写约 300 行,换的是 so 里不带任何序列化运行时。这是 qrose / KStore 一脉相承的纪律。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 26.0(7.0.0,API 26)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64,1080x2229 / 360x743dp / density 3)。

6.2 JVM 交叉验证

./gradlew :core:desktopTest :ui2:desktopTest

23 项用例全部通过(内核与 ohos target 编译同一份源码):

测试类用例数钉住什么
OSTest(core)2平台探测有效性
SheetGeometryTest5内容/半高/全高解析、Medium 低于最小高度被丢弃而非钳制、等高去重、previousHeight 就近偏高、Height 档随密度缩放含 inset
SheetSettleTest948dp 阈值边界(恰好关闭 / 差 1px 不关)、1000dp/s 关闭速度边界、非可关闭不判关、600dp/s 上抛/下抛越级、超最高档落最大、慢速就近吸附
SaltColorsTest4明暗默认值(含手工独立计算的混合期望值)、iOS 分支、动态配对
SaltDimensTest3四平台 item/itemIcon、default 匹合当前平台

期望值手工独立计算是关键纪律:混合色期望若由被测实现生成,测试只是同义反复。0x80FFFFFF over 0xFFF3F3F3 逐通道手算 255×0.50196 + 243×0.49804 = 249 得 #FFF9F9F9,测试通过即证明 Long 数学混合与 Compose 的 Color.compositeOver 结果一致。

6.3 HAP 验证

:core / :ui2 compileKotlinDesktop / OhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SaltUiDispatch / libohossaltui_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libsaltui_napi.so)
CompileArkTS: finished(严格 ArkTS lint 通过)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 7.0 MB(23 个文件),内含双 ABI 各三件套(libsaltui_napi.so / libohossaltui.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 2.0 MB(debugShared 未 strip 时约 3.7 MB,含 Kotlin/Native 运行时)、x86_64 约 1.9 MB。

6.4 功能用例(真机)

真机(1080x2229,density 3,底部避让 132px)实测:

  1. 启动后「关于」页版本握手显示 3.0.0-beta02-ohos.1 / 上游 3.0.0-beta02(来自真实 Native 调用);OS.current 显示 HarmonyOS 变体;
  2. 「主题」页深色开关切换,11 个 token 色块与 hex 同步变化;平台单选四档对比,HarmonyOS 档 item 为 52dp / itemIcon 22dp(移动档),与桌面 32/20dp 并排可辨;
  3. 「Sheet」页默认勾选 Content/Medium/Large、内容高度 230dp,解析出三个吸附点:Content 690px(230dp)、Medium 1181px(394dp)、Large 2229px(743dp),初始选中 Content——与内核推导一致(可用高度 2229−132=2097,Medium = 132 + 2097×0.5 ≈ 1181);
  4. 显示模式判定 Bottom(底部吸附),窗口 360x743dp、阈值 600x480dp——宽 360 < 600 命中底部档;
  5. 点「显示 Sheet」弹层以 Content 档 690px 展开,遮罩随高度渐变;
  6. 快速上拖松手,日志记录 settle → Medium (1181px, velocity=-4479px/s)——负速度(向上)且 |4479| ≥ 1800px/s(600dp/s),按方向越级从 Content 落 Medium;
  7. 慢速拖小幅松手就近吸附回原档;拖过最低档继续下拉松手即关闭(可关闭开时);
  8. 关闭「可关闭」开关后重复强甩,不判关、就近吸附;
  9. 每步操作按时间戳滚动留痕,深浅色全程对比度成立。

6.5 验证结论

内核逻辑(OS 探测 / 主题 token / Sheet 几何 / settle 规则)、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,三 Tab 页面展示了真实的内核会话:版本握手、跨平台 token 对比、detent 解析与拖拽吸附闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/SaltUI.git
cd SaltUI
export JAVA_HOME="/path/to/jdk-21"
./gradlew :core:compileKotlinOhosArm64 :ui2:compileKotlinOhosArm64             # klib
./gradlew :core:desktopTest :ui2:desktopTest                                   # 23 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon          # 需 DevEco node + jbr 在 PATH
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.saltui -m entry
hdc shell hilog | grep SaltUi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 上游 Gradle 9.3.1 wrapper 在本机从 services.gradle.org 下载停滞两个多小时——换用本地已缓存且 KStore 验证过的 9.6.1,构建脚本按 9.6 语法适配;
  2. val desktopMain by getting 在 Gradle 9.6 是错误用法,改 getByName("desktopMain") 显式获取;
  3. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  4. 内核 API 原为 internal,桥在独立模块调不到——升 public 并统一标注 @UnstableSaltUiApi;
  5. ArkTS 的 @Builder 不支持 lambda 参数——抽成自定义组件(@Prop + @BuilderParam 尾随闭包);
  6. ArkTS 对象字面量必须对应显式类型——JSON 桥接一律 Record<string, Object> 声明;
  7. 自定义状态命名 tabIndex 与组件内置属性冲突——换名 currentTab;
  8. PanGesture 的 velocity 在当前 SDK 上类型不可靠——onActionUpdate 里 offsetY / 时间差自算(5ms 采样);
  9. hvigor 打包报 spawn java ENOENT——PATH 必须含 DevEco 自带 jbr/bin(或任一 JDK 21);
  10. 自绘 216px 应用图标初版渲染成彩条——生成脚本把 0-255 标度的像素又乘了一次 255,写缓冲溢出截断;修正后本地解码回验再入库;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. abiFilters 必须双 ABI、每 ABI 三件套缺一不可(HAP 缺当前 ABI 的 so 报安装失败 9568347);build-profile.json5 的 signingConfigs 仓库内留空数组,签名由 DevEco 自动签名回填,证书绝不进入 AtomGit。

8.2 已知问题

  • Compose 组件(197 文件)冻结在 composeMain,鸿蒙侧暂不可用,等待社区 CMP 到 1.12 线或上游支持 ohos target;
  • OS.HarmonyOS.apiVersion 恒为 0:纯 Kotlin/Native 无法不经 NAPI 查询,宿主需要真实值时走系统 API 展示;后续可在 napi_init.cpp 查询后注入;
  • Demo 的拖拽速度是 offsetY/时间差近似(5ms 采样),极端瞬时速度可能与系统手势估计有差;
  • AnchoredDraggable / 动画中断状态未整体移植(留在 Compose 侧),ArkTS 持有动画状态;
  • android / ios / desktop 示例 app 与 ui-navigation、salt-adaptive-window 留盘不进构建;本 fork 不发 Maven Central;
  • ArkTS 页面的 getContext 在新 SDK 上有 deprecation 警告(功能正常,后续可换 getUIContext().getHostContext())。

8.3 未来优化方向

  • 二期把 AnchoredDraggable 状态机按纯函数边界继续抽取(中断恢复、动画曲线参数),桥面同步扩展;
  • apiVersion 经 N-API 查询注入内核,替换恒 0 占位;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图。

九、总结

9.1 核心难点回顾

上游 core + ui2(216 commonMain 文件,196 个深 Compose)
    → 依赖度脚本盘点:19 文件内核 / 197 文件存档
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(换线不换版,Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式)+ 源码集手术(composeMain 存档)
    → 内核抽取:Color→Long、Dp→Float、settle 纯函数化
    → ohosArm64 / ohosX64 KLIB + C ABI(@CName × 1,六 op)
    → C++ N-API(DisposeString 纪律)+ CMake IMPORTED / $ORIGIN
    → ArkTS 三 Tab 宿主(真实手势 → settle 判定 → 动画吸附)
    → HAP 构建、签名、设备安装和真机验证

9.2 封装层次

  • 内核层:上游源码抽取件——OS 探测(含 HarmonyOS 变体)、主题 token(Long ARGB + sRGB 混合)、尺寸四平台分档、Sheet 几何与 settle 四规则;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 解析、手势速度估计、操作分发、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库适配的判断标准是"价值有多少不在 UI 框架里":KStore 整库真编译是因为引擎零 UI;SaltUI 九成文件是 Compose,正确答案就变成抽内核 + 存档渲染层。两条路线是同一方法论在不同库况下的两个方向,选择必须基于逐文件的依赖度盘点,而不是库的类型标签;
  2. 换表示不换语义,靠手工期望值钉死:Color→Long、Dp→Float 的每一步都可以漂移,desktopTest 里手工独立计算的混合值(#FFF9F9F9 / #FF272727 / alpha 打包 0x26)是"内核没漂"的唯一可信证明——期望值绝不能由被测实现生成;
  3. 工具链经验按线传递:Kotlin 钉版跟依赖制品线走(2.4.20-0.1.0-07 与上游同代零成本),Gradle 组合跟已验证先例走(9.6.1),下载停滞就换本地缓存——环境问题不该消耗适配时间。

9.4 适配成果

当前适配已完成:

  • SaltUI 3.0.0-beta02 内核(OS 探测 + 主题 token + Sheet 几何 / settle 规则,19 文件)的双 ABI Kotlin/Native 编译,与 Android / iOS / 桌面同一份源码;
  • 1 个 C ABI 符号(六 op JSON 分发)和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(版本握手、跨平台 token 对比、detent 解析、拖拽吸附、显示模式判定、操作日志);
  • 23 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 7.0MB)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐