本文记录 SpriteView-KMP 接入 OpenHarmony 的完整过程,覆盖 Compose UI 组件库盘点、社区 Compose ohos 制品线选型、整库真编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

SpriteView-KMP 是基于 Compose Multiplatform 的精灵图(Sprite Sheet)动画组件库:一张按网格排布的雪碧图,按帧序循环切出子矩形连续绘制,即得到 2D 帧动画。上游把能力分成三层——SpriteState(协程驱动的帧序列状态机:start() / stop() / currentFrame StateFlow)、SpriteSpec + SpriteSheet(按屏宽四档 ScreenCategory 响应式选图,未提供档位回退 default)、SpriteView composable 与 drawSpriteView()(Canvas 取子矩形绘制,支持水平 / 垂直 / 双向翻转)。本次适配与社区早期路线(qrose 的"零依赖内核提取")有本质区别:社区 Maven 的 Compose 制品线已到 1.12.0-0.1.0-07,连 components-resources 都发布了 ohos klib,因此不需要提取内核、也不需要重写渲染——整库 commonMain(Compose 组件本体、协程引擎、响应式选图)被原样编译成 OpenHarmony 动态库,一个协程帧循环真实地跑在真机上,ArkUI 页面通过 JSON 桥逐帧采样驱动精灵动画,作为验收宿主。

项目地址: AtomGit/oh-tpc/SpriteView-KMP.git

上游仓库: stevdza-san/SpriteView-KMP

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

SpriteView 为 Compose Multiplatform 应用提供"精灵图 → 动画"这一件事的完整答案:rememberSpriteState(totalFrames, framesPerRow, animationSpeed) 一行建引擎,SpriteView() 一个 composable 出动画,drawSpriteView() 让你把同样的动画画进任意 DrawScope。多档 SpriteSheet 让同一动画在小屏 / 正常屏 / 大屏 / 平板上各自使用最合适的分辨率,选图逻辑(screenWidth.parseCategory() + 回退链)在库内闭环。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、wasm 共享。OpenHarmony 应用同样需要轻量 2D 帧动画能力,但上游构建(Kotlin 2.0.21 / Gradle 8.7 / AGP 8.5.2 / compose-multiplatform 1.7.0)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮和画布,却无法证明公共引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:协程帧循环引擎、响应式选图逻辑、帧矩形数学、Compose 组件本体的 ohos 编译、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.0.21 / Gradle 8.7 / AGP 8.5.2 与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵commonMain 声明了 compose runtime / foundation / material3 / ui / components-resources 与 lifecycle-runtime-compose,每一个都要在 ohos 侧可解析或显式处置。
资源加载链断裂SpriteSheet.image 是 Compose DrawableResource,SpriteSpec.imageBitmap 走 imageResource() 解码——OHOS 没有 Compose composition 宿主,这条链不能按原样运行。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:整库源码的 ohos 编译(根构建替换,cmp_qrose 模式)与 C ABI / N-API 桥接层。动画与选图语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和栅格化当前子帧。

1.2 库提供的能力

Demo 提供一个真实的精灵动画会话,对应桥的全部入口:

  • 建会话(create):SpriteState(totalFrames, framesPerRow, animationSpeed) 原样构造并 start()——协程帧循环在 Dispatchers.Default 上自跑,currentFrame StateFlow 按 animationSpeed 步进;
  • 采样(frame):返回当前帧号、运行状态,以及按 SpriteView composable 同款数学算出的渲染矩形(row = frame / framesPerRow、column = frame % framesPerRow、srcOffset = (column × frameWidth, row × frameHeight));
  • 播放 / 停止(start / stop):映射 SpriteState.start() / stop();停止时帧复位到 0 是上游语义,Demo 特意展示;
  • 销毁(destroy):SpriteState.cleanup() 取消协程作用域,返回剩余会话数;
  • 响应式选图(select):以真实 SpriteSpec + 真实 SpriteSheet(携带 DrawableResource)执行选图,返回 ScreenCategory、命中的档位(含 default 回退)与该档单帧尺寸;
  • 版本握手(version):spriteVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应字段一览:

字段作用
ok操作是否成功,失败时 error 带原文。
op / id回显本次操作与引擎会话 id。
frame / running当前帧号与运行状态(frame 采样 op 的核心输出)。
row / column / srcX / srcY / srcWidth / srcHeight渲染矩形,ArkTS drawImage 直接消费。
category / chosen / resource选图结果:屏宽分档、命中档位、精灵图资源名。

引擎侧完整能力(协程帧循环、StateFlow 出口、start/stop 复位语义、四档选图与 default 回退、翻转枚举)全部在上游 commonMain 里原样随编译进入 .so。

1.3 实现适配

