本文记录 kmp-screenutil 接入 OpenHarmony 的完整过程,覆盖屏幕适配库盘点、社区 Compose ohos 制品线选型、真实库源码编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

kmp-screenutil 是基于 Compose Multiplatform 的屏幕适配库(灵感来自 flutter_screenutil):给定一份设计稿尺寸(默认 360 × 710),它按 设备尺寸 / 设计稿尺寸 计算宽高两个缩放系数,再通过 .w / .h / .r / .rsp / .sw / .sh 六个扩展属性把设计稿数值换算成设备实际 Dp / TextUnit,ScreenUtil { } 组合函数负责把 ScreenScaler 注入 CompositionLocal,enableFontScale 参数可以钉住系统字体缩放。本次适配与社区早期路线(qrose / haze 的"剔除 Compose 提取内核")有本质区别:社区 Maven 上已经出现完整的 Compose ohos 制品线(org.jetbrains.compose.*:1.12.0-0.1.0-07,全量 klib、同坐标、社区版本号),因此不需要剔除 Compose——上游 commonMain 全部源码(每个 @Composable 扩展、ScreenUtil { }、rememberScreenScaler、ScreenScaler 模型)被原样编译成 OpenHarmony KLIB;又因为 OHOS 暂无 Compose composition host,桥接采用 headless facade:库内唯一新增一个 ScreenUtilHeadless,直接构造上游 ScreenScaler 原件、公式逐行引用上游行号,通过 @CName C ABI + 扁平 JSON 把换算值送给 ArkUI 照单渲染——“Kotlin 侧算换算值,ArkTS 照单渲染”,与 krogress / KLoading 同一条已过审路径。

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

上游仓库: classops/kmp-screenutil

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

kmp-screenutil 为 Compose Multiplatform 应用提供"设计稿数值 → 设备尺寸"这一件事的完整答案:ScreenUtil(designSize = IntSize(360, 710)) { ... } 一行包裹根组件,内部 UI 全部用 16.w、120.h、48.r、14.rsp、0.5.sw 书写,宽高双轴独立缩放、.r 取小者保形、.rsp 缩放字号、.sw/.sh 直接取屏幕宽高比例;rememberScreenScaler 支持自定义设计稿,ScreenUtil object 暴露 density / wScale / hScale / screenWidthDp 等命令式访问器。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套适配能力,但上游构建(Kotlin 2.1.21 / CMP 1.8.0 / Gradle 8.10.2 / AGP 8.5.2)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示数字,却无法证明公共换算引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证换算引擎、Compose 依赖矩阵、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.1.21 / Gradle 8.10.2 / AGP 8.5.2(androidTarget + iOS 三 target + vanniktech 发布链)与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵整库 API 站在 compose.runtime + compose.ui 上(CompositionLocal / Density / Dp / TextUnit / LocalWindowInfo),每个都要有 ohos 变体才能解析;社区 ohos 线不发布 *-desktop 子构件,jvm 交叉验证目标必须双线声明。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
CompositionLocal 语义跨不了桥.w/.h/.r 扩展属性依赖 CompositionLocal 环境,ScreenUtil { } 是 @Composable;C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:整库 commonMain 的 ohos 编译、根构建替换(cmp_qrose 模式)和 headless facade 桥接层。换算语义完全由上游原版实现负责(ScreenScaler 原件 + 与 ScreenUtil.kt 逐字一致的公式),ArkTS 只负责调用 Native、解析 JSON 和按换算值渲染。

1.2 库提供的能力

