开源鸿蒙平台 KMP 三方库「kmp-screenutil」适配全流程
本文记录
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原件、公式逐行引用上游行号,通过@CNameC 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 Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.screenutil |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
library | 1(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-25 | v * wScale |
h(v) | ScreenUtil.kt:27-28 | v * hScale |
r(v) | ScreenUtil.kt:31-35 | v * min(wScale, hScale) |
rsp(v) | ScreenUtil.kt:46-47 | v * wScale(sp;显示大小再乘有效 fontScale) |
sw(v) | ScreenUtil.kt:49-50 | v * deviceWidthDp |
sh(v) | ScreenUtil.kt:52-53 | v * deviceHeightDp |
init(...) | ScreenUtil.kt:98-99 | wScale = 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 方法 | 覆盖内核 |
|---|---|---|
ScreenUtilDispatch | screenutilDispatch(req) | scaler.init(= rememberScreenScaler)+ w/h/r/rsp/sw/sh 换算 + catalog |
ScreenUtilVersion | screenutilVersion() | 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.toml | Kotlin 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.kt | 2 个 C ABI、6 op 分发、fnum 定点序列化、错误通道 |
nativeApp/.../MiniJson.kt | 手写扁平 JSON 读取(不依赖 kotlinx.serialization) |
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 验收页面(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 object | w()/h()/density/wScale/screenWidthDp/... 命令式访问器 |
| facade | ScreenUtilHeadless.init / w / h / r / rsp / sw / sh / catalog | 同一套公式,无 composition 环境 |
| Native | ScreenUtilDispatch / ScreenUtilVersion | C ABI JSON 导出 |
| N-API | screenutilDispatch(req) / screenutilVersion() | 向 ArkTS 暴露能力 |
| ArkTS | applyScaler() | 触发 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 功能用例
- 启动后标题区出现
fork 1.0.2-ohos.1 · upstream 1.0.2(来自真实 Native 调用,非静态文字); - 设备窗口卡自动注入
772vp × 1605vp、density 2.0(display px / densityPixels,对应 Compose containerSize / density); - 缩放系数卡显示
wScale 2.1 / hScale 2.3 / rScale 2.1(772/360 = 2.1444、1605/710 = 2.2606、min = 2.1444); - 换算表首行
4.0 → w 8.6 / h 9.0 / r 8.6 / rsp 8.6,与手算4 × 2.1444 = 8.578、4 × 2.2606 = 9.042一致; - 适配对比卡 RAW 面板按设计稿原值渲染(卡片 320×120、头像 48、按钮 200×44),ADAPTED 面板按桥换算值渲染(卡片 686×271、头像 103、按钮 429×94),真机上肉眼可见比例差异;
- 修改设计稿为
412 × 915点"应用",比例条、缩放系数、换算表、对比块全部即时重算; - sw/sh 比例 chips 显示
0.5.sw → 386vp、1.sw → 772vp、0.5.sh → 802.5vp(屏幕比例语义); - 关于卡展示
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:library真正编译成ohosArm64/ohosX64KLIB 并链出双 ABI 动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 社区 Compose 线不发布
*-desktop子构件:jvm 交叉验证目标必须双线声明(ohosMain社区线 /jvmMain上游线),composeJvm 必须等于上游实际编译版本(本库 1.8.0)——写错版本号 jvm 目标就解析失败; - Kotlin/Native 上
@Volatile不在默认导入里,Unresolved reference 'Volatile'——需要显式import kotlin.concurrent.Volatile(本次实测踩中); - ArkTS 的
@Builder不能接收闭包参数:card(inner: () => void)里调用inner()报does not meet UI component syntax,且build()里所有this.card(() => {...})调用点连锁解析失败(二十多条伪报错)——每个分区改为独立@Builder方法;对象字面量统一const req: XxxRequest = {...}显式类型标注; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝(报00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 根构建的
/build/是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的example/harmonyApp/.idea/、entry/libs/、oh_modules/要单独追加 gitignore,否则混进提交; - 签名材料由 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/.rcomposable 渲染(库零改动); - 桥补
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 { }、六个扩展属性、ScreenUtilobject; - facade 层:
ScreenUtilHeadless——同一套公式脱离 composition 环境运行,行号可审计; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 校验、op 分发与对比块渲染;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:Compose ohos 线 1.12.0 出现后,纯 Compose 计算类库的正确答案从"剔除 Compose 提内核"(qrose 时代)变成"整库真编译 + headless facade"——路线选择必须基于当天的制品可达性,而不是历史结论;
- 工具链版本跟着依赖走:Kotlin 钉版由 Compose 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线上 9.x,2.2.21 线降 8.14.1);
- 交叉验证是"内核没漂"的唯一可信证明: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。
参考文档
更多推荐

所有评论(0)