开源鸿蒙平台 CMP 三方库「LetterAvatarGenerator」适配全流程
本文记录
LetterAvatarGenerator接入 OpenHarmony 的完整过程,覆盖字母头像库盘点、Compose 层隔离决策、字形软件光栅化、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
LetterAvatarGenerator是基于 Compose Multiplatform 的字母头像生成库:给一个名字,取首字母,按textSize百分比算字号,在纯色背景上居中绘制,返回ImageBitmap给业务方当头像占位图,RandomColors负责随机配色。本次适配与社区既有路线(qrose 的"编码器留 commonMain + 光栅化放 example"、combo-breaker 的"Kotlin 只度量、ArkTS 绘制")有本质区别:上游的对外语义就是"库自己出成品位图"——build(): ImageBitmap,绘制即库的核心。因此隔离 Compose 层之后,渲染栈(TTF 解析、字形轮廓提取、超采样光栅化、零依赖 PNG 编码)以纯 Kotlin 形式重写进commonMain,与 ohos 设备执行同一份代码由 JVM 交叉验证兜底,这是社区这条路上第一个在 Kotlin/Native 侧完成字形栅格化的适配。
项目地址: AtomGit/oh-tpc/LetterAvatarGenerator.git
上游仓库: Pranathi-pellakuru/LetterAvatarGenerator
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
LetterAvatarGenerator 为 Compose Multiplatform 应用提供"名字 → 头像位图"这一件事的完整答案:rememberAvatarCreator() 一行拿到创建器(内部 rememberTextMeasurer() + LocalDensity.current),AvatarCreator 链式配置 setLetter / setAvatarSize / setTextSize / setLetterColor / setBackgroundColor / setFontFamily,默认值 180px、40% 字高、白字灰底,build() 返回 ImageBitmap——纯 Compose CanvasDrawScope 绘制:drawRect 背景 + TextMeasurer.measure + 居中 drawText;RandomColors 管理随机配色对。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要默认头像占位能力(注册即生成、可上传后端),但上游构建(Kotlin 2.1.10 / Compose 1.7.3 / AGP 8.13.2)不会生成 OpenHarmony 变体,且 ohos 工具链上根本没有这套 Compose。
如果只在 ArkTS 里摆一个 Text 组件拼头像,页面能显示字母,却无法证明公共渲染管线在 OpenHarmony ARM64 设备上完成编译、链接和执行——而且"摆 UI"根本不是这个库的语义,上游输出的是可上传后端的位图成品。适配需要同时验证模型层、文字度量、字形光栅化、PNG 编码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| Compose 编不过 | 上游三个源文件全部依赖 androidx.compose(TextMeasurer / CanvasDrawScope / ImageBitmap / Compose Color),ohos 工具链上没有这套 Compose(社区 CMP 1.9.2 与上游 Compose 1.7 二进制不兼容)。 |
| 没有平台文字栈 | Kotlin/Native for ohos 没有 TextMeasurer 等价物,字形渲染必须自带;嵌位图字体点阵放大全是马赛克,与上游矢量渲染质量差距过大。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 字体资源跨端 | 上游 setFontFamily 走 composeResources;鸿蒙侧字体文件要从 ArkTS 沙箱喂进 Native。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:Compose 层隔离(cmp_qrose 模式)、纯 Kotlin 渲染栈下沉 commonMain、ohosArm64 / ohosX64 双 ABI 动态库和 C ABI/N-API 桥接层。头像语义(首字母规则、百分比字号、默认配色、链式 Builder)与上游逐字对齐,ArkTS 只负责调桥、解码 PNG 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的字母头像渲染会话,对应桥接的全部入口:
- 首字母提取(letter):
LetterAvatar.letterOf(text)——trim → 取首字符 → a–z 确定性大写 → 空输入兜底-(上游ifEmpty { "-" }是死代码,本 fork 让它真正可达); - 尺寸与字高(size / textSize):沿用上游公式
size * textSize / 100.0,钳制 [16, 1024] / [5, 100] 防误传打爆内存; - 配色(fg / bg / random):
#RRGGBB/#AARRGGBB;random: true对应上游RandomColors.getColorPair()——默认 Material 12 对色板随机取一对(上游空列表random()会直接抛异常,纯 Kotlin 版空列表回退默认色板,setter 优先级不变); - 字形渲染:TTF 真实轮廓 → 二次贝塞尔展平 → 4×4 超采样 nonzero 绕数填充,天然抗锯齿;
- 出图(pngBase64):零依赖 PNG 编码(RGBA、filter 0、stored-deflate),ArkTS
Base64Helper→image.createImageSource→PixelMap显示。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
letter / width / height | 回显实际渲染的首字母与画布尺寸(宽高对账用)。 |
bytes / pngBase64 | PNG 字节数与 Base64 正文。 |
模型层完整能力(Builder 链、默认值 180/40%/白/灰、RandomColors 三组 setter/getter)全部在 commonMain 里随编译进入 .so,JVM 侧保留 15 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的渲染逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 模型层语义与上游逐字对齐(默认值、百分比公式、链式 API、setter 优先级);渲染栈以纯 Kotlin 重写进 commonMain,不依赖任何第三方库。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 Compose 层整体迁入 src/composeMain(不是源集,不参与编译),androidMain / androidTest / test 留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 隔离 Compose 后库零第三方依赖,工具链走社区既有 2.2.21-1.0.0 线(Gradle 8.14.1),无制品线选型负担。 |
| UI 完整 | 单页覆盖全部桥接入口:名字输入、尺寸/字高滑条、五套色板、RandomColors 开关、生成按钮和字母墙。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit,全仓不出现第三方镜像站品牌名。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
LetterAvatarGenerator的 UI 面就是rememberAvatarCreator()一个入口,适配面天然最小;ArkTS Demo 只是把桥暴露的出图闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 层不可编、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Compose 层迁 composeMain
第 3 阶段:渲染栈建模 ── 模型层纯 Kotlin 化、TTF 解析、字形轮廓、光栅化与 PNG 的 commonMain 落位
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 字体引导、参数面板、RandomColors、字母墙
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
LetterAvatar/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
LetterAvatar/src/commonMain/ 新写纯 Kotlin 核心:模型/配色/TTF/渲染/PNG 五文件
LetterAvatar/src/composeMain/ 上游 Compose 层整体迁入(AvatarCommon / AvatarCompose / RandomColors,不编译)
LetterAvatar/src/jvmTest/ 交叉验证用例 + 字体子集资源 + 样图导出
LetterAvatar/src/androidMain|test|… 上游遗留,留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + posix 字体读取)
example/harmonyApp/ DevEco Stage、CMake N-API、ArkTS 页面和字体子集
example/harmonyApp/tools/ make_font_subset.py / verify_glyphs.py
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :LetterAvatar 与 :example:nativeApp,rootProject.name = "ohos_LetterAvatarGenerator"。上游的 AGP 8.13.2 / Compose 插件 / vanniktech 发布链全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Temurin 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从上游 8.13 钉到社区标准线;2.2.21 线要求 8.x) |
| 第三方依赖 | 零——隔离 Compose 后 commonMain 只有 Kotlin 标准库 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.letteravatar |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 上游文件 | 依赖形态 | 处置 |
|---|---|---|
AvatarCommon.kt | TextMeasurer / CanvasDrawScope / ImageBitmap / Compose Color / sp / Constraints | 整体迁 composeMain,语义重写为纯 Kotlin AvatarSpec |
AvatarCompose.kt | @Composable / remember / LocalDensity | 整体迁 composeMain(未来 CMP 工具链对齐再接回) |
RandomColors.kt | Compose Color | composeMain 留原件;commonMain 另写 ARGB Long 版 |
上游三个文件全部依赖 androidx.compose,“零依赖内核提取"路线走不通——这个库没有 qrose 那样的纯算法内核,它的内核就是绘制本身。因此适配的第一决策是:渲染栈必须由 fork 以纯 Kotlin 重写,且写进库的 commonMain 而不是 example。理由有二:其一,上游语义是"库出成品位图”,渲染器就是本库的"编码器"(qrose 的编码器也在库里);其二,只有放进 commonMain,JVM 交叉验证才能跑与设备完全相同的渲染代码,否则又变成"example 里自说自话"。
1.4 页面边界
标题区 LetterAvatar · OpenHarmony / 副标题(CMP 字母头像库 · Kotlin/Native ohosArm64/ohosX64)
状态行 最近一次操作结果(如"生成成功 A")
预览区 200px 头像预览(Image + PixelMap)+ 信息行(letter=A size=180x180 png=129853B)
名字区 名字输入框(默认 Alpha,取首字母,忽略大小写)
参数区 头像尺寸滑条 96–320 / 字母高度滑条 20–80%
配色区 五套色板(靛蓝/红/青绿/橙/蓝灰)+ RandomColors 随机配色开关
字母墙 4 个 128px 随机 Material 配色头像(Alpha/Letter/Open/Harmony),点击带入主预览
页面启动时先做字体引导(rawfile → 沙箱拷贝),就绪后自动生成第一张头像并铺满字母墙。
第 2 阶段:目标与依赖打通
2.1 依赖边界验证(本适配的成败手)
与 KStore 需要逐个验证 kotlinx ohos 制品线不同,本库的依赖盘点结论更干脆:上游 commonMain 的依赖表里只有 compose.runtime / foundation / ui / material3,全部因 Compose 隔离而退出;androidMain 的 core-ktx / material 在代码里零使用,一并退出。隔离完成后整条依赖链只剩 Kotlin 标准库——不需要制品线选型,工具链直接沿用社区最早批准的 2.2.21-1.0.0 钉法(qrose / haze 同款),Gradle 钉 8.14.1。
这也是"库依赖形态决定适配难度"的典型案例:存储库卡在 kotlinx 制品线,条码库卡在编码器纯度,而 Compose UI 库卡在渲染栈——各自的成败手完全不同。
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_LetterAvatarGenerator"
include(":LetterAvatar")
include(":example:nativeApp")
2.2 工具链钉版(为什么回到 2.2.21-1.0.0)
上游源码是 Kotlin 2.1.10 编的,社区 2.2.21-1.0.0 与之同代且向后兼容,源码级改动为零;又因为零第三方依赖,没有 KStore 那种"stdlib 元数据反推版本"的压力——两条工具链线(2.2.21 降 Gradle 8.14.1 / 2.4.20 留 9.x)在这里选简单的那条。Gradle wrapper 从上游 8.13 钉到 8.14.1,与全部已过审仓对齐。
gradle.properties 写入 version=1.2.0-ohos.1(上游 Maven Central 版本 1.2.0 + ohos 后缀)、JDK 21 路径和 KN 三开关(kotlin.native.ignoreDisabledTargets / kotlin.native.distribution.downloadFromMaven / kotlin.apple.deprecated.allowUsingOpenSourceCompiler)。根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
2.3 编译纯 Kotlin 核心
LetterAvatar/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖
jvmTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
./gradlew :LetterAvatar:jvmTest # 15 项交叉验证 BUILD SUCCESSFUL
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64 # 双 ABI BUILD SUCCESSFUL
第 3 阶段:渲染栈建模(技术难点)
3.1 分层契约
com.pranathicodes.letteravatar/**(commonMain,纯 Kotlin)
↓ AvatarSpec(模型)+ RandomColors(配色)+ TtfFont(字体)+ AvatarRenderer(光栅化)+ PngEncoder
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON + pngBase64
N-API letterAvatarRender()
↓
ArkUI Index.ets(单页滚动)
请求与响应:
请求 : {"text":"pranathi","size":180,"textSize":40,"fg":"#FFFFFF","bg":"#3F51B5",
"random":false,"fontPath":"/data/.../lademo_font.ttf"}
响应 : {"ok":true,"letter":"P","width":180,"height":180,"bytes":129853,"pngBase64":"..."}
{"ok":false,"error":"fontPath is required"}
一个不对称设计:fontPath 必填(渲染字形必须有字体),Demo 由 ArkTS 从 rawfile 拷到沙箱后传绝对路径;fg / bg 在 random: true 时被忽略——随机配色优先,对应上游 RandomColors.getColorPair() 的语义。
3.2 模型层:把上游语义逐字搬进纯 Kotlin
AvatarSpec.kt 里 AvatarCreator 的链式形状与上游 AvatarCreatorInterface 完全一致,只有类型换了世界:
class AvatarCreator {
fun setTextSize(textSize: Int): AvatarCreator // 默认 40,钳制 [5, 100]
fun setAvatarSize(size: Int): AvatarCreator // 默认 180,钳制 [16, 1024]
fun setLetter(letter: Char): AvatarCreator
fun setLetterColor(colorArgb: Long): AvatarCreator // 默认 0xFFFFFFFF(Color.White)
fun setBackgroundColor(colorArgb: Long): AvatarCreator // 默认 0xFF888888(Color.Gray)
fun build(): AvatarSpec // 上游返回 ImageBitmap,这里返回可渲染模型
}
fun letterOf(text: String): Char // trim → 首字符 → a–z 确定性大写 → 空则 '-'
两处有意且文档化的偏差:其一,大写化只对 a–z 做手写映射而不是 Char.uppercase()——后者走平台 locale,JVM 验证与 ohos 设备的输出才可能不一致,确定性优先;其二,上游 name.uppercase().ifEmpty { "-" } 里 ifEmpty 是死代码(Char.uppercase 至少返回一个字符),本 fork 在字符串维度重写后让空输入真正落到 - 兜底。
RandomColors 同样保留上游 setter/getter 形状,颜色换成 ARGB Long,并修正了上游的一个可用性缺陷:空列表时上游 getColorPair() 直接抛 NoSuchElementException,纯 Kotlin 版回退到内置 Material 500 十二对色板(白字 + Red/Pink/Purple/DeepPurple/Indigo/Blue/Cyan/Teal/Green/Orange/DeepOrange/BlueGrey),显式 setColorPairs 仍然优先。构造函数支持注入 kotlin.random.Random 种子——JVM 与设备可复现同一随机序列,这是交叉验证的前提。
3.3 TtfFont:在度量解析器上长出轮廓提取
社区已有的 combo-breaker 适配带过一个"只读度量"的 TTF 解析器(head/hhea/maxp/hmtx/cmap format 4/12)——它渲染在 ArkTS 侧所以不需要轮廓。字母头像必须自己画字形,本 fork 在同一套解析骨架上增读 loca + glyf:
head unitsPerEm、indexToLocFormat(loca 长/短格式)
hhea ascender / descender / lineGap / numberOfHMetrics
maxp numGlyphs
hmtx 前进宽度
cmap format 4 + format 12 二分,缺字形回退 .notdef
loca 字形偏移表(短格式 ×2 / 长格式 u32)
glyf 简单字形(endPts → flags 含 REPEAT 展开 → x/y 增量解码)
复合字形(递归组件 + 平移合成,防换字体翻车)
TrueType 二次曲线有两个容易踩空的约定,flattenContour 都处理了:两个连续 off-curve 点之间隐含一个 on-curve 中点;首点是 off-curve 时以"末点/首点中点"为锚。轮廓按"锚点 → 控制点 → 终点(显式或隐含)"三元组走,每条二次曲线固定展平 20 段。CFF 轮廓(.otf 常见的 CFF 表)明确不支持——构造直接抛、桥转 ok:false,不静默出错字。
3.4 AvatarRenderer:超采样绕数填充
val fontSizePx = size * textSizePercent / 100.0 // 上游公式
val scale = fontSizePx / font.unitsPerEm
// 字体单位 y-up → 位图 y-down,墨迹包围盒中心对准画布中心
for (每像素) {
for (sy in 0..3) for (sx in 0..3) { // 4×4 超采样
水平射线 + nonzero 绕数(升边 +1 / 降边 −1,非零即内)
}
coverage = hits / 16 → 与背景做 straight-alpha 合成
}
上游 build() 对 TextMeasurer 的排版行盒居中——单字母时行盒高(含 ascent/descent)远大于墨迹,视觉上字母偏小且随字体漂移;本 fork 对墨迹包围盒居中,字母视觉真正居中。这是对上游行为的可感知改进,jvmTest 断言的也是墨迹居中;要严格复刻上游,换回 AvatarRenderer 里 offsetX/offsetY 的行盒算法即可。180px 头像单次渲染毫秒级,Demo 滑条实时拖动无压力。
3.5 PngEncoder 与字体子集
PNG 编码器与 qrose / Capturable 过审仓完全同构:RGBA 8-bit、每行 filter 0、zlib stored 未压缩块、adler32、手写 CRC 表——Kotlin/Native 没有 java.util.zip,180px 头像 ≈130 KB 完全可接受。要瘦身只改 IDAT 内部实现,JSON 字段不动。
字体子集由 example/harmonyApp/tools/make_font_subset.py 生成:fontTools 把 DevEco SDK 自带的 HarmonyOS_Sans.ttf 子集化到全量 ASCII,产物 9,068 B 落 rawfile/lademo_font.ttf。两个关键点:drop 变量表(fvar/gvar/avar/cvar/HVAR/VVAR/MVAR/STAT)——HarmonyOS Sans 是可变字体,glyf 存默认实例轮廓,渲染器只读静态 glyf,冻结默认实例最干净;同一份文件两头用——jvmTest/resources/lademo_font.ttf 是它的拷贝,JVM 交叉验证与真机跑同一个字体文件,渲染差异只可能来自平台浮点,不可能来自字库。verify_glyphs.py 校验 A-Z、0-9、-、_ 全覆盖(missing glyphs: NONE)。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libletteravatar_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosletteravatar.so)
│ LetterAvatarRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出坐标又会让页面复制渲染语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("LetterAvatarRender")
fun letterAvatarRender(request: String): String // 出图一口进
| C ABI 符号 | ArkTS 方法 | 覆盖能力 |
|---|---|---|
LetterAvatarRender | letterAvatarRender(req) | 首字母提取 + 尺寸/字高钳制 + 配色(手动/随机)+ 字形光栅化 + PNG |
桥内持单槽字体缓存(key 为路径,字体不会运行期变化),重复调用零重复解析;posix fopen/fread 读沙箱字体文件,64 MB 上限防呆。异常整体 try/catch,Kotlin 异常永远不跨 FFI,非法颜色、越界尺寸、字体读不到全部走同一个 ok:false 通道。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"letterAvatarRender", nullptr, NapiLetterAvatarRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosletteravatar_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosletteravatar_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosletteravatar SHARED IMPORTED)
set_target_properties(ohosletteravatar PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(letteravatar_napi SHARED napi_init.cpp)
target_link_libraries(letteravatar_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosletteravatar)
OHOS_ARCH 为空或 libohosletteravatar.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const letterAvatarRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 字体引导和触发生成
aboutToAppear(): void {
this.bootstrapFont(); // rawfile → filesDir,fs.accessSync 判重
}
private async bootstrapFont(): Promise<void> {
const hostCtx: common.Context | undefined = this.getUIContext().getHostContext();
const sandbox: string = hostCtx.filesDir + '/lademo_font.ttf';
if (!fs.accessSync(sandbox)) {
const raw: Uint8Array = await hostCtx.resourceManager.getRawFileContent('lademo_font.ttf');
// API 18 SDK 返回 Promise,必须 await;写入 filesDir 后把绝对路径交给桥
}
this.fontPath = sandbox;
this.fontReady = true;
this.generate(); // 引导完成自动补发第一张,所有路径必经
this.generateRow(); // 字母墙
}
generate() 组装 { text, size, textSize, fg/bg 或 random, fontPath } 调桥,JSON.parse 结果后走 decodePng:util.Base64Helper().decodeSync(b64) → bytes.buffer.slice(...) → image.createImageSource(...) → createPixelMap() 异步赋给 @State pixelMap。旧的 PixelMap 先 release() 再换新,滚动列表不释放会涨内存。
5.2 页面交互
单页自上而下:标题区、状态行、200px 头像预览 + 信息行(letter=A size=180x180 png=129853B)、名字输入框(改一个字符即时重生成)、头像尺寸滑条 96–320(步进 8)、字母高度滑条 20–80%(步进 1)、五套色板(选中描边)、RandomColors 随机配色开关、生成按钮、字母墙(4 个 128px 随机 Material 配色头像,点击把该名字与随机模式带入主预览)。滑条 onChange 直接触发生成——180px 光栅化毫秒级,无需防抖。
5.3 对比度和错误边界
主按钮蓝色白字,随机开关独立于色板(开启时色板描边消失,明确当前生效来源);ok:false 时错误原文进状态行。fontReady 未就绪前 generate() 直接短路返回,引导完成后自动补发,避免"启动瞬间空图"被误判为渲染失败。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosletteravatar)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + MiniJson + posix 读字体 + 单槽字体缓存
└── harmonyApp/
├── build-profile.json5 signingConfig default(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.letteravatar、versionName 1.2.0-ohos.1
├── tools/ make_font_subset.py / verify_glyphs.py
└── 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/libohosletteravatar_api.h
├── src/main/cpp/types/libletteravatar_napi/{index.d.ts, oh-package.json5}
├── src/main/resources/rawfile/lademo_font.ttf 9 KB HarmonyOS Sans 子集
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :LetterAvatar:jvmTest # 15 项交叉验证
./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 中勾选自动生成,证书写入本机,不进入仓库。
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.letteravatar -m entry
四、完整代码对照
4.1 整体架构
com.pranathicodes.letteravatar/**(commonMain,纯 Kotlin 重写)
│ AvatarSpec + AvatarCreator(模型,语义对齐上游)
│ RandomColors(ARGB Long + Material 默认色板)
│ TtfFont(度量 + glyf 轮廓 + 隐含中点 + 复合字形)
│ AvatarRenderer(4×4 超采样 nonzero 填充 + 墨迹居中)
│ PngEncoder(零依赖 PNG)
example/nativeApp Bridge.kt + MiniJson
│ LetterAvatarRender
libletteravatar_napi.so
│ letterAvatarRender
Index.ets
│ JSON parse + Base64 → PixelMap
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_LetterAvatarGenerator |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版 |
build.gradle.kts(根) | 版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
LetterAvatar/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
LetterAvatar/src/commonMain/.../AvatarSpec.kt | 模型 + Builder + 颜色解析 + 首字母规则 |
LetterAvatar/src/commonMain/.../RandomColors.kt | 随机配色 + 默认 Material 12 对 |
LetterAvatar/src/commonMain/.../TtfFont.kt | 七张表解析 + 轮廓展平 |
LetterAvatar/src/commonMain/.../AvatarRenderer.kt | 超采样光栅化 → IntArray ARGB |
LetterAvatar/src/commonMain/.../PngEncoder.kt | 零依赖 PNG |
LetterAvatar/src/composeMain/ | 上游 Compose 层原样隔离 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、MiniJson、posix 读字体、单槽缓存 |
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 验收页面 |
harmonyApp/tools/*.py | 字体子集生成与字形覆盖校验 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(22 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 模型 | AvatarCreator.setLetter / setAvatarSize / setTextSize / setLetterColor / setBackgroundColor | 链式配置,形状与上游 AvatarCreatorInterface 一致 |
| 模型 | LetterAvatar.letterOf(text) | 首字母规则:trim → 大写 → - 兜底 |
| 模型 | LetterAvatar.parseColorArgb("#RRGGBB") | 桥侧颜色解析 |
| 配色 | RandomColors.getColorPair / setColorPairs / … | 随机配色(空列表回退默认,setter 优先) |
| 字体 | TtfFont.glyphOutline(charCode) | 字形轮廓(font 单位,已展平) |
| 渲染 | AvatarRenderer.render(spec, font) | 位图成品:IntArray ARGB + 宽高 |
| 渲染 | RenderedAvatar.toPng() | PNG 字节 |
| Native | LetterAvatarRender | C ABI JSON 导出 |
| N-API | letterAvatarRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | generate() / generateRow() / decodePng() | 触发出图、字母墙、Base64 → PixelMap |
4.4 ArkTS 与 Kotlin 的边界
const raw = letterAvatarRender(JSON.stringify({
text: 'Alpha', size: 180, textSize: 40,
fg: '#FFFFFF', bg: '#3F51B5', random: false, fontPath: this.fontPath
}));
// JSON.parse(raw) → { ok:true, letter:'A', width:180, height:180, pngBase64:'...' }
val spec = AvatarCreator()
.setLetter(LetterAvatar.letterOf(text))
.setAvatarSize(size).setTextSize(textSize)
.setLetterColor(fg).setBackgroundColor(bg)
.build()
val png = AvatarRenderer.render(spec, fontCache.get(fontPath)).toPng()
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:渲染栈写进库的 commonMain,而不是 example
qrose 把光栅化放 example,是因为它的库语义是"编码器出矩阵",光栅化只是 Demo 的展示手段。LetterAvatarGenerator 的库语义就是"出成品位图"——渲染器即编码器,放 example 等于承认"鸿蒙上库不存在,只有 Demo"。写进 commonMain 还有一个工程红利:JVM 交叉验证能跑与设备完全相同的渲染代码,"内核没漂"才有可信证明。
决策 2:自带 TTF 解析 + glyf 光栅化,不嵌位图字体
ohos 的 Kotlin/Native 没有平台文字栈;combo-breaker 的先例是"Kotlin 只度量、ArkTS fillText 绘制",但那是因为它的库语义是排版(输出段落坐标),绘制本来就属于宿主。字母头像的成品是位图,字形必须在出图侧栅格化。位图点阵方案在 320px 放大下全是马赛克,与上游矢量渲染不可比,也不支持换字体文件。最终实现:TTF 七张表解析 + 二次曲线展平(含隐含中点)+ 4×4 超采样 nonzero 填充——社区这条路上第一个在 Kotlin/Native 侧完成字形栅格化的适配。
决策 3:居中基准从排版行盒改为墨迹包围盒
上游 TextMeasurer 返回排版行盒,build() 对行盒居中——单字母时行盒含整行 ascent/descent,字母视觉偏小且随字体漂移。对墨迹包围盒居中让字母真正落在画布中心,跨字体稳定。这是有意的行为改进:jvmTest 断言墨迹居中,README 适配文档声明差异,要回上游行为只需换 AvatarRenderer 的 offset 计算。
决策 4:确定性优先的 locale 策略与 RandomColors 兜底
大写化只对 a–z 做手写映射(Char.uppercase() 走平台 locale,JVM 与 ohos 输出才可能分叉);RandomColors 空列表回退默认 Material 色板并支持种子注入——上游空列表直接抛异常、随机序列不可复现,两点都会让交叉验证失去意义。显式 setter 的优先级保持上游不变。
决策 5:jvmTest 交叉验证 + 测试分层
渲染逻辑在 commonMain,JVM 与 ohos 跑同一份代码;15 项 jvmTest 分三层钉住:模型层(首字母规则、默认值、钳制、颜色解析)、渲染层(四角纯背景、墨迹存在且水平居中、空输入 - 有墨迹)、编码层(PNG 魔数、IHDR 宽高/位深/颜色类型、stored-deflate 载荷长度公式、IEND 收尾)。另有一个样图导出用例把 P/A/8/- 四张 192px PNG 落到 build 目录供人工目检。ohos 测试任务由根构建全局禁用,ohos 侧只承担编译与链接。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11、JDK 21(Eclipse Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :LetterAvatar:jvmTest
15 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
AvatarSpecTest | 5 | 首字母规则(大小写/空串/空格/非 ASCII)、Builder 默认值与链式、危险参数钳制、#RRGGBB / #AARRGGBB 解析 |
RandomColorsTest | 4 | 自定义对优先、空列表回退默认 12 对、种子序列跨平台确定、字母/背景独立列表 |
RenderPipelineTest | 6 | 七张表解析 + A-Z0-9 轮廓齐全、四角纯背景 + 墨迹水平居中、默认色(Gray)落角、PNG 魔数/IHDR/IDAT 长度公式/IEND、随机色板角像素、空输入 - 兜底 |
另附 AvatarDumpTest 导出 P/A/8/- 四张样图经视觉复核:字形干净、抗锯齿、居中无伪影。
6.3 HAP 验证
:LetterAvatar:jvmTest: BUILD SUCCESSFUL(15 项)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: LetterAvatarRender 为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libletteravatar_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 5.5 MB,内含双 ABI 各三件套(libletteravatar_napi.so ≈65 KB / libohosletteravatar.so / libc++_shared.so)与 9 KB 字体子集;HAP 内 strip 后 KN so arm64 约 1.4 MB、x86_64 约 1.2 MB(debugShared 未 strip 时分别约 2.56 MB / 2.19 MB,含 Kotlin/Native 运行时)。
6.4 功能用例
- 启动后自动完成字体引导并生成默认头像,状态行出现
生成成功,信息行出现letter=… size=180x180 png=…B; - 名字输入
pranathi→ 头像变P;小写自动大写;输入纯数字8→ 大号数字头像;清空输入 → 兜底-; - 头像尺寸滑条 96–320 拖动即时重生成,信息行 size 跟随;
- 字母高度滑条 20–80%:20% 明显变小、80% 接近撑满——与上游
textSize百分比语义一致; - 五套色板点击即变,选中态描边;
- RandomColors 开关开启后每次生成随机换 Material 12 对色板中的一对(截图中的紫色
A即随机色板命中 Purple 档); - 字母墙 4 个 128px 头像随机配色,点击任一即把该名字与随机模式带入主预览;
hdc shell hilog | findstr LetterAvatar无 error,异常路径全部走 JSONerror回显,HAP 不崩。
6.5 验证结论
模型层、文字度量、字形光栅化、PNG 编码、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的渲染会话:首字母提取、尺寸/字高联动、手动与随机配色、字母墙批量出图(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/LetterAvatarGenerator.git
cd LetterAvatarGenerator
export JAVA_HOME="/path/to/jdk-21"
./gradlew :LetterAvatar:jvmTest # 15 项交叉验证
./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.letteravatar -m entry
hdc shell hilog | findstr LetterAvatar
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:LetterAvatar真正编译成ohosArm64/ohosX64动态库;同理,只在 ArkTS 里Text摆字母也不算移植——上游语义是出位图成品; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 新语法线里
Char - Char + Char链式算术受限,first - 'a' + 'A'编不过,要显式(first.code - 'a'.code + 'A'.code).toChar(); String.format在 Kotlin/Native common 里不存在,十六进制拼装要手写(colorToString用移位 + 字符表);- ArkTS 的
@State size与组件内置size属性撞名,编译报Property 'size' is not assignable,改名avatarSize; hvigorw assembleHap报00308018 Unknown Error时先查 build.log——本例是spawn java ENOENT:命令行打包必须把$JAVA_HOME/bin放进 PATH(hvigor 直接 spawnjava,不读 JAVA_HOME);- 可变字体必须 drop 变量表(fvar/gvar/…)再进子集,渲染器只读静态
glyf;CFF 轮廓字体直接不支持,构造抛异常转ok:false; - TrueType 两个隐含约定别想当然:连续 off-curve 点之间的隐含中点、首点 off-curve 时的中点锚——漏掉任何一个,
O/S这类全曲线字形直接画歪; getRawFileContent在 API 18 SDK 返回 Promise,必须await,否则拿到 undefined 写出空字体文件;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 仓库规范:全仓与文档不出现第三方镜像站品牌名,仓库地址只保留 AtomGit。
8.2 已知问题
setFontFamily(FontFamily)没有直接等价物:语义平移为传任意 TrueType TTF 的沙箱路径(fontPath),Demo 用 HarmonyOS Sans 子集;换品牌字体换文件即可,桥不挑字体;- sp / fontScale 不参与:上游字号走 sp(本身与画布密度换算不一致、有溢出隐患),桥统一物理像素 + 百分比,跨端更确定,文档声明差异;
- 非 ASCII 首字母原样输出(大写化只对 a–z);中文等字符按原样查 cmap,Demo 字体子集只含 ASCII,要中文头字母需重新生成子集;
- PNG 未压缩(180px ≈130 KB),列表场景按需生成并复用
PixelMap; - android / iOS 目标剥离(源文件留在树里),发布链一并剥离,本 fork 不发 Maven Central;
- Compose 层(
rememberAvatarCreator)在composeMain等社区 CMP 工具链与上游 Compose 版本对齐后整体接回,当前鸿蒙入口是 JSON 桥。
8.3 未来优化方向
- 评估圆形 / 圆角背景作为可选参数(上游为纯方形,ArkTS 侧可先以
borderRadius过渡); - 把 Demo 验收脚本化(hdc uitest + snapshot_display),补双 ABI 验收矩阵与自动化截图;
- 字体子集工具支持从 Demo 文本自动收集字符集,重新生成并同步 jvmTest 资源;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 若
compose.uiohos 制品出现,composeMain接回commonMain,软件渲染降级为 fallback。
九、总结
9.1 核心难点回顾
上游 LetterAvatar(Compose 语义:build() 出 ImageBitmap)
→ Compose 层隔离(cmp_qrose 模式,三文件迁 composeMain)
→ 渲染栈纯 Kotlin 重写进 commonMain(模型/配色/TTF/光栅化/PNG 五件套)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1,零第三方依赖)
→ ohosArm64 / ohosX64 KLIB + 字体子集(drop 变量表)
→ C ABI(@CName × 1)+ pngBase64 载荷
→ C++ N-API(DisposeString 纪律)
→ ArkTS Base64 → PixelMap
→ ArkUI 真机单页验收(首字母 / 滑条 / 色板 / 随机配色 / 字母墙)
9.2 封装层次
- 模型层:
AvatarSpec+AvatarCreator(首字母规则、百分比字号、默认值、链式 Builder,语义对齐上游); - 配色层:
RandomColors(ARGB Long、Material 默认色板、种子可复现); - 字体层:
TtfFont(七张表解析、轮廓展平、复合字形); - 渲染层:
AvatarRenderer(超采样绕数填充、墨迹居中、alpha 合成)+PngEncoder(零依赖 PNG); - Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:字体引导、页面状态、参数分发、PixelMap 生命周期;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 库的对外语义决定渲染归属:qrose 出矩阵所以光栅化在 example,combo-breaker 出坐标所以绘制在 ArkTS,LetterAvatarGenerator 出位图所以字形光栅化必须下沉到 Kotlin 侧——"算法在库里、平台胶水在 example"这条边界要按每个库的语义重新画,不能照抄先例;
- 零依赖是最强的适配弹药:Compose 隔离完成后整条依赖链只剩标准库,没有制品线选型、没有版本反推、没有降级链——评估一个 CMP 库的适配成本,先看它 Compose 剥掉之后还剩多少第三方依赖;
- 交叉验证的可信度取决于"同代码 + 同资源":渲染器进 commonMain(JVM 跑的就是设备跑的)+ 字体子集双份同步(差异只可能来自平台浮点),15 项像素级断言才站得住——比"能出一张图"高一个量级的证明。
9.4 适配成果
当前适配已完成:
- LetterAvatarGenerator
1.2.0的双 ABI Kotlin/Native 编译(模型语义零漂移,渲染栈纯 Kotlin 重写,零第三方依赖); - 社区首例 Kotlin/Native 侧字形栅格化(TTF 七表解析 + 二次曲线展平 + 4×4 超采样 nonzero 填充);
- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(名字输入、双滑条、五色板、RandomColors 随机配色、字母墙);
- 15 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so + 9 KB 字体子集,约 5.5 MB)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)、适配实录(22 节)和真机效果图。
适配过程 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)