本文记录 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 / pngBase64PNG 字节数与 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Temurin 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 8.13 钉到社区标准线;2.2.21 线要求 8.x)
第三方依赖零——隔离 Compose 后 commonMain 只有 Kotlin 标准库
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.letteravatar
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游文件依赖形态处置
AvatarCommon.ktTextMeasurer / CanvasDrawScope / ImageBitmap / Compose Color / sp / Constraints整体迁 composeMain,语义重写为纯 Kotlin AvatarSpec
AvatarCompose.kt@Composable / remember / LocalDensity整体迁 composeMain(未来 CMP 工具链对齐再接回)
RandomColors.ktCompose ColorcomposeMain 留原件;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 方法覆盖能力
LetterAvatarRenderletterAvatarRender(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.tomlKotlin 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.kt1 个 C ABI、MiniJson、posix 读字体、单槽缓存
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
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 字节
NativeLetterAvatarRenderC ABI JSON 导出
N-APIletterAvatarRender(req)向 ArkTS 暴露能力
ArkTSgenerate() / 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 项用例全部通过:

测试类用例数钉住什么
AvatarSpecTest5首字母规则(大小写/空串/空格/非 ASCII)、Builder 默认值与链式、危险参数钳制、#RRGGBB / #AARRGGBB 解析
RandomColorsTest4自定义对优先、空列表回退默认 12 对、种子序列跨平台确定、字母/背景独立列表
RenderPipelineTest6七张表解析 + 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 功能用例

  1. 启动后自动完成字体引导并生成默认头像,状态行出现 生成成功,信息行出现 letter=… size=180x180 png=…B;
  2. 名字输入 pranathi → 头像变 P;小写自动大写;输入纯数字 8 → 大号数字头像;清空输入 → 兜底 -;
  3. 头像尺寸滑条 96–320 拖动即时重生成,信息行 size 跟随;
  4. 字母高度滑条 20–80%:20% 明显变小、80% 接近撑满——与上游 textSize 百分比语义一致;
  5. 五套色板点击即变,选中态描边;
  6. RandomColors 开关开启后每次生成随机换 Material 12 对色板中的一对(截图中的紫色 A 即随机色板命中 Purple 档);
  7. 字母墙 4 个 128px 头像随机配色,点击任一即把该名字与随机模式带入主预览;
  8. hdc shell hilog | findstr LetterAvatar 无 error,异常路径全部走 JSON error 回显,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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :LetterAvatar 真正编译成 ohosArm64 / ohosX64 动态库;同理,只在 ArkTS 里 Text 摆字母也不算移植——上游语义是出位图成品;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 新语法线里 Char - Char + Char 链式算术受限,first - 'a' + 'A' 编不过,要显式 (first.code - 'a'.code + 'A'.code).toChar();
  4. String.format 在 Kotlin/Native common 里不存在,十六进制拼装要手写(colorToString 用移位 + 字符表);
  5. ArkTS 的 @State size 与组件内置 size 属性撞名,编译报 Property 'size' is not assignable,改名 avatarSize;
  6. hvigorw assembleHap 报 00308018 Unknown Error 时先查 build.log——本例是 spawn java ENOENT:命令行打包必须把 $JAVA_HOME/bin 放进 PATH(hvigor 直接 spawn java,不读 JAVA_HOME);
  7. 可变字体必须 drop 变量表(fvar/gvar/…)再进子集,渲染器只读静态 glyf;CFF 轮廓字体直接不支持,构造抛异常转 ok:false;
  8. TrueType 两个隐含约定别想当然:连续 off-curve 点之间的隐含中点、首点 off-curve 时的中点锚——漏掉任何一个,O/S 这类全曲线字形直接画歪;
  9. getRawFileContent 在 API 18 SDK 返回 Promise,必须 await,否则拿到 undefined 写出空字体文件;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 仓库规范:全仓与文档不出现第三方镜像站品牌名,仓库地址只保留 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.ui ohos 制品出现,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 三条经验

  1. 库的对外语义决定渲染归属:qrose 出矩阵所以光栅化在 example,combo-breaker 出坐标所以绘制在 ArkTS,LetterAvatarGenerator 出位图所以字形光栅化必须下沉到 Kotlin 侧——"算法在库里、平台胶水在 example"这条边界要按每个库的语义重新画,不能照抄先例;
  2. 零依赖是最强的适配弹药:Compose 隔离完成后整条依赖链只剩标准库,没有制品线选型、没有版本反推、没有降级链——评估一个 CMP 库的适配成本,先看它 Compose 剥掉之后还剩多少第三方依赖;
  3. 交叉验证的可信度取决于"同代码 + 同资源":渲染器进 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