本文记录 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 不编译,场景几何代码逐字节机械移植到纯 Kotlin commonMain 内核(只改 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 / opsPNG 字节数与内核记录的绘制指令数(观测场景复杂度)。
msNative 侧记录 + 光栅化 + 编码耗时(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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 写入 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 wrapper 9.5.0 主动降级;2.2.21 线要求 8.x,见决策 2)
内核依赖零第三方依赖(纯 Kotlin,不拉 kotlinx 制品)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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/15DrawScope 场景函数 + Path/Brush 原语机械移植(只改 import)
emptystates/11同上 + EmptyState 枚举与分发器10 个机械移植 + 1 个拆分(枚举/分发器入内核,Composable 留 composeMain)
icons/3PathMeasure 重采样 + Icon 枚举2 个机械移植 + 1 个拆分
utils/5Painting.kt 是原语集散地(含 TextMeasurer 标签)2 个机械移植 + 3 个手工适配(dp/sp 折叠进设计比例)
根3SketchyColors/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.ktColor(argb: Int) + Long 工厂 + red/green/blue/alpha + copy + lerp + White/Black/Transparent
Geometry.ktOffset / Size / Rect(offset, size) + inflate + Mat2 仿射矩阵(Skia 约定,pre 系列)
Path.ktPath(moveTo/lineTo/quadraticTo/cubicTo/arcTo/addOval/addPath/close/getBounds)+ PathFlatten(命令 → 变换后折线轮廓)
PathMeasure.kt首轮廓弧长采样:length / getPosition / getTangent(sketch() 的全部依赖)
Brush.ktSolid / LinearGradient / RadialGradient + 三个 Compose 同名工厂 + 变换映射
DrawStyle.ktFill / 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.ktTextMeasurer / 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 方法覆盖内核
SketchyRendersketchyRender(req)catalog 目录握手 + 130 场景渲染 + perf 计时,一口进
请求模式字段说明
catalogmode返回三族全量目录(name/displayName/category/title/subtitle)
renderfamily / 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.tomlKotlin 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.ktcatalog / 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.kt256 张 JVM 交叉验证 harness
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鸿蒙适配主文档(18 节)

4.3 关键 API 对照

层次API作用
内核SketchyKernel.catalog()三族 130 场景目录(名称/分类/默认文案)
内核SketchyKernel.record(...)场景 + t + 模式 → 变换烘焙指令流
内核Sketch / EmptyState / Icon 枚举上游枚举原样(含 80 条默认标题/副标题)
facadeink.DrawScope记录器:drawPath/drawCircle/drawLine/drawText + 变换/裁剪/图层
facadeink.PathMeasure首轮廓弧长采样(sketch() 手绘抖动的全部依赖)
光栅化Rasterizer.rasterize(list, ss)指令流 → ARGB 像素 + PNG
NativeSketchyRenderC ABI JSON 导出(catalog / render / perf)
N-APIsketchyRender(req)向 ArkTS 暴露能力
ArkTSrender()触发渲染并刷新 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 功能用例

  1. 启动后 aboutToAppear 完成 catalog 握手,三 Tab 按分类生成全部场景按钮,随即渲染默认场景,状态行出现 生成成功 MorningCoffee 320x320 png=410018B ms=0;
  2. 图像区显示晨间咖啡手绘线稿(咖啡杯、蒸汽、桌面、装饰星点),线条干净、无渲染伪影;
  3. 点"播放",动画循环运行——采样时进度从 0% 推进到 47%,按钮切换为"暂停",无失败状态;
  4. 拖动 t 滑条,动画暂停并按相位即时重渲染;
  5. 切"空状态"Tab,目录按 Weather / Clock and Time / Plants and Things…分组展示;点 No Connection 场景,状态行变为 生成成功 WeatherNoConnection 320x320,图像区渲染云朵断连插画;
  6. 线稿/彩色开关切换后同一场景整体换色(渐变着色生效);
  7. 图标 Tab 五风格切换,Default/Outlined/Sharp/Filled/TwoTone 渲染语义正确(Sharp 方角、Filled 带门洞镂空、TwoTone 低透明度剪影 + 描边);
  8. 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 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让场景内核真正编译成 ohosArm64 / ohosX64 并在设备上渲染出图;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 超采样坐标忘乘 factor:coverage mask 跑在 size × ss 分辨率,而指令流几何是最终像素——第一版光栅化器所有画面只占左上四分之一。教训:降采样前的每一层缓冲分辨率都要和几何坐标系对账(renderDump 的内容包围盒检查正是为抓这类 bug 加的);
  4. PathMeasure 闭合段塌缩:闭合轮廓的"最后一条边"若不作为真实采样段,末段采样全部塌到同一点,sketch() 会把闭合路径误判为开放路径——图标闭合边丢失、evenodd 填充出缺口。修复是把闭合边 append 为真实段,length 端点回到起点,sketch() 的闭合判定自然成立;
  5. @JvmInline 在鸿蒙 Native 编译器不可用(JVM 目标反而强制要求它),common 里的 value class 改普通类并补 equals/hashCode——两端编译器行为不一致时,选两边都能编的写法;
  6. Gradle 9.5.0 在 2.2.21 工具线上不被支持,只改 gradle-wrapper.properties 的 distributionUrl 降到 8.14.1 即可,wrapper jar 通用;
  7. 命令行 hvigorw assembleHap 报 spawn java ENOENT:DevEco 的 jbr\bin 不在 PATH——PackageHap 阶段会另起 java 进程,仅设 JAVA_HOME 不够;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;DevEco 模拟器是 x86_64,只编 arm64 连安装都过不去;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. stored PNG 的字节数只由分辨率决定(320×320 恒为 410018B),不要拿它当内容指纹——验证内容变化要看像素或指令数 ops;
  11. 根构建的 /build/ 与 .idea/* 是根锚定模式,盖不住嵌套目录——example/harmonyApp/.gitignore 要单独追加(build/.hvigor/oh_modules/entry/libs 下 .so/.hap);
  12. 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 三条经验

  1. 绘制型 CMP 库的适配单位是"API 面"而不是"文件":先 grep 钉死库真正用到的 Compose 表面(本项目 16 个类型 + 10 个操作),facade 只做这一份清单;同名同签名让场景文件"只改 import"就能进内核——机械移植比任何手工重写都更能保证"几何没漂";
  2. 工具链选线看依赖可达性,两条线经验不互套:本库内核零 kotlinx 依赖,就选已审最广的 2.2.21 线并接受 Gradle 降级;KStore 因为 coroutines 2.4 元数据必须升 2.4.20 线——版本新旧不是依据,"当天制品可达性 + 最少解释"才是;
  3. 绘制库的交叉验证是像素不是断言:存储库用 51 项单测钉状态机,绘制库用 256 张全目录渲染 + 内容包围盒 + 视觉抽查钉几何。renderDump 曾在一次运行里同时抓出超采样缩放与 PathMeasure 闭合段两个真 bug——可复现的像素基线比任何"看起来没问题"都有说服力。

9.4 适配成果

当前适配已完成:

  • Sketchy 1.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。

参考文档

Logo

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

更多推荐