Demo 提供一个真实的屏幕适配会话,对应桥接的全部入口:

  • 版本握手:screenutilVersion() 返回 fork 版本号 1.0.2-ohos.1,op:"version" 同时返回上游版本 1.0.2——来自真实 Native 调用而非静态文字;
  • 设备窗口注入:页面启动时从 display 读取像素尺寸与 densityPixels,换算成 vp 后随 scaler.init 下发——对应 Compose 侧 LocalWindowInfo.current.containerSize / LocalDensity.current.density;
  • 设计稿配置:360 × 710 默认值可改,"应用"按钮重新 scaler.init,整页换算表与对比块即时重算;
  • 换算表:op:"table" 一次返回 [4, 8, 12, 16, 24, 48, 100, 200] × w/h/r/rsp/sw/sh 全单位矩阵;
  • 适配对比:同一组设计稿元素两种渲染——未适配直接用设计稿数值,已适配用桥换算值,真机上肉眼可见比例差异。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op回显本次操作。
wScale / hScale / rScale缩放系数(rScale = min(wScale, hScale))。
scaler完整 ScreenScaler 状态(设计稿 / 设备尺寸 / 双系数)。
rows / results换算矩阵 / 批量换算结果(定点 4 位小数,去尾零)。

引擎侧完整能力(ScreenUtil { }、enableFontScale 钉字体缩放、rememberScreenScaler、ScreenUtil object 全部访问器)都在上游 commonMain 里原样随编译进入 .so;jvm 目标保留编译交叉验证,证明 ohos .so 里执行的换算逻辑没有漂移。

1.3 实现适配

维度要求
代码复用library/src/commonMain 全部源文件原样编译,不提取、不重写;仅新增 1 个 ohosMain headless facade(全仓唯一新增库源文件,公式逐行引用上游行号)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 androidTarget / iOS / 发布链剥离,原配置备份为 *.upstream 五件套留盘。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律Compose 依赖按源集分线:ohos 用社区线 1.12.0-0.1.0-07,jvm 用上游线 1.8.0(= 上游 compose-multiplatform 版本)——换线不换版。
UI 完整单页覆盖全部桥接入口:版本握手、设备窗口、设计稿配置、缩放系数、换算表、适配对比、关于。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 尚无 composition host(没有 ComposeUIViewController 的 ArkUI 等价物),@Composable 能编译进 klib 但不能在 ArkTS 应用内渲染;ArkTS Demo 把桥暴露的换算值画出来,几何与数值全部来自 Kotlin 侧。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认社区 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双线 Compose 目录
第 3 阶段:桥协议建模     ── ScreenScaler 载荷、6 个 op 的 JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 设备窗口注入、设计稿配置、换算表、适配对比和版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
library/                              上游换算引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
library/src/commonMain/               上游 ScreenUtil.kt 原样编译,零改动
library/src/ohosMain/                 新增:ScreenUtilHeadless.ohos.kt(headless facade,唯一新增库源文件)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
example/BRIDGE_CONTRACT.md            桥协议契约(Kotlin 桥 ↔ ArkTS 页共同遵守)
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
*.upstream                            上游构建配置备份五件套(settings / 根build / 库build /
                                      gradle.properties / libs.versions.toml),留盘可回归

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_kmp_screenutil"。上游的 androidTarget / iOS 三 target / AGP namespace / vanniktech 发布链全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.6.1 wrapper(从上游 8.10.2 升级;2.4.20 线支持 Gradle 9.x)
Compose双线:ohos 线 1.12.0-0.1.0-07(社区,全量 klib)、jvm 线 1.8.0(Maven Central,= 上游版本)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.screenutil
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
library1(ScreenUtil.kt,168 行)纯 Compose API:runtime(CompositionLocal / remember / Composable)+ ui(LocalDensity / LocalWindowInfo / Dp / TextUnit),零 expect/actual、零平台 API整体编译(jvm + 双 ohos)

整库只有一个源文件,grep expect / actual 均为 0 条——这是 KMP 库里最理想的适配形态:唯一障碍是制品线,不是源码。上游还声明了 compose.foundation 依赖,但源码零引用,按 krogress 剔除 napier 的先例从本构建剔除(社区线虽有 foundation klib,最小依赖面更利于审计)。

