开源鸿蒙平台 CMP 三方库「Sketchy」适配全流程
本文记录
Sketchy接入 OpenHarmony 的完整过程,覆盖手绘绘制库盘点、Compose 绘制面无法真编译的路线选择、零依赖图形 facade 设计、场景代码机械移植、Kotlin/Native 软件光栅化、ohosArm64/ohosX64双 ABI 动态库、C ABI / N-API 桥接、ArkUI 三 Tab 页面、HAP 构建和模拟器验收。
Sketchy是 Compose Multiplatform 手绘插画库:30 张插画(Sketch枚举)、80 个空状态(EmptyState枚举,自带默认标题/副标题)、20 个图标(Icon枚举 × Default / Outlined / Sharp / Filled / TwoTone 五种风格),共 130 个场景。每个场景默认是纯墨线手绘(线稿模式),同一份绘制代码可整体切换为彩色填充(彩色模式);插画与空状态全部由一个 4 秒循环相位t驱动环境动画(蒸汽上升、指针扫动、雨落、星光闪烁);插画/空状态绘制在 320 单位设计网格上、图标在 24 单位网格上,等比缩放到任意尺寸。本次适配与KStore的"真编译整库"路线有本质区别:Sketchy的全部价值都在androidx.compose.ui的DrawScope/Path/Brush绘制面里,社区 CMP 工具链(Compose 1.9.2 / Kotlin 2.2.21-1.0.0)与上游(Compose 1.11.1 / Kotlin 2.3.21 / AGP 9.3.3)无法共存于一个 Gradle 构建,因此走的是社区早期已审路线(qrose / ProShape / Korner)的加强版:上游 Compose 源码原样隔离进composeMain不编译,场景几何代码逐字节机械移植到纯 KotlincommonMain内核(只改 import),由自研ink图形 facade 记录绘制指令,Kotlin/Native 软件光栅化器出图,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/Sketchy.git
上游仓库: Ali0092/Sketchy
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Sketchy 为 Compose Multiplatform 应用提供"手绘感"这一件事的完整答案:SketchyIllustration 一行放进布局就是一张会呼吸的插画,SketchyEmptyState 是带默认文案的完整空状态视图,SketchyIcon 是单色调手绘图标。它的手绘感来自两处算法:所有图标路径先经 sketch() 重采样——按弧长步进、沿法线做确定性伪随机抖动、再用短曲线重连,让每个几何边都带"手抖";线稿/彩色双模式靠 SketchyStyle 把彩色模式的材质槽全部读回 Transparent,同一份场景代码不分支地渲染成纯线稿或全彩。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、wasm 共享。OpenHarmony 应用同样需要这套插画能力,但上游构建(Kotlin 2.3.21 / Gradle 9.5.0 / AGP 9.3.3 / Compose Multiplatform 1.11.1)不会生成 OpenHarmony 变体。
绘制型 CMP 库的适配障碍和存储型完全不同:如果只把示例页面改成 ArkTS 重画一遍,页面可以显示图片,却无法证明手绘场景本身在 OpenHarmony ARM64 设备上完成编译、链接和执行——那只是把 130 个场景翻译一遍,几何一改就漂移。适配需要同时验证场景内核、指令记录、光栅化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 制品。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Gradle 9.5.0 / AGP 9.3.3 与鸿蒙工具链无法共存于一个 Gradle 构建;社区 CMP 是 Kotlin 2.2.21-1.0.0 线,必须整体降线。 |
| 绘制面跨不了编译 | 全库 44 个源文件的绘制代码直接依赖 androidx.compose.ui.graphics.DrawScope / Path / Brush / TextMeasurer,社区 Maven 上没有这些包的 ohos 制品。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 渲染语义跨不了桥 | Compose Painter 语义(Skia 矢量、blur、平台字体)在 C ABI 另一侧不存在,必须自建光栅化。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:场景内核的纯 Kotlin 重表达(几何代码零改动)、根构建替换(cmp_qrose 模式)和"C ABI / N-API / PNG"桥接层。手绘几何完全由上游原版代码负责,ArkTS 只负责调用 Native、解码 PNG 和展示结果。
1.2 库提供的能力
Demo 覆盖桥接的全部入口,对应 130 个场景:
- 插画(30):
Sketch枚举全量——Weather / Plants and Things / Signboards 三张全场景画 + Featured 六张 elaborate 场景(晨间咖啡、家庭工位、雨天窗边等)+ Productivity / Finance / Fitness / Food Delivery / Travel / Music 主题的 Onboarding 插画; - 空状态(80):
EmptyState枚举全量,按 Weather(云朵吉祥物)/ Clock and Time(双铃闹钟)/ Plants and Things / Lined Man(单线小人)/ Signboards(路牌)/ Network(网络硬件)/ Connectivity / Content / Saved & Commerce / Everyday 十类分组,每类同一主体换表情与道具; - 图标(20 × 5 风格):Home / Search / Heart / Star / Bell / Settings / User / Mail / Calendar / Clock / Cart / Edit / Trash / Camera / Image / Folder / Download / Upload / Share / Lock,五种渲染风格全量可切;
- 双模式:每个插画/空状态可在"线稿"(墨线 + 少量点缀色)与"彩色"(渐变着色、接触阴影、高光、光晕)间切换,同一份场景代码;
- 动画:
t相位 0…1 循环驱动环境动效,Demo 提供播放/暂停(90ms 定时器)与进度滑条两种驱动方式; - 目录握手:
mode=catalog返回全场景目录(名称、显示名、分类、默认文案),ArkTS 页面据此构建三个 Tab 的分组按钮,不硬编码任何场景表。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
kind / scene | 回显场景族与场景名。 |
width / height | 输出 PNG 尺寸(正方形,size 参数指定)。 |
bytes / ops | PNG 字节数与内核记录的绘制指令数(观测场景复杂度)。 |
ms | Native 侧记录 + 光栅化 + 编码耗时(320px 实测 < 1ms)。 |
pngBase64 | 渲染结果,ArkTS Base64Helper 解码后经 image.createImageSource 出 PixelMap。 |
上游完整能力(sketch() 手绘重采样、线稿/彩色语义、PathMeasure 弧长采样、渐变材质、图层挖空)全部随内核进入 .so;JVM 侧保留 256 张全目录渲染交叉验证(见 6.2),证明 ohos .so 里执行的场景几何没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 31 个场景文件的几何代码逐字节原样移植(仅重写 import 行);上游 Compose 源文件零改动隔离进 composeMain;内核新增代码全部是"承载"而非"重写"(图形 facade、分发器、入口)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 androidApp / composeApp / iosApp 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI(SketchyRender)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖(不拉 kotlinx 制品,MiniJson 手写平面 JSON 解析),工具链钉社区已审的 2.2.21-1.0.0 线。 |
| UI 完整 | 三 Tab 覆盖全部 130 场景:分组按钮、线稿/彩色开关、动画播放/暂停、t 滑条、图标五风格、状态行。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。当前社区 Compose(1.9.2)追上上游(1.11.1)之前,
composeMain里的SketchyIllustration等 Composable 不在鸿蒙编译;ArkTS Demo 把内核渲染出的每一帧画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 Compose API 依赖面、确定"内核提取 + 机械移植"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1、上游源码隔离 composeMain
第 3 阶段:内核建模 ── ink 零依赖图形 facade、DrawScope 指令记录器、31 个场景机械移植、SketchyKernel 入口
第 4 阶段:光栅化与原生桥接 ── 纯 Kotlin 扫描线光栅化器、stored PNG、@CName C ABI、C++ N-API
第 5 阶段:ArkUI 宿主 ── catalog 目录握手、三 Tab 分组按钮、动画定时器、PixelMap 生命周期
第 6 阶段:示例与验证 ── 256 张 JVM 交叉验证、双 ABI 链接、HAP、签名、模拟器安装运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
library/src/composeMain/ 上游 Compose 源码整体迁入(44 个文件原样保留,不注册源集、不编译)
library/src/commonMain/
└─ com/sketchy/library/ink/ 零依赖图形 facade(Color/Path/PathMeasure/Brush/DrawScope 记录器/5×7 点阵字库)
└─ com/sketchy/library/ 场景移植件 + 枚举分发器 + SketchyKernel 入口
example/nativeApp/ Kotlin/Native 动态库(Bridge + Rasterizer + PngEncoder + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
androidApp / composeApp / iosApp 上游示例工程,留盘不进 settings
doc/ 本文章与效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_sketchy"。上游的 AGP / Compose 编译器 / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 写入 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 9.5.0 主动降级;2.2.21 线要求 8.x,见决策 2) |
| 内核依赖 | 零第三方依赖(纯 Kotlin,不拉 kotlinx 制品) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 26.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.sketchy |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
先 grep 全库的 androidx.compose import,把 API 面钉死再设计 facade:
| 模块 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
illustrations/ | 15 | DrawScope 场景函数 + Path/Brush 原语 | 机械移植(只改 import) |
emptystates/ | 11 | 同上 + EmptyState 枚举与分发器 | 10 个机械移植 + 1 个拆分(枚举/分发器入内核,Composable 留 composeMain) |
icons/ | 3 | PathMeasure 重采样 + Icon 枚举 | 2 个机械移植 + 1 个拆分 |
utils/ | 5 | Painting.kt 是原语集散地(含 TextMeasurer 标签) | 2 个机械移植 + 3 个手工适配(dp/sp 折叠进设计比例) |
| 根 | 3 | SketchyColors/SketchyStyle 纯色板 + Composable 包装 | 2 个机械移植 + 1 个拆分 |
全库使用的 Compose API 面收敛为:类型 Color / Path / PathFillType / PathMeasure / Offset / Rect / Size / Brush / DrawScope / Stroke / Fill / StrokeCap / StrokeJoin / PathEffect / BlendMode / Paint / TextMeasurer;操作 drawPath(178 处)/ drawCircle / drawLine / drawText / withTransform(41 处)/ rotate(28 处)/ clipPath / scale / translate / saveLayer + BlendMode.Clear——这就是 facade 要覆盖的全部表面,一处不多一处不少。
1.4 页面边界
标题区 SKETCHY · OPENHARMONY / 副题(手绘插画 / 空状态 / 图标 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 插画 / 空状态 / 图标
场景区 按 category 分组的手绘按钮(catalog 握手后动态生成,点击即渲染)
图像区 Image(PixelMap),生成成功前占位,替换前 release 旧图
控件区 线稿/彩色 Toggle(默认线稿)、播放/暂停(90ms 定时器 t += 0.0225 循环)、t 进度 Slider(拖动即暂停)
图标风格区 Default / Outlined / Sharp / Filled / TwoTone 五按钮(仅图标 Tab)
状态行 生成成功 <scene> <W>x<H> png=<N>B ms=<N>;失败时 error 原文
页面启动时先调 mode=catalog 拉全场景目录,随即渲染默认场景 illustration/MorningCoffee(t=0、线稿)。
第 2 阶段:目标与依赖打通
2.1 根构建替换
settings.gradle.kts 把 eazytec 仓库写在最前(顺序错了配置期直接 Unresolved reference: ohosArm64):
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_sketchy"
include(":library")
include(":example:nativeApp")
gradle/libs.versions.toml 收敛为一行插件钉版,gradle.properties 写死 JDK 21 与 fork 版本 1.0.9-ohos.1,wrapper 从 9.5.0 降到 8.14.1(只改 distributionUrl,wrapper jar 通用)。
2.2 库模块重写
library/build.gradle.kts 从"AGP KMP + Compose + 发布"收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm()
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
}
}
上游全部源码 git mv 进 library/src/composeMain/——它不是注册源集,Gradle 根本看不见,天然获得"原样保留、零编译"的隔离(qrose 的 Painter 隔离同款)。
2.3 钉版理由(为什么回到 2.2.21-1.0.0 / Gradle 8.14.1)
Sketchy 内核不依赖任何 kotlinx 制品,工具链选择只看一条:ohosArm64() / ohosX64() 目标只存在于 HarmonyOS Kotlin 插件(eazytec Maven)。社区已审的两条线——2.2.21-1.0.0(qrose / ProShape / Korner,Gradle 8.14.1)与 2.4.20-0.1.0-07(KStore,Gradle 9.x,为读 coroutines 2.4 元数据而升)——本库没有后者那条"依赖逼升"的理由,选已审最广的 2.2.21 线最稳;该线不支持 Gradle 9,wrapper 从上游 9.5.0 降 8.14.1(qrose 同款处置)。两条工具链线的经验不能互相套用(KStore 文章同款结论)。
2.4 隔离后的编译验证
.\gradlew.bat :library:compileKotlinJvm # BUILD SUCCESSFUL(内核 + facade 全量过 JVM 编译)
第 3 阶段:内核建模(本适配的核心一刀)
3.1 ink 图形 facade
com.sketchy.library.ink 包提供第 1.3 节钉死的 API 面的纯 Kotlin 等价物,命名与签名与 Compose 逐一对齐(Color(0xFF0D1B2A) Long 工厂、copy(alpha=…)、lerp、Path.arcTo(rect, startAngleDegrees, sweepAngleDegrees, forceMoveTo)、Brush.verticalGradient(colors=, startY=, endY=)……),分九个文件:
| 文件 | 内容 |
|---|---|
Color.kt | Color(argb: Int) + Long 工厂 + red/green/blue/alpha + copy + lerp + White/Black/Transparent |
Geometry.kt | Offset / Size / Rect(offset, size) + inflate + Mat2 仿射矩阵(Skia 约定,pre 系列) |
Path.kt | Path(moveTo/lineTo/quadraticTo/cubicTo/arcTo/addOval/addPath/close/getBounds)+ PathFlatten(命令 → 变换后折线轮廓) |
PathMeasure.kt | 首轮廓弧长采样:length / getPosition / getTangent(sketch() 的全部依赖) |
Brush.kt | Solid / LinearGradient / RadialGradient + 三个 Compose 同名工厂 + 变换映射 |
DrawStyle.kt | Fill / Stroke(width, miter, cap, join, pathEffect) / StrokeCap / StrokeJoin / PathEffect.dashPathEffect / BlendMode / Paint |
DrawScope.kt | 记录器:矩阵栈 + 裁剪栈 + 图层栈 + drawPath/drawCircle/drawLine + withTransform/rotate/scale/translate/clipPath + drawContext.canvas.saveLayer/restore,产出 SketchyDrawList |
Text.kt | TextMeasurer / TextStyle / FontWeight / TextAlign / Constraints / drawText(逐字符换行) |
Font5x7.kt | 内置 5×7 点阵字库(A–Z、0–9、常用符号),文本以像素矩形轮廓入指令流 |
关键设计:变换在记录期烘焙。drawPath 被调用时,路径当场经当前矩阵展平成设备坐标轮廓,画笔几何(渐变起止/圆心/半径)同步映射,描边宽度按矩阵行列式缩放——指令流里没有任何矩阵状态,光栅化器消费的是纯设备空间几何。这是"记录器在库、光栅化器在 example"这一分层能成立的前提。
文本是唯一的有损近似:上游用平台 TextMeasurer 量真实字体,鸿蒙内核用 5×7 点阵字库替(场景里全部是设备读数与手绘路牌字号,语义无损、字形近似,已列入已知限制)。
3.2 机械移植
场景文件的移植用脚本一次完成——删掉所有 androidx.compose.* import 行,在 package 行后插入 import com.sketchy.library.ink.*:
awk '/^import androidx\.compose/{next}
/^package /{print; print ""; print "import com.sketchy.library.ink.*"; next}
{print}' "$SRC" > "$DST"
31 个文件的几何代码逐字节未动——移植前后 diff 只有 import 区。三个 Composable 包装文件(SketchyIllustrations.kt / EmptyState.kt / Icons.kt)拆分:枚举与 drawXxx 分发器是纯函数,抽进内核(SketchyScenes.kt / EmptyStates.kt / IconScenes.kt);@Composable 部分留在 composeMain 原文件里对照。
3.3 内核入口
SketchyKernel 是无 Compose 的公开入口(对应 qrose 适配中 Qrose.encodeQr 的角色):
object SketchyKernel {
fun catalog(): List<FamilyInfo> // illustration 30 / empty 80 / icon 20,含分类与默认文案
fun record(family, name, t, colorful, sizePx,
iconStyle, tint, strokeWidth): SketchyDrawList
}
record 按 family 建记录器:插画/空状态设计比例 sizePx / 320f,图标 sizePx / 24f——上游"320dp 画布 + withTransform(scale(fit))"的数学被折叠进 d() 一个乘法,几何等价。SketchyStyle(palette, outlined = !colorful, textMeasurer) 的线稿/彩色语义原样复用。
第 4 阶段:光栅化与原生桥接(技术难点)
4.1 软件光栅化器
example/nativeApp 的 Rasterizer 是纯 Kotlin(commonMain,ohos 与 JVM 共用):
SketchyDrawList(变换烘焙指令流)
→ 逐指令 coverage mask:扫描线填充(nonzero / even-odd,ss 倍超采样)
填充:多轮廓 + fillType
描边:段四边形 + miter/round/bevel 连接 + round/square/butt 端帽 + 虚线分段
→ 裁剪栈(clipPath 交集)+ 离屏图层(saveLayer + BlendMode.Clear 线稿挖空)
→ 线性/径向渐变逐像素求值 + 预乘 SrcOver 合成
→ ss×ss 盒滤波降采样(alpha 加权去预乘)→ stored PNG(零依赖 zlib)
性能:JVM 上 320px、ss=2 单帧约 20ms(含 PNG 编码);Native 侧模拟器实测 < 1ms 记录+光栅化。动画按 90ms 节流驱动完全够用。
PngEncoder 用 zlib stored 块不压缩——Kotlin/Native 没有 java.util.zip,qrose 同款取舍;320×320 输出恒定 410018 字节(尺寸只由分辨率决定,与画面内容无关),短文本场景完全可接受。
4.2 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libsketchy_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohossketchy.so)
│ SketchyRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出位图又会让 ArkTS 复制场景语义,所以选择 C ABI + JSON + PNG。
4.3 Kotlin/Native 导出函数
@CName("SketchyRender")
fun sketchyRender(request: String): String = try {
when (MiniJson.parse(request).str("mode").ifBlank { "render" }) {
"catalog" -> catalogJson() // 全场景目录
"perf" -> perf(json) // recordAvgMs / rasterAvgMs 计时
else -> render(json) // family/scene/t/colorful/size/ss/iconStyle/tint/strokeWidth
}
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SketchyRender | sketchyRender(req) | catalog 目录握手 + 130 场景渲染 + perf 计时,一口进 |
| 请求模式 | 字段 | 说明 |
|---|---|---|
catalog | mode | 返回三族全量目录(name/displayName/category/title/subtitle) |
render | family / scene / t / colorful / size / ss | 主体渲染,icon 族另收 iconStyle / tint / strokeWidth |
perf | 同 render + iterations | 循环计时统计 |
MiniJson 手写平面 JSON 解析(逗号切顶层、引号状态机跳串内逗号),不拉 kotlinx.serialization——桥的入参是扁平键值对,C 签名 const char* → const char* 永远不用改(qrose 同款决策)。
4.4 C++ N-API 与 CMake
add_library(ohossketchy SHARED IMPORTED)
set_target_properties(ohossketchy PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(sketchy_napi SHARED napi_init.cpp)
target_link_options(sketchy_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(sketchy_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohossketchy)
OHOS_ARCH 为空或 libohossketchy.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。--no-undefined 让 CMake 链接期就验证 KN so 符号齐全。N-API 回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回;KN 返回串必须经 libohossketchy_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。
export const sketchyRender: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 目录握手与渲染管线
aboutToAppear(): void {
const raw = sketchyRender(JSON.stringify({ mode: 'catalog' }));
this.families = (JSON.parse(raw) as CatalogResult).families ?? []; // 三 Tab 按分组生成按钮
this.render(); // 默认 MorningCoffee
}
private render(): void {
const req: RenderRequest = {
mode: 'render', family: this.family(), scene: this.scene(), t: this.t,
colorful: this.colorful, size: 320, ss: 2,
iconStyle: this.iconStyle, tint: '#2F8FA8', strokeWidth: 2.2, // 仅 icon 族序列化
};
const res = JSON.parse(sketchyRender(JSON.stringify(req))) as RenderResult;
if (res.ok && res.pngBase64) {
this.pixelMap?.release(); // 替换前释放旧图
const bytes = util.Base64Helper.decodeSync(res.pngBase64);
const source = image.createImageSource(bytes);
source.createPixelMap().then((pm) => { this.pixelMap = pm; source.release(); });
}
this.status = res.ok
? `生成成功 ${res.scene} ${res.width}x${res.height} png=${res.bytes}B ms=${res.ms}`
: `失败: ${res.error}`;
}
5.2 页面交互
三 Tab 自上而下:分组场景按钮(Flex wrap,点击即选中并渲染)、图像区(Image(pixelMap),Contain)、共用控件区(线稿/彩色 Toggle、播放/暂停、t Slider)、图标 Tab 额外五风格按钮、状态行。动画用 setInterval 90ms 推进 t = (t + 0.0225) % 1,拖动滑条即暂停播放按值渲染——t 由 ArkTS 外驱,是"动画相位不跨 C ABI"设计(见 8.2)在 UI 侧的自然形态。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohossketchy)+ cxxShared + copyKnSo* + renderDump
│ └── src/
│ ├── commonMain/kotlin/example/nativeapp/ Rasterizer / PngEncoder / MiniJson(JVM 与 ohos 共用)
│ ├── ohosMain/kotlin/example/nativeapp/ Bridge.kt(@CName("SketchyRender"))
│ └── jvmMain/kotlin/example/nativeapp/ RenderDump.kt(256 张交叉验证 harness)
└── harmonyApp/
├── build-profile.json5 signingConfig "default"(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.sketchy、versionName 1.0.9-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、POST_BUILD 拷三件套
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohossketchy_api.h
├── src/main/cpp/types/libsketchy_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
.\gradlew.bat :example:nativeApp:renderDump # 256 张 JVM 交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so + copyKnSo* 回填
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
命令行打包需要 DevEco 的 JBR、Node 与 ohpm 同时进 PATH(少一个 JBR 就在 PackageHap 阶段报 spawn java ENOENT):
set DEVECO_SDK_HOME=E:\DE\DevEco Studio\sdk
set PATH=E:\DE\DevEco Studio\jbr\bin;E:\DE\DevEco Studio\tools\node;E:\DE\DevEco Studio\tools\ohpm\bin;%PATH%
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库。
hdc list targets -v
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap # 模拟器接受 unsigned
hdc shell aa start -a EntryAbility -b org.terminator.ohos.sketchy
四、完整代码对照
4.1 整体架构
com.sketchy.library/**(场景内核:31 个移植件 + 枚举分发器 + SketchyStyle/Colors)
│ drawXxxScene(t, style) 几何代码 = 上游逐字节
com.sketchy.library.ink/**(图形 facade)
│ DrawScope 记录器 → 变换烘焙 SketchyDrawList
example/nativeApp Rasterizer + PngEncoder
│ 扫描线/描边/渐变/裁剪/图层 → stored PNG
Bridge.kt + MiniJson
│ SketchyRender(catalog / render / perf)
libsketchy_napi.so
│ sketchyRender
Index.ets
│ JSON parse + Base64 → PixelMap + @State 更新
▼
ArkUI 页面(三 Tab 全场景验收)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_sketchy、上游 app 工程移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 1.0.9-ohos.1、全局禁用 Ohos*Test* 任务 |
library/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,无 Compose/AGP/发布插件 |
library/src/composeMain/** | 上游 44 个源文件原样隔离(不编译) |
library/.../ink/*.kt(9 文件) | 零依赖图形 facade + DrawScope 记录器 + 5×7 字库 |
library/.../SketchyKernel.kt | catalog / record 无 Compose 入口 |
library/.../{SketchyScenes,EmptyStates,IconScenes}.kt | 枚举 + 纯函数分发器 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、renderDump |
nativeApp/.../Rasterizer.kt | 扫描线光栅化器(填充/描边/虚线/渐变/裁剪/图层/预乘合成) |
nativeApp/.../Bridge.kt | @CName("SketchyRender") + 三模式分发 + MiniJson |
nativeApp/src/jvmMain/.../RenderDump.kt | 256 张 JVM 交叉验证 harness |
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 | 鸿蒙适配主文档(18 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | SketchyKernel.catalog() | 三族 130 场景目录(名称/分类/默认文案) |
| 内核 | SketchyKernel.record(...) | 场景 + t + 模式 → 变换烘焙指令流 |
| 内核 | Sketch / EmptyState / Icon 枚举 | 上游枚举原样(含 80 条默认标题/副标题) |
| facade | ink.DrawScope | 记录器:drawPath/drawCircle/drawLine/drawText + 变换/裁剪/图层 |
| facade | ink.PathMeasure | 首轮廓弧长采样(sketch() 手绘抖动的全部依赖) |
| 光栅化 | Rasterizer.rasterize(list, ss) | 指令流 → ARGB 像素 + PNG |
| Native | SketchyRender | C ABI JSON 导出(catalog / render / perf) |
| N-API | sketchyRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | render() | 触发渲染并刷新 PixelMap/状态行 |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(sketchyRender(JSON.stringify({
mode: 'render', family: 'illustration', scene: 'MorningCoffee',
t: 0.25, colorful: true, size: 320, ss: 2,
}))) as RenderResult;
// res.ok === true; res.pngBase64 → Base64Helper.decodeSync → createImageSource → PixelMap
val draw = SketchyKernel.record("illustration", "MorningCoffee", t = 0.25f, colorful = true, sizePx = 320)
val image = Rasterizer.rasterize(draw, ss = 2) // → pngBase64 进响应
两者之间只传输扁平 JSON 字符串与 PNG Base64,不传输 Kotlin 对象、矩阵或未校验的动态结构。
五、关键决策说明
决策 1:内核提取 + 机械移植,而不是真编译或 ArkTS 重写
三条路的取舍:真编译整库(KStore 路线)要求依赖有 ohos 制品,androidx.compose.ui 没有,也轮不到本仓去补一套 Compose 发行版;在 ArkTS 里重写 130 个场景等于把几何抄一遍,上游一改就漂移,CMP 适配失去意义;所以走内核提取——但和 qrose"内核天然纯 Kotlin"不同,Sketchy 的内核(场景绘制代码)本身就长在 Compose 类型上,因此补一层与 Compose 同名同签名的零依赖 facade,让场景文件只改 import 就能进内核。几何代码逐字节未动是本次适配的验收底线。
决策 2:工具线钉 2.2.21-1.0.0、Gradle 降 8.14.1
ohosArm64() 只在 HarmonyOS Kotlin 插件里,eazytec Maven 上有两条线:2.2.21-1.0.0(qrose / ProShape / Korner 已审,配 Gradle 8.14.1)与 2.4.20-0.1.0-07(KStore 为读 kotlinx 2.4 元数据而升,配 Gradle 9.x)。本库内核零 kotlinx 依赖,没有升线理由,选已审最广的 2.2.21 线;上游 Gradle 9.5.0 在该线不被支持,主动降 8.14.1(只改 distributionUrl)。选线依据是当天的依赖可达性,不是版本新旧。
决策 3:根构建替换(cmp_qrose 模式),上游示例留盘
上游根构建(AGP 9.3.3 / Compose 1.11.1 / vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。androidApp / composeApp / iosApp 留盘不进 settings.gradle.kts——等社区 Compose 工具链追上,它们随时可以恢复。
决策 4:单一 SketchyRender + 扁平 JSON + t 外驱动画
一个 C ABI 覆盖 catalog/render/perf 三模式,C 签名 const char* → const char* 永不改变;动画相位 t 由 ArkTS 定时器/滑条驱动、逐帧调用渲染,而不是把 Compose Animatable/infiniteTransition 搬过桥——订阅式动画过不了同步 C 签名,外驱 t 让"同一场景任意帧可复现",反而更适合自动化截图验收。入场动画(appear/graphicsLayer)属 Compose 包装层,明确不跨桥(见 8.2)。
决策 5:renderDump 交叉验证——256 张图是"几何没漂"的唯一可信证明
example/nativeApp 带一个 JVM harness(renderDump 任务),用与 ohos .so 完全相同的内核与光栅化器渲染全目录:插画 30×2(线稿+彩色)、空状态 80 线稿 + 96 张中的 16 张彩色、图标 20×5 风格,共 256 张,零失败、4.6 秒跑完。几何代码逐字节未动 + 256 张可复现 PNG + 视觉抽查(渐变/光晕/图层挖空/点阵字/手绘抖动),三层证据钉死"内核没漂"——这是绘制型库对 KStore"51 项 desktopTest"的对应物:存储库验证状态机,绘制库验证像素。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 10(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 26.0(E:\DE\DevEco Studio)、OpenHarmony 命令行工具(hvigor / hdc)与 OpenHarmony 模拟器(x86_64)。
6.2 JVM 交叉验证
.\gradlew.bat :example:nativeApp:renderDump
# dumped 256 scenes (0 failed) to build\renderDump in 4865 ms
| 检查项 | 结果 |
|---|---|
| 全目录渲染 | 256 张全部成功(插画 60 / 空状态 96 / 图标 100) |
| 单帧性能 | 约 20ms / 320px 帧(JVM,ss=2,含 PNG 编码与白底合成) |
| 内容包围盒 | 全部铺满设计画布(该检查曾抓出超采样缩放 bug,见 8.1) |
| 视觉抽查 | 晨间咖啡彩色(渐变/光晕/接触阴影)、云朵空状态线稿(×眼/闪电/虚线地面)、路牌空状态(“404” 点阵字居中可读)、Home 图标(五边形轮廓 + 门洞镂空 + 手绘抖动)全部通过 |
6.3 HAP 验证
:library:compileKotlinJvm: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
llvm-nm -D: SketchyRender 为全局 T 符号(x86_64 实测)
BuildNativeWithNinja: finished(双 ABI 编出 libsketchy_napi.so,--no-undefined 通过)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 9.8 MB,内含双 ABI 各三件套;libohossketchy.so arm64 约 6.3MB、x86_64 约 6.1MB(debugShared 含 Kotlin/Native 运行时与全部场景内核)。
6.4 功能用例
- 启动后
aboutToAppear完成 catalog 握手,三 Tab 按分类生成全部场景按钮,随即渲染默认场景,状态行出现生成成功 MorningCoffee 320x320 png=410018B ms=0; - 图像区显示晨间咖啡手绘线稿(咖啡杯、蒸汽、桌面、装饰星点),线条干净、无渲染伪影;
- 点"播放",动画循环运行——采样时进度从 0% 推进到 47%,按钮切换为"暂停",无失败状态;
- 拖动 t 滑条,动画暂停并按相位即时重渲染;
- 切"空状态"Tab,目录按 Weather / Clock and Time / Plants and Things…分组展示;点
No Connection场景,状态行变为生成成功 WeatherNoConnection 320x320,图像区渲染云朵断连插画; - 线稿/彩色开关切换后同一场景整体换色(渐变着色生效);
- 图标 Tab 五风格切换,Default/Outlined/Sharp/Filled/TwoTone 渲染语义正确(Sharp 方角、Filled 带门洞镂空、TwoTone 低透明度剪影 + 描边);
png=410018B恒定、ms=0是两个可解释的观测值:stored PNG 尺寸只由分辨率决定,与画面无关;Native 记录+光栅化低于 1ms。
6.5 验证结论
场景内核、图形 facade、软件光栅化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在 OpenHarmony 模拟器上成功安装并进入前台,页面展示了真实的渲染会话:目录握手、130 场景渲染、动画驱动与双模式切换(见下节效果图)。arm64 真机与签名验收按流程留给设备侧人工执行,链路各层已分别验证(符号导出、链接、模拟器运行)。
七、运行效果
7.1 运行截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Sketchy.git
cd Sketchy
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :library:compileKotlinJvm # 内核 JVM 编译
.\gradlew.bat :example:nativeApp:renderDump # 256 张交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so + 回填
# HAP(DevEco 工具链进 PATH 后)
set DEVECO_SDK_HOME=E:\DE\DevEco Studio\sdk
set PATH=E:\DE\DevEco Studio\jbr\bin;E:\DE\DevEco Studio\tools\node;E:\DE\DevEco Studio\tools\ohpm\bin;%PATH%
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry\build\default\outputs\default\entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.sketchy
hdc shell hilog | grep -i Sketchy
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让场景内核真正编译成
ohosArm64/ohosX64并在设备上渲染出图; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 超采样坐标忘乘 factor:coverage mask 跑在
size × ss分辨率,而指令流几何是最终像素——第一版光栅化器所有画面只占左上四分之一。教训:降采样前的每一层缓冲分辨率都要和几何坐标系对账(renderDump 的内容包围盒检查正是为抓这类 bug 加的); - PathMeasure 闭合段塌缩:闭合轮廓的"最后一条边"若不作为真实采样段,末段采样全部塌到同一点,
sketch()会把闭合路径误判为开放路径——图标闭合边丢失、evenodd 填充出缺口。修复是把闭合边 append 为真实段,length端点回到起点,sketch()的闭合判定自然成立; @JvmInline在鸿蒙 Native 编译器不可用(JVM 目标反而强制要求它),common 里的 value class 改普通类并补equals/hashCode——两端编译器行为不一致时,选两边都能编的写法;- Gradle 9.5.0 在 2.2.21 工具线上不被支持,只改
gradle-wrapper.properties的distributionUrl降到 8.14.1 即可,wrapper jar 通用; - 命令行
hvigorw assembleHap报spawn java ENOENT:DevEco 的jbr\bin不在 PATH——PackageHap 阶段会另起 java 进程,仅设JAVA_HOME不够; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;DevEco 模拟器是 x86_64,只编 arm64 连安装都过不去; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - stored PNG 的字节数只由分辨率决定(320×320 恒为 410018B),不要拿它当内容指纹——验证内容变化要看像素或指令数
ops; - 根构建的
/build/与.idea/*是根锚定模式,盖不住嵌套目录——example/harmonyApp/.gitignore要单独追加(build/.hvigor/oh_modules/entry/libs 下 .so/.hap); - ArkTS import 的 so 名、
nm_modname、index.d.ts导出名、CMake target 名四个名字必须逐字对齐,错一个的表现都是"能安装、一点就崩"。
8.2 已知问题
- 文本渲染是内置 5×7 点阵字库,非平台字体:场景内标签(设备读数、路牌字样)语义无损、字形近似;自由排版文本不在内核范围;
- 无 blur 滤镜:上游本身用三层半透明椭圆伪造接触阴影(
contactShadow),内核原样保留该语义,非新增近似; - 渐变仅支持等距 stops(上游全部用法即如此)、纯色/线性/径向三类画笔;Compose 特有
PathOp、ImageFilter不在 facade; - 动画相位
t由 ArkTS 外驱;入场动画(appear/graphicsLayer 缩放淡入)属 Compose 包装层,明确不跨桥,等 Compose 工具链接回时自然恢复; - PNG 未压缩(stored):320×320 恒 410KB,动画逐帧传输在真机上若偏重,只换
PngEncoder的 IDAT 实现即可,桥接口不动; - android / ios / wasmJs 目标与 Composable 包装剥离进
composeMain(源文件留在树里),本 fork 不发 Maven Central; harmonyApp/entry/oh-package-lock.json5由 DevEco 首次 sync 重新生成(仓库不提交陈旧锁文件);- HAP 安装必须使用与 bundle name
org.terminator.ohos.sketchy匹配的签名;arm64 真机与签名验收留给设备侧人工流程。
8.3 未来优化方向
- IDAT 换压缩实现(或 ArkTS 侧解码裸 ARGB 通道),降低动画帧传输体积;
- 补 arm64 真机验收矩阵与
uitest自动化截图脚本(qrose 的 capture.py 模式),把 130 场景截图全部脚本化; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查、renderDump 像素基线对比和 HAP 构建;
- 跟踪社区 Compose 工具链(当前 1.9.2)对上游 1.11 的追平进度:追平后
composeMain接回commonMain,内核与光栅化路径不变、降级为 fallback。
九、总结
9.1 核心难点回顾
上游 Sketchy(44 文件,全部长在 androidx.compose 绘制面上)
→ 盘点 Compose API 面(DrawScope/Path/Brush/PathMeasure/TextMeasurer…)
→ 根构建替换(cmp_qrose 模式,Gradle 9.5.0 → 8.14.1)
→ 上游源码零改动隔离 composeMain
→ ink 零依赖图形 facade(与 Compose 同名同签名)
→ 31 个场景文件机械移植(几何逐字节未动,只改 import)
→ SketchyKernel(catalog 130 场景 / record 变换烘焙指令流)
→ 纯 Kotlin 扫描线光栅化器(填充/描边/虚线/渐变/裁剪/图层/预乘)
→ stored PNG + @CName("SketchyRender") 单一 C ABI
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON/Base64/PixelMap
→ ArkUI 三 Tab 模拟器验收(目录握手 / 动画 / 双模式 / 五风格)
9.2 封装层次
- 内核层:31 个上游场景移植件 + 枚举分发器 +
SketchyStyle线稿/彩色语义(几何逐字节未动); - facade 层:
ink图形包——类型、绘制入口、变换/裁剪/图层栈、5×7 文本; - 光栅化层:指令流 → coverage mask → 预乘合成 → 降采样 ARGB → PNG;
- Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、目录握手、动画定时器、PixelMap 生命周期与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和模拟器运行。
9.3 三条经验
- 绘制型 CMP 库的适配单位是"API 面"而不是"文件":先 grep 钉死库真正用到的 Compose 表面(本项目 16 个类型 + 10 个操作),facade 只做这一份清单;同名同签名让场景文件"只改 import"就能进内核——机械移植比任何手工重写都更能保证"几何没漂";
- 工具链选线看依赖可达性,两条线经验不互套:本库内核零 kotlinx 依赖,就选已审最广的 2.2.21 线并接受 Gradle 降级;KStore 因为 coroutines 2.4 元数据必须升 2.4.20 线——版本新旧不是依据,"当天制品可达性 + 最少解释"才是;
- 绘制库的交叉验证是像素不是断言:存储库用 51 项单测钉状态机,绘制库用 256 张全目录渲染 + 内容包围盒 + 视觉抽查钉几何。renderDump 曾在一次运行里同时抓出超采样缩放与 PathMeasure 闭合段两个真 bug——可复现的像素基线比任何"看起来没问题"都有说服力。
9.4 适配成果
当前适配已完成:
Sketchy1.0.9 全部 130 个场景(30 插画 / 80 空状态 / 20 图标 × 5 风格)的双 ABI Kotlin/Native 编译(场景几何代码逐字节未动);ink零依赖图形 facade(9 文件)与SketchyKernel无 Compose 入口(catalog / record);- 纯 Kotlin 软件光栅化器(扫描线 / 描边几何 / 虚线 / 线性径向渐变 / 裁剪栈 / 图层 + Clear / 预乘合成 / 超采样抗锯齿);
- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,--no-undefined链接通过); - DevEco Stage ArkUI 三 Tab 验收页面(目录握手、动画播放/暂停、t 滑条、线稿/彩色、五风格、状态行);
- 256 张 JVM 交叉验证全绿(零失败,约 20ms/帧)+ HAP 构建(双 ABI 六件套 so,约 9.8MB)、模拟器安装运行与效果图;
- AtomGit 项目文档、OpenHarmony README(中英双份)、本文章与本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐




所有评论(0)