维度要求
代码复用sprite 模块全部 commonMain 文件(8 个)原样编译,不提取、不重写;新增源文件仅 2 个(getScreenSize.ohos.kt expect/actual 补位 + SpriteHeadless.ohos.kt 无头门面),全部在 ohosMain。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 用上游自己的 Compose 1.7.0 制品线做交叉编译验证;库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 android / iOS / wasmJs 目标源码留盘不进构建。
桥接稳定2 个 C ABI(SpriteDispatch / SpriteVersion)+ UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游一致(compose 1.7.0 线用于 jvm 验证目标),ohos 侧换社区线(1.12.0-0.1.0-07):换线不换版;上游声明但源码从未 import 的 lifecycle-runtime-compose / material3 移除,coroutines 由隐式传递改为显式声明。
UI 完整单页三 Tab 覆盖全部桥接入口:动画画布、播放 / 停止 / 重置引擎、速度滑杆、四档翻转、屏宽滑杆 + 档位开关的选图面板、操作日志与版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 目前没有 Compose composition 宿主,视觉由 ArkUI Canvas 复刻;帧序列、选图与矩形计算全部来自 Kotlin 侧真实对象——与 qrose / eungabi / compose-shimmer 等已过审库同一条路线。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认社区 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、Compose 双线声明、skiko 冲突排除
第 3 阶段:无头门面建模   ── SpriteHeadless:会话管理、真实 SpriteSpec 选图、帧矩形采样
第 4 阶段:桥协议与原生桥接 ── SpriteDispatch JSON 契约、@CName C ABI、C++ N-API、CMake
第 5 阶段:ArkUI 宿主     ── Canvas 逐帧栅格化、控制面板、选图面板、日志与握手
第 6 阶段:示例与验证     ── 双 ABI so、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
sprite/                               库模块(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
sprite/src/commonMain/                上游全部源码,原样编译(零改动)
sprite/src/ohosMain/                  新增:getScreenSize actual + SpriteHeadless 无头门面(仅有的 2 个新文件)
sprite/src/desktopMain/               上游 desktop actual,jvm 验证目标使用
sprite/src/androidMain|iosMain|wasmJsMain/
                                      上游 actual,留盘不进构建
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API、ArkTS 页面、rawfile 四档精灵图
images/                               上游 README 素材(四档精灵图同时供 Demo 使用)
doc/                                  适配全流程文章与真机效果图
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.md / README.upstream.md        fork 首页 / 上游原文备份
*.upstream                            上游构建文件备份(settings / 根 build / 模块 build / catalog)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :sprite 与 :example:nativeApp,rootProject.name = "ohos_sprite"。上游的 AGP、compose-multiplatform 插件别名、vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。上游仓库没有 example 目录(只有库模块和 README 配图),因此没有需要移出构建的示例工程。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle9.6.1(wrapper 升级自上游 8.7;2.4.20 线支持 Gradle 9.x)
Compose(ohos 线)org.jetbrains.compose.* 1.12.0-0.1.0-07(社区发布 ohos klib)
Compose(jvm 验证线)1.7.0(上游 1.1.0 所用版本主体,Maven Central desktop 制品)
coroutinesohos 线 1.11.0-0.1.0-07 / jvm 验证线 1.8.0
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.sprite
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点

上游 commonMain 共 8 个源文件,逐个盘点对 Compose / 协程的依赖形态:

文件依赖形态ohos 处置
domain/SpriteState.ktkotlinx-coroutines(StateFlow / CoroutineScope / delay / collect)原样编译(引擎本体);文件尾部的 rememberSpriteState @Composable 一并编译
domain/ScreenCategory.kt纯 Kotlin 枚举原样编译
domain/SpriteFlip.kt纯 Kotlin 枚举原样编译
domain/SpriteSheet.ktorg.jetbrains.compose.resources.DrawableResource原样编译(社区线有 components-resources ohos klib)
domain/SpriteSpec.kt@Composable + imageResource + parseCategory原样编译(选图逻辑在 Native 真实执行;imageBitmap 属性无宿主时不被调用)
component/SpriteView.ktCanvas / withTransform / drawImage 等 Compose UI原样编译(帧矩形数学被门面镜像复用)
util/Functions.kt纯 Kotlin(Float.parseCategory())原样编译
util/getScreenSize.kt@Composable expect原样编译,ohos actual 在 ohosMain 补位

expect 声明全库只有 1 处:getScreenWidth() / getScreenHeight()。上游给了 4 个平台 actual(android LocalConfiguration、desktop onGloballyPositioned、ios LocalWindowInfo、wasmJs)。ohos 补第 5 个,镜像 ios 实现去掉 UIScreen 密度换算:

// sprite/src/ohosMain/kotlin/com/stevdza_san/sprite/util/getScreenSize.ohos.kt
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun getScreenWidth(): Dp = LocalWindowInfo.current.containerSize.width.dp

@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun getScreenHeight(): Dp = LocalWindowInfo.current.containerSize.height.dp

OHOS 当前没有 composition 宿主,这条路径不会被无头 Demo 触发;它的存在让 commonMain 完整编译,并为将来社区宿主就位后 SpriteView() 直接可用留好接缝。

1.4 页面边界
标题区          SPRITEVIEW · OPENHARMONY / 版本握手行(CMP 精灵图动画库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        动画 / 响应式选图 / 关于
动画 Tab        Canvas 画布(16ms 轮询 op=frame,子矩形栅格化 + SpriteFlip 翻转变换)
                播放/停止、重置引擎按钮,animationSpeed 滑杆(20-200ms,重建会话)
                翻转芯片(无/水平/垂直/双向),frame 计数与当前图状态行
选图 Tab        screenWidth 滑杆(0-1200px)、四档提供开关(small/normal/large/tablet)
                重新选图按钮 → 真实 SpriteSpec 结果面板(ScreenCategory/命中档位/单帧尺寸/回退说明)
关于 Tab        版本握手、库简介、ArkTS → N-API → C ABI → 引擎 分层、仓库与社区链接
日志区          带时间戳的操作日志(最新在前,上限 60 条)

页面启动时先调 spriteVersion() 做握手,再以默认 screenWidth = 360 执行一次 op=select 完成首次选图并解码 rawfile 精灵图,随后 op=create 建引擎会话并启动 16ms 轮询。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上 Compose ohos 制品线的可达性,用 .module 元数据逐个验证:

org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / jvmApiElements …
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07     同上
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07                     同上
org.jetbrains.compose.components:components-resources:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements / ohosArm64ResourcesElements / ohosX64ApiElements …
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published

三个关键结论:其一,components-resources 有 ohos 变体——这是本库能走"整库真编译"路线(而非 qrose 的"编码器提内核"路线)的决定性前提,SpriteSheet 的 DrawableResource 与 SpriteSpec 的 imageResource import 都靠它解析;其二,社区线不发布 *-desktop 子制品,jvm 验证目标不能从这条线解析,必须双线声明;其三,coroutines 1.11.0-0.1.0-07 依赖 stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 钉版由它反推——旧线 2.2.21-1.0.0 读不了 2.4 元数据,直接报 Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin。

同时验证出本线独有的 skiko 冲突:compose-ui 依赖完整版 org.jetbrains.skiko:skiko klib,而 components-resources 额外声明 skiko-ohosarm64-api 与 skiko-ohosarm64-fusionrenderer——三者在 KLIB 加载期撞 unique_name,在链接期撞 Skia 符号(duplicate symbol: SkIRect::intersect(...))。处置是依赖声明上排除后两个制品,只保留完整版 skiko klib(先于已过审仓库踩到:compose-shimmer 只依赖 runtime+foundation 没碰过这条边)。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose 用的)。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 2.2.21 只能读到 2.3.0,编译期直接报元数据不兼容。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.0.21 语法,2.4.20 编译器向下兼容,源码级兼容成本为零;Gradle 方面 2.4.20 线支持 9.x(compose-shimmer 同款 9.6.1 先例),上游 wrapper 8.7 升级到 9.6.1。

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_sprite"

include(":sprite")
include(":example:nativeApp")
2.3 编译真实引擎

sprite/build.gradle.kts 收敛为纯 KMP 模块。Compose 依赖按目标线声明(ohosMain 走社区线、desktopMain 走上游 1.7.0 线),commonMain 保持线无关:

kotlin {
    applyDefaultHierarchyTemplate()
    jvm("desktop") { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
    ohosArm64()
    ohosX64()

    sourceSets {
        ohosMain.dependencies {
            api(libs.compose.runtime)        // 1.12.0-0.1.0-07
            api(libs.compose.foundation)
            api(libs.compose.ui)
            api("org.jetbrains.compose.components:components-resources:...") {
                // 排除 skiko-ohos{arm64,x64}-api / -fusionrenderer(skiko 冲突,见 2.1)
            }
            implementation(libs.kotlinx.coroutines.core)
        }
        val desktopMain by existing
        desktopMain.dependencies {
            api(libs.compose.runtime.jvm)    // 1.7.0(上游版本主体)
            api(libs.compose.foundation.jvm)
            api(libs.compose.ui.jvm)
            api(libs.compose.resources.jvm)
            implementation(libs.kotlinx.coroutines.core.jvm)
        }
    }
}

依赖声明的可见性由 source set 的 dependsOn 链保证:commonMain 编译到 ohosArm64 时,ohosMain 上声明的依赖参与 classpath——上游源码一行不改即可双线编译。两个说明:上游还声明了 androidx.lifecycle:lifecycle-runtime-compose:2.8.3,但全部源文件无一处 import(协程是它传递进来的),fork 将其移除并把 coroutines 显式化;上游 commonMain 的 material3 依赖同样无 import,随根构建替换一并消失。desktopMain 用 by existing 取用(自定义命名目标的 source set 没有静态 accessor)。实测:

./gradlew :sprite:compileKotlinDesktop                          # BUILD SUCCESSFUL
./gradlew :sprite:compileKotlinOhosArm64 :sprite:compileKotlinOhosX64   # BUILD SUCCESSFUL

引擎与选图的全部 commonMain 文件一个没动,只有 ohosMain 下 2 个新文件。

2.4 测试依赖的处置

上游仓库没有测试源集(无 commonTest),因此没有"turbine 无 ohos 制品"式的依赖问题;根构建仍保留全局禁用 Ohos*Test* 任务的守卫,与其它 cmp_qrose 模式仓库纪律一致。交叉验证落在编译级::sprite:compileKotlinDesktop 用上游自己的 Compose 1.7.0 / coroutines 1.8.0 制品线编译同一份 commonMain + desktopMain,证明源码在换回上游依赖时依然成立——"换线不换版"的对称证据;运行级语义由真机 Demo 验收(见第六节)。

第 3 阶段:无头门面建模

3.1 JSON 契约
com.stevdza_san.sprite/**(上游引擎 + 选图 + 矩形数学,原样编译)
        ↓ SpriteState 帧循环 / SpriteSpec 选图 / SpriteView 矩形
SpriteHeadless.ohos.kt(无头门面:会话 + 快照)
        ↓ 扁平 JSON
@CName C ABI(libohossprite.so):SpriteDispatch / SpriteVersion
        ↓ UTF-8 JSON
N-API libsprite_napi.so:spriteDispatch(req) / spriteVersion()
        ↓
ArkUI Index.ets(三 Tab 验收页)

请求与响应:

select : {"op":"select","screenWidth":360,"small":true,"normal":true,"large":true,"tablet":true}
  → {"ok":true,"category":"Small","chosen":"small","frameWidth":149,"frameHeight":179,"resource":"sprite_small"}
create : {"op":"create","totalFrames":9,"framesPerRow":3,"animationSpeed":50,"frameWidth":149,"frameHeight":179,"start":true}
  → {"ok":true,"op":"create","id":1,"frame":0,"running":true}
start/stop : {"op":"stop","id":1} → {"ok":true,"op":"stop","id":1,"frame":0,"running":false}
frame  : {"op":"frame","id":1}
  → {"ok":true,"frame":5,"running":true,"row":1,"column":2,"srcX":506,"srcY":303,"srcWidth":253,"srcHeight":303}
destroy: {"op":"destroy","id":1} → {"ok":true,"op":"destroy","id":1,"sessionsLeft":0}
失败通道: {"ok":false,"op":"...","error":"..."}

Demo 兔子(上游 README 同款素材,实为忍者角色雪碧图)的四档单帧尺寸:small 149x179、normal 253x303、large 377x451、tablet 619x740,default 档为 sprite_normal。

3.2 门面为什么放在库模块 ohosMain

SpriteState.framesPerRow 是库内 internal 成员,桥模块访问不到;门面进库模块后既能取 internal 成员,又让"提升引擎为公开无头面"成为库自身的能力。门面职责:

object SpriteHeadless {
    fun select(screenWidth, hasSmall, hasNormal, hasLarge, hasTablet): SelectionSnapshot
    fun create(totalFrames, framesPerRow, animationSpeedMillis,
               frameWidth, frameHeight, autoStart): Int
    fun start(id): Boolean
    fun stop(id): Boolean          // 上游语义:停 + 帧复位 0
    fun sample(id): FrameSnapshot? // 帧号 + 运行态 + row/column/srcX/srcY/srcW/srcH
    fun destroy(id): Boolean       // SpriteState.cleanup()
}

三个设计点:其一,select 构造的是真实 SpriteSpec + 真实 SpriteSheet,选图结果读 spec.screenCategory / spec.spriteSheet——parseCategory 的 0-360-600-800 分档与 default 回退链全部是上游原版执行(DrawableResource 直接构造,无需资源管线;社区 1.12 线的构造函数是 @InternalResourceApi(id, items),传空 items 并 OptIn);其二,sample 的行列数学逐行镜像 SpriteView composable 的 Canvas 取帧代码;其三,会话表用 AtomicInt 自旋锁保护(Kotlin/Native 没有 synchronized),派发调用间隔 16ms 起步,自旋开销为零。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("SpriteDispatch")
fun spriteDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"op":"dispatch","error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺参、未知会话 id、非正参数全部走同一个 ok:false 通道,ArkTS 侧统一渲染。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libsprite_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohossprite.so)
  │ SpriteDispatch / SpriteVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制动画语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("SpriteDispatch")
fun spriteDispatch(request: String): String   // select/create/start/stop/frame/destroy 一口进

@CName("SpriteVersion")
fun spriteVersion(): String = "1.1.0-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
SpriteDispatchspriteDispatch(req)会话生命周期 + 选图 + 帧矩形采样
SpriteVersionspriteVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"spriteDispatch", nullptr, NapiSpriteDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"spriteVersion", nullptr, NapiSpriteVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打请求字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossprite_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossprite_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohossprite SHARED IMPORTED)
set_target_properties(ohossprite PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(sprite_napi SHARED napi_init.cpp)
target_link_libraries(sprite_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohossprite)

OHOS_ARCH 为空或 libohossprite.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const spriteDispatch: (request: string) => string;
export const spriteVersion: () => string;

Gradle 侧 example/nativeApp 声明双 ABI binaries.sharedLib { baseName = "ohossprite" },linkDebugSharedOhosArm64/X64 完成后由 copyKnSo* 任务把 libohossprite.so 与 konan 的 libc++_shared.so 拷入 example/harmonyApp/entry/libs/{arm64-v8a,x86_64}/。

第 5 阶段:ArkUI 宿主

5.1 启动握手和触发操作
aboutToAppear(): void {
  this.version = spriteVersion();      // 版本握手:来自真实 Native 调用
  this.reselect(true);                 // op=select + op=create(首次选图并建会话)
  this.tickTimer = setInterval(() => {
    this.tick();                       // 16ms 轮询 op=frame
  }, 16);
}

private tick(): void {
  const res = JSON.parse(spriteDispatch(JSON.stringify({ op: 'frame', id: this.sessionId })));
  this.frame = res.frame; this.running = res.running; this.draw();
}

draw() 把 op=frame 返回的子矩形画到 Canvas。真机首验暴露了一个 SDK 行为差异:ArkUI Canvas 的 9 参 drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 在该版本上对 PixelMap 不裁剪(整图绘制),修复改为经典方案——先 rect + clip 裁出目标帧窗口,再把整张雪碧图按 s = dw / sw 缩放平移绘制,使源子矩形恰好落在窗口内(只依赖可靠的 5 参 drawImage)。

5.2 页面交互

动画 Tab 自上而下:画布(左上角 frame 计数、左下角当前图与档位)、播放 / 停止 / 重置引擎三按钮、animationSpeed 滑杆(释放时重建会话)、四档翻转芯片。选图 Tab:screenWidth 滑杆、四档提供开关、重新选图按钮与结果面板(ScreenCategory / 命中档位 / 单帧尺寸 / 回退说明)。每条日志带 [HH:mm:ss] 时间戳,→ 请求 ← 响应,最新在前、上限 60 条。

5.3 对比度和错误边界

播放深绿、停止红、重置蓝,语义与破坏性对齐;ok:false 时错误原文进状态行与日志;Native 调用本身抛错同样被 try/catch 收进日志区。精灵图放 resources/rawfile(四档 PNG 即上游 images/ 原图),getRawFdSync + createImageSource + createPixelMap + getImageInfo 解码并缓存,翻转到画布上以 pivot 居中的 translate + scale(±1) 实现,镜像 SpriteView 的 withTransform(scale(...))。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohossprite)+ copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.sprite、versionName 1.1.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohossprite_api.h
        ├── src/main/cpp/types/libsprite_napi/{index.d.ts, oh-package.json5}
        ├── src/main/resources/rawfile/       四档精灵图 PNG
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(三 Tab)
6.2 构建、签名和安装
./gradlew :sprite:compileKotlinDesktop                                # jvm 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap --mode module -p module=entry@default \
  -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

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.sprite -m entry

四、完整代码对照

4.1 整体架构

com.stevdza_san.sprite/**(上游 commonMain 原样编译)
    │ SpriteState(协程帧循环)/ SpriteSpec+SpriteSheet(响应式选图)
    │ SpriteView+drawSpriteView(Canvas 取帧数学)/ ScreenCategory / SpriteFlip
SpriteHeadless.ohos.kt(无头门面:会话表 + 选图快照 + 帧快照)
    │ SpriteDispatch / SpriteVersion
libsprite_napi.so
    │ spriteDispatch / spriteVersion
Index.ets
    │ JSON parse + @State 更新 + Canvas 栅格化
    ▼
ArkUI 真机页面(单页三 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_sprite、双模块 include
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + compose 双线(ohos 1.12.0-0.1.0-07 / jvm 1.7.0)+ coroutines 双线钉版
build.gradle.kts(根)版本 1.1.0-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21 路径、kotlin.native.* 开关(上游本无此文件)
sprite/build.gradle.kts纯 KMP:jvm(“desktop”) + ohosArm64 + ohosX64,Compose 双线依赖 + skiko 排除
sprite/src/ohosMain/.../getScreenSize.ohos.ktexpect/actual 补位(全库唯一接缝的第 5 个 actual)
sprite/src/ohosMain/.../SpriteHeadless.ohos.kt无头门面(会话 + 选图 + 帧矩形)
example/nativeApp/build.gradle.kts双 ABI sharedLib、copyKnSo* 回填 harmonyApp
nativeApp/.../Bridge.kt2 个 C ABI、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(clip+缩放平移的单帧栅格化)
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎SpriteState(totalFrames, framesPerRow, animationSpeed)帧序列状态机,协程循环按步进推进 currentFrame
引擎start() / stop()启动 / 停止并复位到第 0 帧(上游语义)
引擎currentFrame / isRunningStateFlow 状态出口
引擎cleanup()取消内部 CoroutineScope
选图SpriteSpec(screenWidth, default, small?, normal?, large?, tablet?)parseCategory 四档分档 + default 回退,选出 spriteSheet
渲染SpriteView() / drawSpriteView()Canvas 取子矩形绘制,SpriteFlip 翻转变换
门面SpriteHeadless.select/create/start/stop/sample/destroyUI 无关引擎出口
NativeSpriteDispatch / SpriteVersionC ABI JSON 导出
N-APIspriteDispatch(req) / spriteVersion()向 ArkTS 暴露能力
ArkTSreselect() / tick() / draw()选图、16ms 采样、子帧栅格化

4.4 ArkTS 与 Kotlin 的边界

// 16ms 一拍:帧号与渲染矩形全部来自 Native 的真实 SpriteState
const res = JSON.parse(spriteDispatch(JSON.stringify({ op: 'frame', id: this.sessionId })));
// clip + 缩放平移:源子矩形 (res.srcX, res.srcY, res.srcWidth, res.srcHeight) 落进帧窗口
ctx.drawImage(pm, dx - res.srcX * s, dy - res.srcY * s, sheetW * s, sheetH * s);
// 门面侧:真实 SpriteSpec 选图 + SpriteView 同款矩形数学
val spec = SpriteSpec(screenWidth, default, small?, normal?, large?, tablet?)
val chosen = spec.spriteSheet            // parseCategory + 回退链,上游原版
val row = frame / state.framesPerRow     // SpriteView.kt 同款
val column = frame % state.framesPerRow

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:整库真编译,而不是提取"纯 Kotlin 内核"

qrose 当年抽编码器内核,是因为它上游的 Compose 版本(1.12/2.4)与彼时社区线(1.9.2 时代)差距过大。本适配先验证制品线:社区线已到 1.12.0-0.1.0-07,连 components-resources 都有 ohos klib——障碍消失后再抽内核等于把 SpriteState / SpriteSpec 手抄一遍,反而引入漂移风险。真编译让协程帧循环、四档选图、翻转数学全部是上游原件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升 9.6.1

coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20-0.1.0-07 构建,旧钉法 2.2.21-1.0.0 读不了 2.4 元数据。上游源码是 Kotlin 2.0.21 语法,2.4.20 向下兼容,源码零改动。Gradle 按工具链线区分:2.4.20 线支持 9.x(compose-shimmer 同款 9.6.1 先例),上游 wrapper 8.7 升级而非降级。

决策 3:Compose 双线声明(换线不换版)

ohos 线没有 *-desktop 子制品,jvm 验证目标解析不到。处置是把 Compose/coroutines 依赖从 commonMain 移到 ohosMain / desktopMain 按线声明:ohos 走社区 1.12.0-0.1.0-07,desktop 走上游自己的 1.7.0——jvm 侧编译用的就是上游发布链,构成"换线不换版"的对称证据。上游声明但零 import 的 lifecycle-runtime-compose / material3 随之移除,coroutines 显式化。

决策 4:无头门面放在库模块 ohosMain,而不是桥模块

SpriteState.framesPerRow 是库内 internal 成员,桥模块访问不到;门面进库模块后既能取 internal 成员,又让"提升引擎为公开无头面"成为库自身的能力(将来宿主就位,同一门面可继续服务测试与工具链)。桥模块只做协议编解码,与 compose-shimmer 的分工一致。

决策 5:视觉 ArkUI 复刻,帧值必须来自 Native

Demo 的精灵动画不用 ArkUI 自己的动画能力驱动:每一帧的行列与子矩形都由 op=frame 从真实 SpriteState 采样(16ms 轮询)。停止引擎画面立即定格第 0 帧、销毁会话后采样报错——屏幕上的像素变化完全由 Kotlin 侧引擎状态决定,这是"引擎真在跑"的直接证据。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21、Gradle 9.6.1、社区 HarmonyOS Kotlin 工具链与 konan ohos sysroot、DevEco Studio 26.0(hvigor / CMake / ArkTS 工具链)、hdc 与鸿蒙真机(arm64)。

6.2 交叉验证

./gradlew :sprite:compileKotlinDesktop

上游 commonMain(含 SpriteView composable、SpriteSpec、rememberSpriteState)与 desktopMain actual 在上游自己的 Compose 1.7.0 / coroutines 1.8.0 制品线上编译通过——同一份源码在"上游线"与"ohos 社区线"双线编译成立,这是"换线不换版"的直接证据。

6.3 HAP 验证

:sprite:compileKotlinDesktop / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(3m 9s)
llvm-nm -D: SpriteDispatch / SpriteVersion / libohossprite_symbols 均为全局 T 符号(双 ABI)
libohossprite.so: arm64 ≈ 28.2 MB、x86_64 ≈ 27.0 MB(debug,含完整 Compose 运行时)
BuildNativeWithCmake / BuildNativeWithNinja: finished(双 ABI 编出 libsprite_napi.so)
CompileArkTS: finished
PackageHap: finished → entry-default-unsigned.hap ≈ 44.0 MB
BUILD SUCCESSFUL / install bundle successfully / start ability successfully

HAP 内含双 ABI 各三件套(libsprite_napi.so / libohossprite.so / libc++_shared.so)。

6.4 功能用例

  1. 启动后标题区出现 spriteVersion() → 1.1.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 动画 Tab:精灵以 50ms/帧 循环播放,画布左上角 frame n/9 计数滚动;
  3. 点停止:引擎会话停止,frame 回到 0/9(上游 stop() 复位语义);
  4. 点播放:从第 0 帧重新循环;
  5. 拖动 animationSpeed 滑杆后释放:引擎重建,帧率肉眼可辨地变化;
  6. 翻转芯片:水平 / 垂直 / 双向即时生效(画布翻转变换);
  7. 选图 Tab:screenWidth 从 0 拉到 1200,ScreenCategory 依次 Small → Normal → Large → Tablet(360px 落在 Small 档,真机日志已验证);
  8. 关掉某档开关再选图:该档位回退 default(sprite_normal),结果面板如实显示;
  9. 切换档位后回到动画 Tab:画布上的精灵图切换为对应分辨率变体(small 明显更小);
  10. 操作日志按时间戳滚动留痕,重置引擎 / 重新选图均有 ← create / ← select 回显。

6.5 验证结论

编译与交付链路已完成本机验证:整库 commonMain(Compose 组件 + 协程引擎 + 响应式选图)在 jvm("desktop")(上游 1.7.0 线)、ohosArm64、ohosX64 三目标编译通过;双 ABI 动态库链接通过且符号检查全绿;CMake N-API 与 ArkTS 页面编译通过;HAP 成功产出并安装。真机首验(见第七节截图)确认:版本握手、360px → Small/small 的真实 SpriteSpec 选图、rawfile 解码、引擎会话 create/start/stop 与停止复位语义、操作日志全部正确;首验同时暴露 ArkUI 9 参 drawImage 不裁剪 PixelMap 的问题,已用 clip + 缩放平移方案修复并重建 HAP(见 8.1 第 11 条)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/SpriteView-KMP.git
cd SpriteView-KMP
export JAVA_HOME="/path/to/jdk-21"
./gradlew :sprite:compileKotlinDesktop                                # jvm 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap --mode module -p module=entry@default \
  -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.sprite -m entry
hdc shell hilog | grep Sprite

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :sprite 真正编译成 ohosArm64 / ohosX64 并链出动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 社区 Compose 线没有 *-desktop 子制品,jvm 验证目标必须双线声明,commonMain 保持线无关;
  4. skiko 三重冲突:components-resources 声明的 skiko-ohos{arm64,x64}-api 与 -fusionrenderer 会和 compose-ui 拉入的完整版 skiko klib 在 KLIB 加载期撞 unique_name、在链接期撞 Skia 符号——两者都要在依赖声明上排除,只留完整版;
  5. 社区 1.12 线的 DrawableResource 构造函数是 @InternalResourceApi(id, items),直接 DrawableResource(id) 编不过;Resource.id 还是 internal,门面要从自己构造时已知的资源名推导,不能读 image.id;
  6. Kotlin/Native 没有 synchronized,会话表用 AtomicInt 的 compareAndSet 自旋锁保护;
  7. 自定义命名目标(jvm("desktop"))的 source set 没有静态 accessor,要用 val desktopMain by existing 取用;
  8. gradlew 启动自身就需要 JAVA_HOME(org.gradle.java.home 只作用于 daemon),Windows 下先 $env:JAVA_HOME = ...;
  9. 上游 .gitignore 忽略 gradle.properties(为藏签名私钥),fork 的该文件是工具链配置,必须把这行移除才能入库;
  10. hvigor 打包器要 spawn java(PackingTool 是 jar),命令行构建前把 JDK 21 加进 PATH/JAVA_HOME,否则 PackageHap 报 00308018 spawn java ENOENT;assembleHap 要用 --mode module -p module=entry@default,裸的 project 模式报任务不存在;
  11. ArkUI 9 参 drawImage 对 PixelMap 不裁剪:真机首验截图(画布出现整张 3×3 雪碧图而非单帧)暴露此问题,修复改为 rect + clip 裁出帧窗口、整图按 s = dw / sw 缩放平移绘制——只依赖可靠的 5 参 drawImage;
  12. SpriteState.stop() 的帧复位是协程异步生效:stop 响应里可能还读到旧帧号,下一拍轮询即归 0——状态行与画布显示以轮询为准;
  13. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  14. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  15. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor 拒绝——仓库内留空数组,签名材料由 DevEco 自动生成回填,绝不进入 AtomGit。

8.2 已知问题

  • OHOS 无 Compose composition 宿主:SpriteView() / rememberSpriteState() / getScreenWidth() 已编译进 so 但无法执行,getScreenSize.ohos.kt 是为社区宿主预留的接缝;
  • SpriteSpec.imageBitmap(imageResource 解码链)无宿主时不可运行,Demo 精灵图由 ArkTS rawfile 提供;SpriteSpec 的选图逻辑本身在 Native 真实执行;
  • currentFrame StateFlow 的订阅式推送不跨 C ABI,ArkTS 以 16ms 轮询采样(对 20-200ms 的帧间隔足够);
  • android / iOS / wasmJs 目标剥离(源文件留在树里),vanniktech 发布、Maven Central 发布一并剥离;
  • ArkTS 页面的 getContext 在新 SDK 上有 deprecation 警告(功能正常,后续可换 getUIContext().getHostContext());
  • HAP 安装必须使用与 bundle name org.terminator.ohos.sprite 匹配的签名。

8.3 未来优化方向

  • 社区 Compose 宿主就位后,直接在宿主中运行 SpriteView() composable,桥退役;
  • 桥增加 currentFrame 推送(N-API 线程安全回表),替代轮询;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 sprite commonMain(Compose 组件 + 协程引擎 + 选图)
    → 验证社区 Compose ohos 制品线(runtime/ui/foundation/components-resources 全齐)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,Compose 双线声明 + skiko 冲突排除)
    → ohosArm64 / ohosX64 KLIB + 2 个 ohosMain 新文件(actual + 无头门面)
    → C ABI(@CName × 2)+ 扁平 JSON
    → C++ N-API(DisposeString 纪律)+ CMake($ORIGIN 三件套)
    → ArkTS Canvas 逐帧栅格化(clip + 缩放平移)
    → ArkUI 真机三 Tab 验收(动画 / 选图 / 关于)

9.2 封装层次

  • 引擎层:上游原版 SpriteState(协程帧循环、start/stop 复位语义、StateFlow 出口);
  • 选图层:上游原版 SpriteSpec / SpriteSheet / parseCategory(四档分档 + default 回退);
  • 渲染数学层:SpriteView 的行列与子矩形计算(被门面镜像复用);
  • Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON;
  • N-API 层:参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:页面状态、Canvas 栅格化、翻转与缩放、日志与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:components-resources 的 ohos klib 存在,让"整库真编译"取代"提内核"成为正确答案;路线选择必须基于当天的制品可达性,而不是 qrose 时代的历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 按工具链线区分(2.4.20 线用 9.x);
  3. 真机首验是发现"最后一公里"问题的唯一手段:编译、链接、符号检查全绿之后,画布上多出来的 8 个忍者才暴露出 ArkUI 9 参 drawImage 不裁剪——Demo 的价值不在展示成功,在于把平台差异钉出来。

9.4 适配成果

当前适配已完成:

  • SpriteView-KMP 1.1.0 整库(Compose 组件 + 协程引擎 + 响应式选图)的双 ABI Kotlin/Native 编译(上游 commonMain 零改动,仅 2 个 ohosMain 新文件);
  • 社区 Compose 制品线(1.12.0-0.1.0-07,含 components-resources)在纯 Compose UI 库上的完整落地,含 skiko 冲突的排除方案;
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(动画播放 / 停止 / 复位语义、速度、四档翻转、屏宽选图与 default 回退、日志、版本握手);
  • jvm 交叉验证(上游 1.7.0 线编译同一份源码)+ HAP 构建与真机安装(双 ABI 六件套 so,约 44.0MB);
  • 真机首验通过(握手 / 选图 / 会话 / 复位 / 日志),首验暴露的 ArkUI drawImage 裁剪问题已修复;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