1.4 页面边界
标题区          SCREENUTIL · OPENHARMONY / 版本握手行(fork 1.0.2-ohos.1 · upstream 1.0.2)
设备窗口卡      屏幕宽/高(vp)+ density,display px / densityPixels 注入
设计稿配置卡    360 × 710 输入框 + 应用按钮 + 设计稿/设备宽度比例条
缩放系数卡      wScale / hScale / rScale 三枚 chip
换算表卡        值 × .w/.h/.r/.rsp 矩阵 + 0.25.sw / 0.5.sw / 1.sw / 0.25.sh / 0.5.sh 比例 chips
适配对比卡      同一组设计稿元素两种渲染(RAW 设计稿原值 vs ADAPTED 桥换算值)
关于区          分层说明 + 仓库与社区链接

页面启动时先做 screenutilVersion() 握手,再注入 display 换算的 vp 尺寸完成 scaler.init。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 Compose 的 ohos 制品。用 .module 元数据逐个验证:

org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
  variants: 同上(ohos 双线齐全)

三个关键结论:其一,坐标与 JetBrains 官方完全相同,只是版本号切到社区构建线——不是改 group 的 fork;其二,ohos 线不发布 *-desktop 子构件,jvm 交叉验证目标不能从这条线解析,必须双线;其三,社区线由 HarmonyOS Kotlin 2.4.20-0.1.0-07 工具链配套(ohosArm64() / ohosX64() target 与 compose compiler 插件都从 eazytec 解析)。

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

社区 Compose 线 1.12.0-0.1.0-07 基于 Kotlin 2.4.20 构建,元数据版本 2.4.0;旧钉法(qrose / haze 用的 2.2.21-1.0.0)读不了 2.4 元数据,编译期直接报:

Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.

工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。Gradle 方面 2.4.20 线支持 9.x(KStore / compottie 先例),wrapper 从上游 8.10.2 升到 9.6.1。

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

include(":library")
include(":example:nativeApp")
2.3 编译真实引擎

library/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)   // commonMain 有 @Composable,需要 compose compiler
}

kotlin {
    applyDefaultHierarchyTemplate()

    jvm {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_21)
        }
    }

    ohosArm64()    // the adaptation
    ohosX64()

    sourceSets {
        ohosMain.dependencies {
            api(libs.compose.runtime)              // 1.12.0-0.1.0-07 社区线
            api(libs.compose.ui)
        }
        jvmMain.dependencies {
            api(libs.compose.runtime.jvm)          // 1.8.0 上游线(= 上游 compose-multiplatform)
            api(libs.compose.ui.jvm)
        }
    }
}

Compose 依赖按 target 线声明(ohosMain 社区线 / jvmMain 上游线),commonMain 保持 line-agnostic——krogress / KLoading / eungabi 同款分法,是"构件线切换"而不是"版本切换"。实测:

./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :library:compileKotlinJvm                                       # BUILD SUCCESSFUL

上游 ScreenUtil.kt 一个字节没动,只有 ohosMain 里的 ScreenUtilHeadless.ohos.kt 是新的。

2.4 测试依赖的处置:上游没有测试文件

