本文记录 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 / returnedcount 返回图标总数;list 返回匹配总数与实际返回条数。
names / charslist 的名称数组与字形字符串数组(一一对应)。
name / codepoint / charinfo 的名称、码点、字形字符串。

上游引擎侧完整能力(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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 9.7.1 必须降版;2.2.21 线不支持 Gradle 9.x)
外部依赖零(commonMain 纯 Kotlin,无 coroutines / serialization 制品线问题)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 / FontVariationandroidx.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 出「图标名 → 码点」。两个必须处理的坑:

  1. 数字开头的图标名带前缀下划线。字体里 10k 图标的字形名是 _10k(字形名不允许数字开头,字体设计工具自动加了下划线),而库的规范名是 10k。解析时剥掉前缀下划线还原;生成 Kotlin 常量时数字开头不合法,反向补一个前缀:const val _10K = "10k"。
  2. 单字母 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 方法覆盖内核
MaterialSymbolsQuerysymbolsQuery(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.tomlKotlin 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.kt1 个 C ABI、count/list/info 分发、码点转字形、MiniJson、错误通道
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鸿蒙适配主文档(17 节,中文正文 5000+ 字)

4.3 关键 API 对照

层次API作用
数据层MaterialSymbols.HOME / _10K编译期可校验的图标名常量(上游同构)
数据层MaterialSymbolsCodepoints.CODEPOINTS3954 项 name→codepoint 映射,字典序
NativeMaterialSymbolsQueryC ABI JSON 导出(count / list / info)
N-APIsymbolsQuery(req)向 ArkTS 暴露能力
ArkTSfont.registerFont三族字体注册(rawfile 路径)
ArkTSText(char).fontFamily(...)码点直渲染,不依赖连字
ArkTSloadGallery() / 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.size3954三族 TTF 解析交集
home0xe88aGoogle 官方 U+E88A
close0xe14cGoogle 官方 U+E14C
10k0xe951Google 官方 U+E951
wifi0xe63eGoogle 官方 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
三份 TTFentry/src/main/resources/rawfile/共 34,273,576 B ≈ 32.7 MBcopyFonts 与库内字体同源
HAP(debug,双 ABI)DevEco 构建产物以实际 assembleHap 为准字体占体积主体

6.4 功能用例

  1. 启动后 count 握手,状态行出现 图标总数 3954(来自真实 Native 调用,非静态文字);
  2. 首屏 list(空 query)拉前 120 项,状态行 匹配 3954 · 当前显示 120 · 耗时 Xms——真机实测 11ms;
  3. 搜索框输入 home,网格实时过滤为 home 子串匹配项;
  4. 点击任意图标切到试验场,info 行显示 名称 · 码点 U+XXXX · 字体族,与 JVM 对账值一致;
  5. 风格三选一切换 fontFamily(Outlined / Rounded / Sharp),字形随族变化;
  6. 字号滑条 16~96、字重三档、5 色预设即时作用于中央预览;
  7. 非法输入(未知 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让数据层真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. buildSrc 只要存在就会被 Gradle 自动构建——"不 apply 插件"挡不住它,必须整目录删除;
  4. fonts.google.com 国内不可达且无稳定镜像,联网生成路线直接判死——数据源改仓内 TTF 三表(maxp / post / cmap)join,生成物落盘提交;
  5. 字体里数字开头图标名带前缀下划线(_10k),规范名要剥前缀还原,Kotlin 常量名再补前缀(const val _10K = "10k")——两个方向别搞反;
  6. 单字母 a–z 是连字基础字形,cmap 里有映射、不是图标——按名称长度 < 2 过滤,否则图标表混入 26 个假条目;
  7. Windows 下跑 gradlew,org.gradle.java.home 不够,shell 包装脚本在 JVM 启动前就检查 JAVA_HOME——两处都要指 JDK 21;
  8. Gradle 9.7.1 上游 wrapper 与 Kotlin 2.2.21 线不兼容,必须降 8.14.1;JVM 侧 jvmToolchain(21) 与 java.home 保持同代;
  9. @CName 符号挂不到 ArkTS(CAdapter 只导出 stdlib zip 类型),CMake NAPI 包装层是硬需求不是优化项;
  10. llvm-readelf -d 检查 NEEDED 必须全为短名——出现 C:\Users\... 说明 IMPORTED 绝对路径漏了 IMPORTED_NO_SONAME + $ORIGIN;
  11. HAP 缺当前 ABI 的 so 报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  13. ArkTS @State 成员不能与通用属性方法重名:@State size: number 撞上 .size(SizeOptions),编译报 10505001——改名 iconSize 即解;
  14. 字体与 so 走同一条 Gradle 回填管线(copyFonts / copyKnSo*),rawfile 与 libs 目录 gitignore——34MB 二进制不在 git 里存两份,HAP 内字体与库内字体永远同源;
  15. CAdapter 生成胶水 api.cpp:448 的 -Wreturn-type 警告是 HarmonyOS Kotlin 已知问题(qrose 同款),不影响链接,等上游修生成器。

8.2 已知问题

  • 鸿蒙侧无 Compose MaterialSymbol Composable:五个上游文件隔离在 src/composeMain,等社区 compose.ui ohos 制品与 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 三条经验

  1. 字体图标库的适配对象是资产不是算法:把"渲染本体(TTF)+ 检索本体(码点表)"平移过去,比在 ArkTS 里重写任何一层都可靠——数据源是字体本身,连"元数据漂移"这种风险都不存在;离线生成物落盘让构建对网络零依赖;
  2. 码点是比连字更硬的渲染契约:连字依赖文本引擎的 shaping 实现,码点只依赖 cmap 命中——跨平台适配时,选依赖面最小的那条渲染路径,把不确定性从链路里拿掉而不是在文档里解释它;
  3. 零依赖库的工具链纪律反而更简单也更严格:没有 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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