开源鸿蒙平台 CMP 三方库「compose-material-symbols」适配全流程
本文记录
compose-material-symbols接入 OpenHarmony 的完整过程,覆盖字体图标库的资产盘点、离线符号数据生成、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-material-symbols是基于 Google Material Symbols 可变字体(Outlined / Rounded / Sharp 三族,四轴 wght / FILL / GRAD / opsz)的 Compose Multiplatform 图标库:传一个图标名("home"、"close"),拿一个按字号、字重、颜色定制的图标 Composable,同时生成全量图标名常量对象MaterialSymbols。本次适配与存储类、条码类库都不同:它的价值不在算法引擎,而在字体 + 数据双资产——三份 TTF 就是渲染本体,3954 个「图标名 → 码点」映射就是检索本体。适配把这两块资产原样带到鸿蒙:数据层直接编译成 OpenHarmony KLIB,字体经 rawfile 注册进 ArkUI 文本管线,ArkTS 页面按码点渲染字形、不依赖连字(ligature)能力。上游构建(Kotlin 2.4.10 / Compose 1.12 / AGP 9.4.0 / Gradle 9.7.1)的 Compose UI 部分隔离在src/composeMain不参与鸿蒙编译,等社区 CMP 工具链对齐后原样接回。
项目地址: AtomGit/oh-tpc/compose-material-symbols.git
上游仓库: ClementVicart/compose-material-symbols
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-material-symbols 为 Compose Multiplatform 应用解决"图标"这一件事:Google 官方的 material-icons-[extended] 已停止演进,Material Symbols 是替代品,但 Compose 侧一直没有趁手的封装。上游给出的是 MaterialSymbol.Filled / .Outlined[.Sharp / .Rounded] 这组 Composable——字体加载、变体轴(FontVariation.Setting("wght"/"FILL"/"GRAD"/"opsz"))、LocalContentColor 染色全部收好,另有 buildSrc 在构建期联网拉取 Google Fonts 元数据、生成 MaterialSymbols 全量常量。KMP 项目把这些放在 commonMain,由 Android、JVM、iOS、JS、Wasm 共享。OpenHarmony 应用同样需要这套图标能力,但上游构建不会生成 OpenHarmony 变体。
如果只在 ArkTS 里手抄一份图标名列表、把 TTF 丢进工程,页面也能出图标,但无法证明库的数据资产在 OpenHarmony ARM64 设备上完成编译、链接和执行,也无法保证图标名、码点与上游逐字一致。适配需要同时验证符号数据层、Native 动态库、C ABI、N-API、字体注册、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.10 / Compose 1.12.0 / AGP 9.4.0 / Gradle 9.7.1 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 生成链路断裂 | buildSrc 的 GenerateSymbolNameTask 构建期访问 fonts.google.com/metadata,国内网络不可达;且 buildSrc 依赖链(Kotlin 2.4 系)与鸿蒙工具链冲突。 |
| Compose 依赖 | 全部 5 个源文件都 import androidx.compose.*,社区 CMP 1.9.2 编不了 Compose 1.12 的 API。 |
| 资源体系不同 | Compose Resources(Res.font.*)与鸿蒙 rawfile 是两套字体分发体系。 |
| 语言边界 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、字体、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:符号数据层的 ohos 编译、离线生成物落盘、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。字形渲染交给 ArkUI 文本引擎 + 官方 TTF 原件,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的图标检索与渲染会话,对应桥接的全部入口:
- 计数(count):
MaterialSymbolsCodepoints.CODEPOINTS.size经 C ABI 返回 3954——来自真实 Native 调用而非静态文字; - 检索(list):按名称子串过滤(大小写不敏感),返回排序后的名称数组和码点转好的字形字符串数组(一一对应),单页上限可调(Demo 固定 120);
- 详情(info):按名称精确查找,返回码点(十进制)与字形字符串,试验场展示
U+E88A形式的十六进制码点; - 渲染闭环:三族字体 rawfile 注册后,
Text(char).fontFamily(...)直接出字形——码点直渲染,不依赖字体连字; - 样式面:Outlined / Rounded / Sharp 三族切换、字号 16~96 滑条、Normal / Medium / Bold 三档字重、5 种预设色。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
total / returned | count 返回图标总数;list 返回匹配总数与实际返回条数。 |
names / chars | list 的名称数组与字形字符串数组(一一对应)。 |
name / codepoint / char | info 的名称、码点、字形字符串。 |
上游引擎侧完整能力(3954 个常量、码点映射、MaterialSymbol Composable、FontVariation 四轴设置)中,数据部分随编译进入 .so,Compose 部分隔离在 src/composeMain 留待工具链对齐。JVM 侧保留码点对账验证(home = U+E88A、close = U+E14C、10k = U+E951、wifi = U+E63E),证明 ohos .so 里执行的数据与官方一致。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 资产复用 | 三份 TTF 原样随 HAP 分发(rawfile),符号数据从 TTF 的 post / cmap 表直接解析生成、落盘提交,构建期零联网。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);buildSrc 删除、kotlin-js-store 删除,上游五文件迁 src/composeMain。 |
| 桥接稳定 | 单一 C ABI 入口 + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 渲染可靠 | 码点直渲染,不依赖连字;字形字符串在 Kotlin 侧完成代理对转换,ArkTS 零处理。 |
| UI 完整 | 三 Tab 覆盖全部桥接入口:画廊(搜索 + 分页 + 状态行)、试验场(三族 / 字号 / 字重 / 颜色 / 码点)、集成说明。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。字体图标库的 UI 面就是"一个 Text",适配面天然小;ArkTS Demo 的职责是把桥暴露的检索与渲染闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点资产与依赖边界、确认离线生成路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、生成物落盘
第 3 阶段:桥协议建模 ── count/list/info JSON 契约、码点对连字的取舍、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 字体注册、画廊首屏、样式试验场和错误边界
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
src/commonMain/kotlin/.../ MaterialSymbols + MaterialSymbolsCodepoints(离线生成物,提交进仓)
src/commonMain/composeResources/font/ 三份 Material Symbols 可变字体 TTF(唯一数据源,进 git)
src/composeMain/kotlin/... 上游 5 个 Compose 文件(MaterialSymbol / Symbol / FontFamilies 等,不进构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API、rawfile 字体和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
buildSrc / kotlin-js-store 上游联网生成与 JS 缓存,删除不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,rootProject.name = "ohos_compose_material_symbols"。上游根构建的 AGP、Compose 编译器、vanniktech 发布插件、自定义 dev.vicart.compose.material.symbols 生成插件全部随根构建替换剥离——本 fork 不发 Maven Central。上游是单模块工程(root 即库模块),这个拓扑保持不变,只是目标收敛为 jvm() + 双 ohos。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.7.1 必须降版;2.2.21 线不支持 Gradle 9.x) |
| 外部依赖 | 零(commonMain 纯 Kotlin,无 coroutines / serialization 制品线问题) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.materialsymbols |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 资产 | 形态 | 依赖 | 处置 |
|---|---|---|---|
| 5 个 Compose 源文件 | Composable / FontFamily / FontVariation | androidx.compose.*、Compose Resources | 迁 src/composeMain,不编译 |
MaterialSymbols 常量 | buildSrc 联网生成 | fonts.google.com 元数据 | 离线再生成,落盘提交 |
| 三份 TTF | 二进制资产 | 无 | 原样进 rawfile,同时是再生成 的唯一数据源 |
上游的生成链路是 GenerateSymbolNameTask:构建期拉 https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true,解析出图标名后用 KotlinPoet 生成 MaterialSymbols 常量对象。这条链路在鸿蒙 fork 里必须断掉:其一,国内网络到 fonts.google.com 不可达(实测 curl 直接连接失败);其二,buildSrc 自身是 Kotlin 2.4 依赖链,Gradle 8.14.1 配置期就解析不动。
替代路线是从仓内 TTF 直接解析——字体本身就是 ground truth,比线上元数据更权威(元数据描述字体,字体才是渲染本体)。用 JDK 21 单文件程序(JEP 330,java FontDump.java 免编译直接跑)解析三张表:
maxp:numGlyphs;post(format 2.0):glyph ID → 字形名,Material Symbols 的字形名就是图标名(如home、check_circle);cmap(format 4 / 12,优选 (3,10) 全 UTF-32 子表):码点 → glyph ID。
三张表 join 出「图标名 → 码点」。两个必须处理的坑:
- 数字开头的图标名带前缀下划线。字体里
10k图标的字形名是_10k(字形名不允许数字开头,字体设计工具自动加了下划线),而库的规范名是10k。解析时剥掉前缀下划线还原;生成 Kotlin 常量时数字开头不合法,反向补一个前缀:const val _10K = "10k"。 - 单字母 a–z 是连字基础字形,不是图标。字体为了支持"输入图标名即出字形"的连字(ligature),内置了 26 个基础字母字形,cmap 里同样有映射。不过滤会把它们混进图标表。规则:过滤长度小于 2 的名称。
三个字体各解析出 3980 个 post 名称、去掉 26 个基础字形后 3954 个图标,且三族完全一致(交集 = 并集)——这本身就是一次字体资产对账。抽查码点与 Google 官方公布值一致:home = U+E88A、close = U+E14C、arrow_back = U+E5C4、wifi = U+E63E、favorite = U+E87D、10k = U+E951。
生成两个文件提交进 src/commonMain:
// MaterialSymbols.kt —— 与上游生成物同构的常量对象(3954 项)
object MaterialSymbols {
const val HOME = "home"
const val CLOSE = "close"
const val _10K = "10k" // 数字开头补前缀
// …
}
// MaterialSymbolsCodepoints.kt —— fork 新增的码点映射(数据层的检索本体)
object MaterialSymbolsCodepoints {
val CODEPOINTS: Map<String, Int> = buildMap {
put("home", 0xE88A)
put("close", 0xE14C)
put("10k", 0xE951)
// …
}
}
解析器是一次性工具,用完即弃不进仓库;再生成的唯一合法来源是仓内 TTF,不手改生成物。
1.4 页面边界
标题区 MATERIAL SYMBOLS · OPENHARMONY / 副标题(Google 字体图标库 · outlined / rounded / sharp 可变字体)
Tab 切换 图标画廊 / 样式试验场 / 集成说明
画廊 Tab 搜索框(输入即查)+ 状态行(图标总数 · 匹配 · 当前显示 · 耗时)+ 4 列网格(字形 + 名称小字)
试验场 Tab 选中图标行 + 200vp 大预览区 + info 行(名称 · 码点 U+XXXX · 字体族)+ 风格三选一 +
字号滑条(16~96) + 字重三选一 + 5 色预设
说明 Tab 三步集成(注册字体 / 引入 so / 查询渲染)+ 已知限制
页面启动时注册三族字体、count 握手、空关键字 list 拉首屏、默认图标 info 拉详情,四件事都在 aboutToAppear 完成。
第 2 阶段:目标与依赖打通
2.1 离线生成验证(本适配的成败手)
先验证 fonts.google.com 不可达(curl 返回 000),确认上游生成链路在本地无法复现;再验证仓内 TTF 可解析出与官方一致的码点(上一节抽查六个全部命中)。两个结论合起来,离线生成物落盘就是唯一可行路线:
- 生成物提交进仓库,构建期零联网——审核环境、内网环境、CI 都能直接构建;
- 数据源是字体本体,生成物与渲染资产天然同源,不存在"元数据说有、字体里没有"的漂移;
- 再生成有明确契约(解析 TTF 三表 + 两条过滤规则),生成物头部注释写明"不要手改"。
2.2 工具链钉版(为什么是 2.2.21-1.0.0)
本库 commonMain 收敛后是纯 Kotlin 零依赖(无 coroutines、无 serialization),不存在 KStore 那种"stdlib 元数据 2.4 逼着工具链升级"的约束,直接沿用社区批准的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款)。代价是 Gradle 必须从上游 9.7.1 降到 8.14.1——2.2.21 线不支持 Gradle 9.x,这条纪律与 qrose 完全一致。JDK 钉 21(Temurin),org.gradle.java.home 写进 gradle.properties,Windows 命令行跑 gradlew 前还要 JAVA_HOME 指同一路径(shell 包装脚本在 JVM 启动前就检查它)。
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_compose_material_symbols"
include(":example:nativeApp")
2.3 编译真实数据层
根构建收敛为纯 KMP 模块(root 即库模块,上游单模块拓扑保持):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
allprojects {
group = "dev.vicart"
version = providers.gradleProperty("version").orElse("1.1.6-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
}
commonMain 里只剩两个生成物,零外部依赖。实测:
.\gradlew.bat compileKotlinJvm # BUILD SUCCESSFUL in 23s
数据层的 7900 余行生成代码(3954 常量 + 3954 映射项)在 JVM 与双 ohos 目标上全部编译通过。
2.4 buildSrc 的处置:联网生成链路整体摘除
buildSrc 目录只要存在,Gradle 就会在配置期自动构建它——即使根构建不再 apply 那个插件。所以处置不是"不 apply",而是整个删除,连同它的 kotlinpoet / kotlinx-serialization 依赖链和 fonts.google.com 访问一起出局。kotlin-js-store(js / wasmJs 目标的 yarn 锁)随目标剥离一并删除。上游生成逻辑保留在 git 历史里,README.upstream.md 也完整保存了上游文档——需要回看时随时可查。
第 3 阶段:桥协议建模
3.1 JSON 契约
dev.vicart.compose.material.symbols/**(符号数据层,原样编译)
↓ MaterialSymbolsCodepoints.CODEPOINTS(3954 项,TreeMap 序即字典序)
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API symbolsQuery()
↓
ArkUI Index.ets(三 Tab)
请求与响应:
请求 : {"op":"count"}
{"op":"list","query":"home","limit":120}
{"op":"info","name":"home"}
响应 : {"ok":true,"total":3954}
{"ok":true,"total":3,"returned":3,"names":["home",...],"chars":["...",...]}
{"ok":true,"name":"home","codepoint":59530,"char":"..."}
{"ok":false,"error":"..."}
一个关键的不对称设计:list 不只返回名称,还成对返回字形字符串(names[i] 与 chars[i] 一一对应)。画廊一次调用就拿到可渲染的全部数据,不需要为每个格子再发一次 info——3954 项里翻页 120 个,一次 list 就是一屏。
3.2 码点为什么比连字稳
上游 Compose 侧的渲染入口是 BasicText(text = icon)——传图标名字符串,靠字体的连字(ligature)机制把 “home” 五个字母合成一个字形。这条路在 ArkTS 上有不确定性:鸿蒙文本引擎对自定义注册字体的连字支持没有公开承诺。所以桥在 Kotlin 侧就把码点转成 UTF-16 字形字符串:
private fun codepointToChar(cp: Int): String = buildString {
when {
cp < 0 || cp > 0x10FFFF -> {}
cp <= 0xFFFF -> append(cp.toChar())
else -> { // 增补平面走代理对
val v = cp - 0x10000
append(((v shr 10) + 0xD800).toChar())
append(((v and 0x3FF) + 0xDC00).toChar())
}
}
}
Material Symbols 的码点集中在 BMP 私有区(U+E000–U+F8FF),但转义逻辑按完整 Unicode 写——JSON 输出统一 \uXXXX 转义,ArkTS JSON.parse 一次还原。码点直渲染只依赖字体注册成功 + cmap 命中,把连字这个不确定变量从链路里整个拿掉。info 同时返回 codepoint(十进制)与 char,试验场展示 U+E88A 十六进制形式,方便和官方码点表人工对账。
3.3 错误通道
导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("MaterialSymbolsQuery")
fun materialSymbolsQuery(request: String): String {
return try {
val json = MiniJson.parse(request)
when (json.str("op")) {
"count" -> count()
"list" -> list(json)
"info" -> info(json)
else -> err("unknown op '${json.str("op")}'")
}
} catch (t: Throwable) {
err(t.message ?: t.toString())
}
}
非法 JSON、未知 op、info 查不到名称(unknown symbol 'xxx'),全部走同一个 ok:false 通道,ArkTS 侧统一渲染。list 的 limit 做了 1…500 收敛,query 统一 trim + 小写——Demo 传空串就是首页。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libmaterialsymbols_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosmaterialsymbols.so)
│ MaterialSymbolsQuery
▼
UTF-8 JSON + DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 函数挂到 ArkTS 模块上——libohosmaterialsymbols.so 里能 nm 到 MaterialSymbolsQuery,但 ArkTS import 它只能看到 stdlib 的 zip 类型。所以 CMake 再包一层 NAPI so,ArkTS 只 import 这一层。
4.2 Kotlin/Native 导出函数
@CName("MaterialSymbolsQuery")
fun materialSymbolsQuery(request: String): String // count / list / info 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
MaterialSymbolsQuery | symbolsQuery(req) | CODEPOINTS 全量检索(计数 / 子串过滤分页 / 精确查找 + 码点转换) |
单一入口、单一 const char* → const char* 签名,以后加字段只动 JSON 语义,不动 C 签名。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"symbolsQuery", nullptr, NapiSymbolsQuery, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosmaterialsymbols_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosmaterialsymbols_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。API 26 的 NAPI 头把 napi_get_value_string_utf8 的缓冲区标成非 const,取串先量长度、开 std::vector<char>、拷完再构造 std::string。
4.4 CMake 和类型声明
add_library(ohosmaterialsymbols SHARED IMPORTED)
set_target_properties(ohosmaterialsymbols PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(materialsymbols_napi SHARED napi_init.cpp)
target_link_libraries(materialsymbols_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosmaterialsymbols)
OHOS_ARCH 为空或 libohosmaterialsymbols.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const symbolsQuery: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 字体注册和首屏加载
aboutToAppear(): void {
font.registerFont({ familyName: 'MaterialSymbolsOutlined', familySrc: '/rawfile/material_symbols_outlined.ttf' });
font.registerFont({ familyName: 'MaterialSymbolsRounded', familySrc: '/rawfile/material_symbols_rounded.ttf' });
font.registerFont({ familyName: 'MaterialSymbolsSharp', familySrc: '/rawfile/material_symbols_sharp.ttf' });
this.loadTotal(); // count 握手:状态行的 3954 来自真实 Native 调用
this.loadGallery(); // list 首屏:空 query 拉前 120 项
this.loadInfo('home'); // info 默认图标
}
private callQuery(req: string): QueryResult {
try {
return JSON.parse(symbolsQuery(req)) as QueryResult;
} catch (e) { // so 缺失 / 返回非法 JSON 都收进 ok:false,绝不崩 HAP
const fail: QueryResult = { ok: false, error: `${e}` };
return fail;
}
}
字体不进 git:copyFonts 任务在 link 阶段把三份 TTF 从 src/commonMain/composeResources/font 拷进 entry/src/main/resources/rawfile,保证 HAP 里的字体与库内字体永远同源,仓库也不重复存储 34MB 二进制。
5.2 页面交互
画廊 Tab 自上而下:搜索框(onChange 即重新 list)、状态行(图标总数 3954 · 匹配 M · 当前显示 K · 耗时 Xms,耗时用 Date.now() 差值)、4 列网格(columnsTemplate('1fr 1fr 1fr 1fr'),每格字形 26vp + 名称 10vp 省略号截断),点击任意图标切到试验场并异步补查 info。试验场 Tab:当前图标行、200vp 大预览区(字号 / 字重 / 颜色即时生效)、info 行(名称 home · 码点 U+E88A · 字体 MaterialSymbolsOutlined)、风格三选一、字号滑条、字重三选一、5 色预设。说明 Tab 用 @Builder DocStep 复用三步集成块(注册字体 / 引入 so 并注明三件套 / 查询渲染),附已知限制段落。
5.3 对比度和错误边界
选中态用深黑胶囊底白字,未选中浅灰底;试验场预览区浅灰圆角卡片衬底,深色图标对比清晰。ok:false 时错误原文进状态行(红色);Native 调用本身抛错(理论不该发生)同样被 callQuery 的 try/catch 收进状态行。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosmaterialsymbols)+ cxxShared
│ │ + copyKnSo* + copyFonts(字体与 so 同一条管线)
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfig default(签名由 DevEco 自动生成)
├── AppScope/app.json5 bundleName org.terminator.ohos.materialsymbols、versionName 1.1.6-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/libohosmaterialsymbols_api.h
├── src/main/cpp/types/libmaterialsymbols_napi/{index.d.ts, oh-package.json5}
├── src/main/resources/rawfile/ copyFonts 回填的三份 TTF(gitignore)
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),构建 HAP
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库。linkDebugShared* 完成后 copyKnSo* 与 copyFonts 自动把三件套 so 和三份字体回填进 HAP 工程。
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.materialsymbols -m entry
四、完整代码对照
4.1 整体架构
dev.vicart.compose.material.symbols/**(符号数据层,离线生成 + 原样编译)
│ MaterialSymbols(3954 常量)/ MaterialSymbolsCodepoints(3954 码点映射)
src/composeMain/**(上游 Compose API,隔离不编译)
│ MaterialSymbol / Symbol / FontFamilies / FontVariation 四轴
example/nativeApp
│ MaterialSymbolsQuery(@CName)+ MiniJson + 码点转字形
libmaterialsymbols_napi.so
│ symbolsQuery
Index.ets
│ JSON parse + font.registerFont + @State 更新
▼
ArkUI 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose_material_symbols |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 1.1.6-ohos.1、jvm + 双 ohos、全局禁用 Ohos*Test* 任务 |
src/commonMain/.../MaterialSymbols.kt | 离线生成物:3954 个图标名常量(上游同构) |
src/commonMain/.../MaterialSymbolsCodepoints.kt | 离线生成物:name→codepoint 映射(fork 新增) |
src/composeMain/...(5 文件) | 上游 Compose API 原样隔离,git 重命名保留历史 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* + copyFonts 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、count/list/info 分发、码点转字形、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(17 节,中文正文 5000+ 字) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 数据层 | MaterialSymbols.HOME / _10K | 编译期可校验的图标名常量(上游同构) |
| 数据层 | MaterialSymbolsCodepoints.CODEPOINTS | 3954 项 name→codepoint 映射,字典序 |
| Native | MaterialSymbolsQuery | C ABI JSON 导出(count / list / info) |
| N-API | symbolsQuery(req) | 向 ArkTS 暴露能力 |
| ArkTS | font.registerFont | 三族字体注册(rawfile 路径) |
| ArkTS | Text(char).fontFamily(...) | 码点直渲染,不依赖连字 |
| ArkTS | loadGallery() / loadInfo() | 触发检索并刷新 UI |
| 上游 | MaterialSymbol.Filled(icon=, weight=, size=, grade=, tint=) | Compose 侧对应入口(composeMain 隔离中) |
4.4 ArkTS 与 Kotlin 的边界
const raw = symbolsQuery(JSON.stringify({ op: 'info', name: 'home' }));
const res = JSON.parse(raw) as QueryResult;
// res.char = "\uE88A";res.codepoint = 59530
Text(res.char).fontFamily('MaterialSymbolsOutlined').fontSize(48)
private fun info(json: MiniJson): String {
val name = json.str("name").trim().lowercase()
val cp = MaterialSymbolsCodepoints.CODEPOINTS[name]
?: return err("unknown symbol '$name'")
return """{"ok":true,"name":${jsonStr(name)},"codepoint":$cp,"char":${jsonStr(codepointToChar(cp))}}"""
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:字体 + 数据双资产平移,不提取内核也不硬编 Compose
字体图标库没有"算法内核"可提取——它的内核就是字体和数据本身。三份 TTF 原样进 rawfile(渲染资产),码点映射原样编进 .so(检索资产),上游 Compose API 隔离待接。任何"在 ArkTS 里手抄图标表"的路线都会引入与上游的漂移风险,而本路线的数据源就是字体本体。
决策 2:工具链沿用 2.2.21 线,Gradle 降 8.14.1
与 KStore 相反:本库收敛后零外部依赖,没有 stdlib 元数据逼升工具链的约束,直接用社区批准的 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款)。代价是 Gradle 从上游 9.7.1 降到 8.14.1——2.2.21 线不支持 Gradle 9.x。两条工具链线的经验不能互相套用:有 kotlinx 依赖看依赖元数据,零依赖看社区批准钉法。
决策 3:根构建替换(cmp_qrose 模式),buildSrc 整体摘除
上游根构建(AGP / Compose 编译器 / vanniktech 发布 / 联网生成插件)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。buildSrc 只要留在目录里就会被 Gradle 自动构建,所以必须删除而非停用;生成物落盘提交,构建期零联网,数据源唯一(仓内 TTF)。
决策 4:码点直渲染,不依赖连字
上游靠连字把图标名合成字形,鸿蒙文本引擎对自定义字体的连字支持没有公开承诺。桥在 Kotlin 侧完成码点 → UTF-16 字形字符串的转换(含代理对),ArkTS 拿到的 char 直接进 Text——把不确定性从链路里拿掉,info 同时返回十进制码点供人工对账。
决策 5:JVM 交叉验证 + 生成物对账
数据层在 commonMain,JVM 与 ohos 跑同一份代码;编译产物对账(total == 3954、home == 0xE88A、close == 0xE14C、10k == 0xE951、wifi == 0xE63E)就是"生成物没漂"的闸门。生成物来自 TTF 三表 join 而非联网元数据,对账基准是 Google 官方公布码点——数据链路的可信度不依赖任何单一环境。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Eclipse Temurin)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven 制品线)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat compileKotlinJvm # BUILD SUCCESSFUL in 23s
编译产物直接对账(java -cp 单文件加载 kotlin-stdlib 后执行):
| 项 | 实测 | 基准 |
|---|---|---|
CODEPOINTS.size | 3954 | 三族 TTF 解析交集 |
home | 0xe88a | Google 官方 U+E88A |
close | 0xe14c | Google 官方 U+E14C |
10k | 0xe951 | Google 官方 U+E951 |
wifi | 0xe63e | Google 官方 U+E63E |
MaterialSymbols.HOME | "home" | 上游常量同构 |
零依赖意味着"换线不换版"的验证面最小:没有制品线替换,就没有 API 漂移风险,对账全部落在数据值本身。
6.3 HAP / so 验证
compileKotlinJvm: BUILD SUCCESSFUL(23s)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(49s,含 copyKnSo* + copyFonts)
llvm-nm -D: MaterialSymbolsQuery / libohosmaterialsymbols_symbols 均为全局 T 符号
llvm-readelf -d: NEEDED 全为短名(libc++_shared.so 等,无 Windows 路径污染),SONAME 正确
CAdapter api.cpp:448 -Wreturn-type 警告: 生成胶水固有问题(qrose 同款),可忽略
DevEco CompileArkTS / PackageHap: BUILD SUCCESSFUL(真机构建)
install bundle successfully / start ability successfully(真机)
实测产物清单:
| 产物 | 位置 | 大小 | 说明 |
|---|---|---|---|
libohosmaterialsymbols.so (arm64) | entry/libs/arm64-v8a/ | 3,389,848 B ≈ 3.23 MB | 含 Kotlin/Native 运行时与全部符号数据 |
libohosmaterialsymbols.so (x86_64) | entry/libs/x86_64/ | 2,789,896 B ≈ 2.66 MB | 模拟器用 |
libc++_shared.so | 各 ABI 目录 | ≈1.77 / 1.65 MB | 与 KN so 同 triple |
| 三份 TTF | entry/src/main/resources/rawfile/ | 共 34,273,576 B ≈ 32.7 MB | copyFonts 与库内字体同源 |
| HAP(debug,双 ABI) | DevEco 构建产物 | 以实际 assembleHap 为准 | 字体占体积主体 |
6.4 功能用例
- 启动后
count握手,状态行出现图标总数 3954(来自真实 Native 调用,非静态文字); - 首屏
list(空 query)拉前 120 项,状态行匹配 3954 · 当前显示 120 · 耗时 Xms——真机实测 11ms; - 搜索框输入
home,网格实时过滤为home子串匹配项; - 点击任意图标切到试验场,
info行显示名称 · 码点 U+XXXX · 字体族,与 JVM 对账值一致; - 风格三选一切换 fontFamily(Outlined / Rounded / Sharp),字形随族变化;
- 字号滑条 16~96、字重三档、5 色预设即时作用于中央预览;
- 非法输入(未知 op / 未知图标名)返回
ok:false,错误原文进状态行,HAP 不崩。
6.5 验证结论
符号数据层、Native 动态库、C ABI、N-API、ArkTS 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,画廊首屏即出 120 项检索结果、状态行报告总数 3954 与毫秒级耗时(见下节效果图)。字体字形渲染的真机表现见 8.2 已知问题的说明。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-material-symbols.git
cd compose-material-symbols
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat compileKotlinJvm # JVM 交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example\harmonyApp,构建 HAP 并自动签名
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.materialsymbols -m entry
hdc shell hilog | grep MaterialSymbols
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让数据层真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- buildSrc 只要存在就会被 Gradle 自动构建——"不 apply 插件"挡不住它,必须整目录删除;
- fonts.google.com 国内不可达且无稳定镜像,联网生成路线直接判死——数据源改仓内 TTF 三表(
maxp/post/cmap)join,生成物落盘提交; - 字体里数字开头图标名带前缀下划线(
_10k),规范名要剥前缀还原,Kotlin 常量名再补前缀(const val _10K = "10k")——两个方向别搞反; - 单字母 a–z 是连字基础字形,cmap 里有映射、不是图标——按名称长度 < 2 过滤,否则图标表混入 26 个假条目;
- Windows 下跑
gradlew,org.gradle.java.home不够,shell 包装脚本在 JVM 启动前就检查JAVA_HOME——两处都要指 JDK 21; - Gradle 9.7.1 上游 wrapper 与 Kotlin 2.2.21 线不兼容,必须降 8.14.1;JVM 侧
jvmToolchain(21)与java.home保持同代; @CName符号挂不到 ArkTS(CAdapter 只导出 stdlib zip 类型),CMake NAPI 包装层是硬需求不是优化项;llvm-readelf -d检查 NEEDED 必须全为短名——出现C:\Users\...说明 IMPORTED 绝对路径漏了IMPORTED_NO_SONAME+$ORIGIN;- HAP 缺当前 ABI 的 so 报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - ArkTS
@State成员不能与通用属性方法重名:@State size: number撞上.size(SizeOptions),编译报10505001——改名iconSize即解; - 字体与 so 走同一条 Gradle 回填管线(
copyFonts/copyKnSo*),rawfile 与 libs 目录 gitignore——34MB 二进制不在 git 里存两份,HAP 内字体与库内字体永远同源; - CAdapter 生成胶水
api.cpp:448的-Wreturn-type警告是 HarmonyOS Kotlin 已知问题(qrose 同款),不影响链接,等上游修生成器。
8.2 已知问题
- 鸿蒙侧无 Compose
MaterialSymbolComposable:五个上游文件隔离在src/composeMain,等社区compose.uiohos 制品与 CMP 1.12 对齐后接回,届时业务代码从鸿蒙桥迁回 Compose API,图标名常量(MaterialSymbols.HOME)不用换; - 可变字体四轴中
wght可经fontWeight近似控制、opsz随fontSize生效;FILL/GRAD两轴 ArkUI 暂无公开 API 控制——上游MaterialSymbol.Filled的"填充"语义在鸿蒙侧暂以族切换近似; - 真机首屏截图(7.1)中检索链路全部正常(总数 / 分页 / 耗时),但网格单元格的图标字形渲染待复核:
font.registerFont对可变字体的注册生效时机、以及首帧渲染与字体加载完成之间的竞争,是首要排查方向(切 Tab 触发重绘后字形是否出现可作为判据);该问题只影响 Demo 的视觉呈现,不影响数据层与桥接链路的已验证结论; - 三份 TTF 共约 32.7MB,HAP 体积主体是字体;对体积敏感的业务可按族裁剪(只带 outlined 一族省约 24MB),但三族切换的 Demo 语义会降级;
- android / js / wasmJs / ios 目标剥离(源文件留在
src/composeMain与 git 历史),发布链路一并剥离,本 fork 不发 Maven Central; list是同步一次一调用,3954 项全量分批拉取需多次调用(Demo 单页 120 已覆盖常规交互)。
8.3 未来优化方向
- 排查并修复真机字体渲染(注册时机 / 首帧竞争 / 可变字体兼容性三层验证),补全三族字形的真机截图矩阵;
- 评估
FILL/GRAD轴的替代方案:静态实例化字体(按轴值预生成静态 TTF)或等待 ArkUI 变量轴 API; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套 + rawfile 手动管线; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 把码点表导出为 ArkTS 侧可用的静态资源(
.json),评估绕过 so 的轻量集成形态作为补充路径。
九、总结
9.1 核心难点回顾
上游 compose-material-symbols(Compose API + 联网生成 + 三份 TTF)
→ fonts.google.com 不可达 → 数据源改仓内 TTF(maxp/post/cmap 三表 join)
→ 过滤连字基础字形 + 还原 _10k 前缀 → 3954 项生成物落盘
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
→ 根构建替换(cmp_qrose 模式,buildSrc 整体摘除)
→ ohosArm64 / ohosX64 KLIB + 码点转字形
→ C ABI(@CName × 1)+ count/list/info JSON 契约
→ C++ N-API(DisposeString 纪律)
→ ArkTS 字体注册 + 码点直渲染
→ ArkUI 真机三 Tab 验收(检索 / 样式 / 集成说明)
9.2 封装层次
- 数据层:离线生成的
MaterialSymbols(3954 常量,上游同构)与MaterialSymbolsCodepoints(码点映射); - 资产层:三份 Google 官方可变字体 TTF,rawfile 分发、Gradle 管线保证与库内同源;
- Native 层:生成双 ABI 动态库并通过单一 C ABI 输出 JSON(含码点 → 字形字符串转换);
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、字体注册、检索分发、码点展示与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 字体图标库的适配对象是资产不是算法:把"渲染本体(TTF)+ 检索本体(码点表)"平移过去,比在 ArkTS 里重写任何一层都可靠——数据源是字体本身,连"元数据漂移"这种风险都不存在;离线生成物落盘让构建对网络零依赖;
- 码点是比连字更硬的渲染契约:连字依赖文本引擎的 shaping 实现,码点只依赖 cmap 命中——跨平台适配时,选依赖面最小的那条渲染路径,把不确定性从链路里拿掉而不是在文档里解释它;
- 零依赖库的工具链纪律反而更简单也更严格:没有 kotlinx 制品线约束,直接用社区批准钉法(2.2.21 线 + Gradle 8.14.1);但"上游生成链路断掉"这类问题也无处借力——数据对账(JVM 侧码点逐项比官方)必须自己做,且要做在发布之前。
9.4 适配成果
当前适配已完成:
- compose-material-symbols
1.1.6数据层的双 ABI Kotlin/Native 编译(3954 项符号数据,离线生成、零联网构建); - 单一 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,llvm-readelf验证 NEEDED 全短名); - DevEco Stage ArkUI 三 Tab 验收页面(画廊检索 / 样式试验场 / 集成说明,
@State与通用属性冲突等编译问题已修复); - JVM 交叉验证全绿(总数 3954,home/close/10k/wifi 码点逐项对官方)+ HAP 构建(双 ABI 六件套 so + 三份字体 rawfile)、签名、真机安装与运行;
- AtomGit 项目文档、OpenHarmony README(中英双份,中文正文 5000+ 字)和真机效果图。
适配过程 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)