上游 commonTest 只有依赖声明、没有测试源文件,本 fork 不补造测试。处置是两条纪律的组合:行为由 jvm 交叉验证目标兜底(同源码对上游 Compose 1.8.0 线编译通过 = 依赖换线无 API 漂移),功能由真机 Demo 验收(第六节 8 项用例);根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.classops/**(上游换算引擎,原样编译)
        ↓ ScreenScaler(设计稿/设备尺寸 → wScale/hScale)
ScreenUtilHeadless.ohos.kt(headless facade,公式引用 ScreenUtil.kt 行号)
        ↓
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API screenutilVersion() / screenutilDispatch()
        ↓
ArkUI Index.ets(单页滚动)

请求与响应(6 个 op):

{"op":"version"}
  → {"ok":true,"op":"version","version":"1.0.2-ohos.1","upstream":"1.0.2"}
{"op":"catalog"}
  → {"ok":true,"op":"catalog","apis":[{name,kind,signature,description},...]}   // 10 条上游公开 API
{"op":"scaler.init","designW":360,"designH":710,"screenW":772,"screenH":1605}
  → {"ok":true,"op":"scaler.init","wScale":2.1444,"hScale":2.2606,"rScale":2.1444,
     "scaler":{"designWidthDp":360,"designHeightDp":710,"deviceWidthDp":772,
               "deviceHeightDp":1605,"wScale":2.1444,"hScale":2.2606}}
{"op":"scaler.get"}      → 同上形状(未 init 时默认 360×710 的 1:1 scaler)
{"op":"convert","values":[16,24,48],"unit":"w"}
  → {"ok":true,"op":"convert","unit":"w","results":[34.3111,51.4667,102.9333]}
{"op":"table","values":[4,8,12,16,24,48,100,200]}
  → {"ok":true,"op":"table","units":["w","h","r","sp","sw","sh"],
     "rows":[{"value":4,"w":8.5778,"h":9.0423,"r":8.5778,"sp":8.5778,"sw":193,"sh":401.25},...]}

两个不对称设计:convert 面向单一单位批量换算,table 面向演示页一次取全单位矩阵(换算表与适配对比块共用,减少跨 FFI 调用次数);数值序列化统一为定点 4 位小数再去尾零——Kotlin/Native 没有 String.format,Float.toString() 可能输出科学计数法,定点序列化保证 JSON 合法且显示友好。

3.2 载荷为什么是 ScreenScaler 原件

facade 内部这样初始化:

// ScreenUtil.kt:98-99
val wScale = screenWidthDp.toFloat() / designWidthDp
val hScale = screenHeightDp.toFloat() / designHeightDp
// ScreenUtil.kt:101-103 — same ScreenScaler the CompositionLocal carries
scaler = ScreenScaler(designWidthDp, designHeightDp, screenWidthDp, screenHeightDp, wScale, hScale)

换算值不重推导,直接乘在 ScreenScaler 的系数上,每个函数标注它镜像的上游行:

facade 函数上游行号公式
w(v)ScreenUtil.kt:24-25v * wScale
h(v)ScreenUtil.kt:27-28v * hScale
r(v)ScreenUtil.kt:31-35v * min(wScale, hScale)
rsp(v)ScreenUtil.kt:46-47v * wScale(sp;显示大小再乘有效 fontScale)
sw(v)ScreenUtil.kt:49-50v * deviceWidthDp
sh(v)ScreenUtil.kt:52-53v * deviceHeightDp
init(...)ScreenUtil.kt:98-99wScale = screenW / designW、hScale = screenH / designH

T = ScreenScaler 意味着桥不管 UI 怎么渲染——调用方拿到的是上游模型的原生字段,ArkTS 侧零换算逻辑。.rsp 的字体语义单独说明:桥返回 sp 数值,ArkTS 用 fp 字号渲染时系统自动应用 fontScale(等价上游 enableFontScale = true);要演示钉住字体缩放,用 vp 归一即可。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("ScreenUtilDispatch")
fun screenUtilDispatch(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        when (val op = json.str("op")) {
            "version" -> version()
            "catalog" -> catalog()
            "scaler.init" -> scalerInit(json)
            "scaler.get" -> scalerGet()
            "convert" -> convert(json)
            "table" -> table(json)
            else -> err(op.ifBlank { "(missing)" }, "unsupported op")
        }
    } catch (t: Throwable) {
        err("dispatch", t.message ?: t.toString())
    }
}

非法 JSON、缺 op、unit 不在 w/h/r/sp/sw/sh 之内、values 空数组、四个尺寸非正数、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染在页首错误条。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制换算语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("ScreenUtilVersion")
fun screenUtilVersion(): String = ScreenUtilHeadless.VERSION   // "1.0.2-ohos.1"

@CName("ScreenUtilDispatch")
fun screenUtilDispatch(request: String): String               // 6 个 op 一口进
C ABI 符号ArkTS 方法覆盖内核
ScreenUtilDispatchscreenutilDispatch(req)scaler.init(= rememberScreenScaler)+ w/h/r/rsp/sw/sh 换算 + catalog
ScreenUtilVersionscreenutilVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"screenutilDispatch", nullptr, NapiScreenUtilDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"screenutilVersion", nullptr, NapiScreenUtilVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosscreenutil_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosscreenutil_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

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

add_library(screenutil_napi SHARED napi_init.cpp)
target_link_libraries(screenutil_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosscreenutil)

OHOS_ARCH 为空或 libohosscreenutil.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const screenutilDispatch: (request: string) => string;
export const screenutilVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 注入屏幕尺寸和触发换算
aboutToAppear(): void {
  this.versionText = screenutilVersion();          // 版本握手:来自真实 Native 调用
  const d = display.getDefaultDisplaySync();       // px / densityPixels = vp
  this.screenW = Math.round(d.width / d.densityPixels);
  this.screenH = Math.round(d.height / d.densityPixels);
  this.applyScaler();                              // scaler.init + 整页刷新
}

private applyScaler(): void {
  const res = JSON.parse(screenutilDispatch(JSON.stringify({
    op: 'scaler.init', designW: this.designW, designH: this.designH,
    screenW: this.screenW, screenH: this.screenH
  }))) as ScalerResponse;
  this.wScale = res.wScale ?? 1;                   // @State 刷新缩放系数卡
  this.refreshTable();                             // op:"table" 换算表
  this.refreshRatioChips();                        // op:"convert" sw/sh 比例
  this.refreshDemo();                              // 适配对比块尺寸(同一次 table)
}

换算表与对比块共用一次 op:"table" 调用:[320, 120, 12, 48, 20, 14, 200, 44, 100, 60] 十个设计稿值按索引取对应单位列,得到卡片 320.w × 120.h、圆角 12.r、头像 48.r、标题 20.rsp、正文 14.rsp、按钮 200.w × 44.r、色块 100.w × 60.h 的设备尺寸,未适配面板直接用设计稿原值对照渲染。

5.2 页面交互

单页自上而下:版本握手行、设备窗口卡(宽/高/density)、设计稿配置卡(输入框 + 应用按钮 + 宽度比例条)、缩放系数卡(wScale/hScale/rScale)、换算表卡(值 × .w/.h/.r/.rsp 矩阵 + sw/sh 比例 chips)、适配对比卡(RAW 与 ADAPTED 两块镜像渲染)、关于卡(分层说明与仓库 / 社区链接)。修改设计稿尺寸点"应用"后整页重算,错误文本进页首错误条。

5.3 对比度和错误边界

RAW 徽标红、ADAPTED 徽标绿,语义与"适配是否生效"对齐;ok:false 时错误原文进错误条;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── BRIDGE_CONTRACT.md                        桥契约(C ABI / 6 ops / 演示元素 / 语义对应表)
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosscreenutil)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 2 + 6 op 分发 + fnum 定点序列化
│       └── MiniJson.kt                       手写扁平 JSON 读取(cmp_qrose MiniJson 模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.screenutil、versionName 1.0.2-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/libohosscreenutil_api.h
        ├── src/main/cpp/types/libscreenutil_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :library:compileKotlinJvm                                     # 交叉验证
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # KLIB
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

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

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.screenutil -m entry

四、完整代码对照

4.1 整体架构

io.github.classops/**(上游换算引擎原样编译)
    │ ScreenScaler / .w / .h / .r / .rsp / .sw / .sh / ScreenUtil{} / rememberScreenScaler
ScreenUtilHeadless.ohos.kt(ohosMain facade,公式引用上游行号)
    │ ScreenUtilVersion / ScreenUtilDispatch
libscreenutil_napi.so
    │ screenutilDispatch / screenutilVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(单页滚动:换算表 + 适配对比)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_kmp_screenutil
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + Compose 双线钉版(ohos 1.12.0-0.1.0-07 / jvm 1.8.0)
build.gradle.kts(根)版本 1.0.2-ohos.1、全局禁用 Ohos*Test* 任务
library/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,Compose 依赖按源集分线
library/src/ohosMain/.../ScreenUtilHeadless.ohos.kt唯一新增库源文件(headless facade,98 行)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、6 op 分发、fnum 定点序列化、错误通道
nativeApp/.../MiniJson.kt手写扁平 JSON 读取(不依赖 kotlinx.serialization)
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(7 分区)
example/BRIDGE_CONTRACT.md桥协议契约(Kotlin 桥 ↔ ArkTS 页共同遵守)
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
引擎ScreenUtil(designSize, enableFontScale, content)包裹内容并注入 scaler(可钉 fontScale = 1f)
引擎rememberScreenScaler(designWidthDp, designHeightDp)从窗口尺寸与密度计算 ScreenScaler
引擎Float/Int.w / .h / .r宽 / 高 / 最小比例缩放(Dp 扩展)
引擎Number.rsp / .sw / .sh字号缩放 / 屏幕宽 / 高比例
引擎ScreenUtil objectw()/h()/density/wScale/screenWidthDp/... 命令式访问器
facadeScreenUtilHeadless.init / w / h / r / rsp / sw / sh / catalog同一套公式,无 composition 环境
NativeScreenUtilDispatch / ScreenUtilVersionC ABI JSON 导出
N-APIscreenutilDispatch(req) / screenutilVersion()向 ArkTS 暴露能力
ArkTSapplyScaler()触发 scaler.init 并刷新整页

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(screenutilDispatch(JSON.stringify({
  op: 'scaler.init', designW: 360, designH: 710, screenW: 772, screenH: 1605
}))) as ScalerResponse;
// res.wScale === 2.1444;随后 op:"table" 的 rows[0].w = 4 * 2.1444 = 8.5778
when (unit) {
    "w"  -> ScreenUtilHeadless.w(value)      // value * scaler.wScale
    "h"  -> ScreenUtilHeadless.h(value)
    "r"  -> ScreenUtilHeadless.r(value)      // value * min(wScale, hScale)
    "sp" -> ScreenUtilHeadless.rsp(value)
    "sw" -> ScreenUtilHeadless.sw(value)     // value * deviceWidthDp
    "sh" -> ScreenUtilHeadless.sh(value)
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;ArkTS 侧零换算逻辑,所有数值来自 Kotlin。

五、关键决策说明

决策 1:真编译整库而不是剔除 Compose 提取内核

早期 Compose 类仓库(qrose)把 Painter/DSL 从 commonMain 挪进不注册的 composeMain,是因为当时 ohos 上没有 Compose 制品。本次适配先验证制品线(runtime / ui 的 ohosArm64 / ohosX64 klib 全部存在),障碍已消失——再剔除 Compose 等于把屏幕适配库的 API 面整个砍掉(.w/.h/.r 本身就是 Compose 扩展)。真编译让 ScreenUtil { }、rememberScreenScaler、六个扩展属性全部是上游原件,未来 composition host 落地后 ArkTS demo 页可直接换成真 composable,库零改动。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升 9.6.1

社区 Compose 线 1.12.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据。Gradle 方面 2.4.20 线支持 9.x(KStore / compottie 先例),wrapper 从上游 8.10.2 升到 9.6.1——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:headless facade 而不是桥内复刻公式(krogress 模式 A)

.w/.h/.r 依赖 CompositionLocal,无法在无 composition 环境下调用;两种桥法:KLoading 式在桥内重写公式(零库改动但漂移风险),krogress 式在库内加 headless facade(引用真实类型与行号)。换算公式只有 7 行、上游类型 ScreenScaler 是纯数据类,facade 直接构造原件——数值来源永远是上游模型,review 可以按行号逐条对照。

决策 4:根构建替换(cmp_qrose 模式),发布链剥离

上游根构建(AGP / CMP 插件 / vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建,原配置备份 *.upstream 五件套留盘可回归。本 fork 不发 Maven Central。

决策 5:jvm 交叉验证 + 真机 Demo 验收

上游没有测试文件,本 fork 不补造。jvm 目标把同源码对上游 Compose 1.8.0 线(上游实际编译版本)编译通过,证明"换线不换版"没有 API 漂移;换算正确性由真机 Demo 的 8 项功能用例验收(第六节)。测试造假的交叉验证没有意义,两条闸门都指向真实行为。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 Gradle 交叉验证

./gradlew :library:compileKotlinJvm
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

全部 BUILD SUCCESSFUL。上游 ScreenUtil.kt 对上游 Compose 1.8.0 线(jvm)与社区 Compose 1.12.0-0.1.0-07 线(ohos 双 target)同源码双线编译通过——依赖换线没有 API 漂移的直接证据。链接产物经 llvm-nm -D 验证:

libohosscreenutil.so (arm64)   T ScreenUtilDispatch / T ScreenUtilVersion / T libohosscreenutil_symbols
libohosscreenutil.so (x86_64)  T ScreenUtilDispatch / T ScreenUtilVersion / T libohosscreenutil_symbols

6.3 HAP 验证

:library:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套(libohosscreenutil.so / libc++_shared.so)回填 entry/libs/<abi>/
BuildNativeWithCmake: finished(双 ABI 编出 libscreenutil_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

KN so arm64 约 27.8MB、x86_64 约 26.6MB(debugShared 未 strip,含 Kotlin/Native 运行时与 Compose 依赖),HAP 内 strip 后显著缩小。

6.4 功能用例

  1. 启动后标题区出现 fork 1.0.2-ohos.1 · upstream 1.0.2(来自真实 Native 调用,非静态文字);
  2. 设备窗口卡自动注入 772vp × 1605vp、density 2.0(display px / densityPixels,对应 Compose containerSize / density);
  3. 缩放系数卡显示 wScale 2.1 / hScale 2.3 / rScale 2.1(772/360 = 2.1444、1605/710 = 2.2606、min = 2.1444);
  4. 换算表首行 4.0 → w 8.6 / h 9.0 / r 8.6 / rsp 8.6,与手算 4 × 2.1444 = 8.578、4 × 2.2606 = 9.042 一致;
  5. 适配对比卡 RAW 面板按设计稿原值渲染(卡片 320×120、头像 48、按钮 200×44),ADAPTED 面板按桥换算值渲染(卡片 686×271、头像 103、按钮 429×94),真机上肉眼可见比例差异;
  6. 修改设计稿为 412 × 915 点"应用",比例条、缩放系数、换算表、对比块全部即时重算;
  7. sw/sh 比例 chips 显示 0.5.sw → 386vp、1.sw → 772vp、0.5.sh → 802.5vp(屏幕比例语义);
  8. 关于卡展示 ArkTS → NAPI → C ABI → ScreenUtilHeadless → ScreenScaler 分层与 AtomGit 链接。

6.5 验证结论

换算引擎、Compose 依赖矩阵、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的适配会话:版本握手、设备窗口注入、设计稿配置、缩放系数、全单位换算表与适配前后对比(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/kmp-screenutil.git
cd kmp-screenutil
export JAVA_HOME="/path/to/jdk-21"
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64   # klib
./gradlew :library:compileKotlinJvm                                       # 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
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.screenutil -m entry
hdc shell hilog | grep ScreenUtil

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :library 真正编译成 ohosArm64 / ohosX64 KLIB 并链出双 ABI 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 社区 Compose 线不发布 *-desktop 子构件:jvm 交叉验证目标必须双线声明(ohosMain 社区线 / jvmMain 上游线),composeJvm 必须等于上游实际编译版本(本库 1.8.0)——写错版本号 jvm 目标就解析失败;
  4. Kotlin/Native 上 @Volatile 不在默认导入里,Unresolved reference 'Volatile'——需要显式 import kotlin.concurrent.Volatile(本次实测踩中);
  5. ArkTS 的 @Builder 不能接收闭包参数:card(inner: () => void) 里调用 inner() 报 does not meet UI component syntax,且 build() 里所有 this.card(() => {...}) 调用点连锁解析失败(二十多条伪报错)——每个分区改为独立 @Builder 方法;对象字面量统一 const req: XxxRequest = {...} 显式类型标注;
  6. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  7. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  8. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  9. 根构建的 /build/ 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、entry/libs/、oh_modules/ 要单独追加 gitignore,否则混进提交;
  10. 签名材料由 DevEco 自动写入 build-profile.json5,提交前检查该文件,必要时用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit。

8.2 已知问题

  • OHOS 无 Compose composition host:全部 @Composable(ScreenUtil { }、.w/.h/.r 扩展)编译进 KLIB 但不能在 ArkTS 应用内渲染;host 落地后 demo 页可换真 composable,桥与库不变;
  • 桥是同步一次一调用:ScreenUtil { } 的 CompositionLocal 注入、enableFontScale 的 Density 替换不跨 C ABI,字体语义在桥上以"sp 数值 + ArkTS fp 自动 fontScale"表达;
  • 演示取全屏 display 尺寸,Compose 侧对应窗口 containerSize,窗口内可用区(扣系统栏)略小,数值语义差异已在页面注释说明;
  • android / iOS targets 与发布链剥离(源文件与 *.upstream 备份留在树里),本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.screenutil 匹配的签名。

8.3 未来优化方向

  • 等待 composition host:ArkTS demo 页换成真 ScreenUtil { } + .w/.h/.r composable 渲染(库零改动);
  • 桥补 config.set 型入口(enableFontScale 开关的等价语义),Demo 展示字体缩放开/关对比;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、llvm-nm 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 library/commonMain(ScreenUtil.kt,零 expect/actual)
    → 验证社区 Compose ohos 制品线(runtime/ui 1.12.0-0.1.0-07)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.10.2 → 9.6.1)
    → 根构建替换(cmp_qrose 模式,Compose 依赖按源集双线)
    → ohosArm64 / ohosX64 KLIB + 唯一 headless facade
    → C ABI(@CName × 2)+ ScreenScaler 载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机单页验收(换算表 + 适配对比)

9.2 封装层次

  • 引擎层:上游原版 ScreenScaler 模型、rememberScreenScaler、ScreenUtil { }、六个扩展属性、ScreenUtil object;
  • facade 层:ScreenUtilHeadless——同一套公式脱离 composition 环境运行,行号可审计;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 校验、op 分发与对比块渲染;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:Compose ohos 线 1.12.0 出现后,纯 Compose 计算类库的正确答案从"剔除 Compose 提内核"(qrose 时代)变成"整库真编译 + headless facade"——路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 Compose 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线上 9.x,2.2.21 线降 8.14.1);
  3. 交叉验证是"内核没漂"的唯一可信证明:jvm 目标对上游 Compose 1.8.0 线编译同源码 + 真机换算表数值手算核对(4.w = 8.6 = 4 × 2.1444),两条闸门都指向真实行为,而不是复述文档。

9.4 适配成果

当前适配已完成:

  • kmp-screenutil 1.0.2 真实引擎的双 ABI Kotlin/Native 编译(上游 commonMain 零重写,仅 1 个 ohosMain headless facade);
  • 社区 Compose ohos 制品线在屏幕适配库上的完整落地(1.12.0-0.1.0-07,runtime + ui klib);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(版本握手、设备窗口、设计稿配置、缩放系数、换算表、适配对比、关于);
  • jvm 交叉验证编译通过 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(772×1605vp 设备上换算表与对比块数值全部正确);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