开源鸿蒙平台 CMP 三方库「compose-shimmer-skeleton」适配全流程
本文记录
compose-shimmer-skeleton接入 OpenHarmony 的完整过程,覆盖骨架屏 shimmer 库盘点、Compose UI 与鸿蒙工具链的版本错位、数学内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 页面、HAP 构建和模拟器自动化验收。
compose-shimmer-skeleton是一个 Compose Multiplatform 骨架屏库:全部能力浓缩在一个Modifier.shimmerEffect()扩展里——页面上所有挂载组件共享同一个 2300msEaseInCubic时钟,一道 30° 的线性渐变高光以同一波形掠过整页("全局同步"是它的核心卖点),明暗主题自动换色,骨架底色恒为基色 25% alpha。本次适配与社区近期"真编译整库"路线(KStore:kotlinx ohos 制品线补齐后引擎原样编译)恰好相反:这是一个纯 Compose UI 库,123 行源码全部站在compose.runtime/foundation/ui上,而上游 Compose 1.10.0 / Kotlin 2.3.0 与鸿蒙社区工具链(Kotlin2.2.21-1.0.0)无法共存于一个构建——因此走 cmp_qrose 审核模式的"数学内核提取":把动画时序、渐变色标、全局同步补偿抽成零依赖纯 Kotlin(ShimmerMath/ShimmerDefaults)编译成 OpenHarmony 动态库,上游Modifier源码原样留在composeMain不编译,ArkUI 页面用每帧一次的 JSON 桥拿归一化渐变参数驱动原生linearGradient,30fps 流畅扫光在模拟器上跑通。
项目地址: AtomGit/oh-tpc/compose-shimmer-skeleton.git
上游仓库: timoseyfarth/compose-shimmer-skeleton
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-shimmer-skeleton 为 Compose 应用提供"加载占位"这一件事的完整答案:Modifier.shimmerEffect(visible, shape, baseColor, highlightColor, durationMillis, widthOfShadowBrush, shimmerTravel, angle) 一个扩展函数挂到任意组件上,就得到带扫光动画的骨架占位。它卖的不是"一个会闪的灰块",而是全局同步的扫光——动画值由 rememberInfiniteTransition 全局驱动,每个组件用 positionInRoot().x 对全局偏移做补偿(dx = translateAnimation - globalX),于是列表里所有占位块的高光以同一道波形依次掠过,视觉上像一束光扫过整页。
OpenHarmony 应用同样需要骨架屏,但上游构建(Compose Multiplatform 1.10.0 / Kotlin 2.3.0 / AGP 8.12.3)不会生成 OpenHarmony 变体。如果只把示例页面改成 ArkTS 灰块,页面可以显示,却无法证明上游的动画数学(缓动、色标、同步模型)在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证数学内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。 |
| 工具链不一致 | 上游 Compose 1.10.0 / Kotlin 2.3.0 与鸿蒙工具链无法共存于一个 Gradle 构建;compose.ui 依赖在 ohos 目标上没有制品。 |
| 源码形态特殊 | 全库 123 行、单文件、零 expect/actual、零平台源码集——但 100% 站在 Compose API 上,没有可"原样编译"的纯 Kotlin 部分。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 动画语义跨不了桥 | rememberInfiniteTransition、Brush.linearGradient、drawWithCache 是 Compose 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:shimmer 数学内核的提取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。动画与渐变的每个数值(缓动曲线、色标位置、alpha 档位、默认配色)完全由上游实现反推复刻,上游 Modifier 源码留盘待社区 CMP 与上游版本对齐后接回,ArkTS 只负责计时、调用 Native、解析 JSON 和渲染。
1.2 库提供的能力
Demo 用四个 Tab 覆盖桥接的全部入口:
- 参数渐变(params):社交资料骨架卡(圆形头像 + 姓名条 + 3 根文字条 + 大图块),每帧调一次
shimmerRender(kind=params)拿归一化色标,喂给 ArkUI 原生linearGradient——这是高性能路径,30fps 流畅扫光; - PNG 光栅(frame):同一张骨架卡的几何转成 shapes 数组,由 Kotlin/Native 软件光栅化整帧出 PNG(Base64)显示,节流 ~100ms——与 Tab1 视觉对拍,证明逐像素路径与参数路径一致;
- 参数配置(defaults + params):duration / angle / shadowWidth / travel 四个滑条 + 深色开关 + 自定义
#RRGGBB配色,「恢复默认」按钮走kind=defaults回填——所有上游默认值(2300ms、30°、500dp、300dp、明暗四色)来自真实 Native 调用而非静态文字; - 全局同步(params × 4 卡):2 张全宽卡 + 1 行 2 张半宽卡共用同一全局时钟,各自传不同页面 x,逐卡显示
dx = offset − x——上游"全局同步"卖点的桥上复刻。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
phase / offset / dx | 周期分数(0…1)、缓动后的扫掠偏移 px、按组件 x 补偿后的偏移 px。 |
angleCss | ArkUI linearGradient 直接可用的角度(= 90 + angle,CSS 语义)。 |
stops | 已按组件几何归一化到 [0,1] 且升序的色标数组(至少 2 个),ArkUI 直接喂 colors。 |
pngBase64 / bytes | frame 模式整帧 PNG 与字节数。 |
数学侧完整契约(5 档色标、0.25 底色 alpha、EaseInCubic、Restart 回绕、明暗默认色)全部钉在 ShimmerMath / ShimmerDefaults 里随编译进入 .so,JVM 侧保留 15 项 jvmTest 交叉验证用例(上游原本没有测试,这 15 项是按上游源码逐值新写的),证明 ohos .so 里执行的动画数学没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 Modifier.shimmerEffect 源码原样留在 composeMain(不注册源集即不编译,零改动);数学内核按上游实现逐值复刻进 commonMain(不手抄 Compose 调用,只提纯数学)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);Android / iOS / wasmJs / js 目标与 AGP、Compose、发布插件整体剥离。 |
| 桥接稳定 | 单个 C ABI(ShimmerRender)+ UTF-8 JSON 三种 kind,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 数学内核零第三方依赖(不需要 kotlinx 制品线),工具链用社区已审旧线 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:参数渐变、PNG 对拍、配置回填、全局同步。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。shimmer 的"渲染"在鸿蒙侧由 ArkUI 原生
linearGradient承担,Kotlin/Native 侧只提供"这一帧的渐变应该长什么样"的数学答案。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源码依赖形态(100% Compose)、确定"数学内核提取"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 钉版、composeMain 隔离
第 3 阶段:桥协议建模 ── params/frame/defaults 三 kind JSON 契约、上游数学契约表、全局同步模型、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 33ms 全局时钟、逐帧 params、四 Tab 页面、PNG 节流对拍
第 6 阶段:示例与验证 ── HAP、签名、模拟器安装、uitest 自动验收、hilog 排障和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
library/ 上游库目录(模块名 :compose-shimmer-skeleton,进 ohos 构建)
library/src/composeMain/.../Shimmer.kt 上游 Modifier 源码原样保留(git mv,不注册源集即不编译)
library/src/commonMain/.../ShimmerMath.kt 新增:动画/渐变数学内核(缓动、色标、同步补偿)
library/src/commonMain/.../ShimmerDefaults.kt 新增:上游默认值与配色(明暗四色、2300ms、500dp、300dp、30°)
library/src/commonTest/.../ShimmerMathTest.kt 新增:15 项数学契约测试(按上游源码逐值钉住)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + Rasterizer + PngEncoder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,CN 版 23 节)
README.upstream.md 上游原文保存(原文件是 UTF-16LE,归档时转 UTF-8)
kotlin-js-store/ 上游 js/wasm 构建锁文件,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :compose-shimmer-skeleton 与 :example:nativeApp,rootProject.name = "ohos_compose_shimmer_skeleton"。上游的 AGP、Compose 编译器、vanniktech 发布插件、maven-publish 块和根目录的 updateReadme 任务(它以 UTF-16LE 读写 README,是编码事故源)全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 钉死 Eclipse Adoptium jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.14 升一档到社区已审模板同款;2.2.21 旧线必须 8.x) |
| 依赖 | commonMain 零第三方依赖(数学内核不需要 kotlinx 制品线) |
| DevEco product | 5.1.0(18) 兼容,模拟器 HarmonyOS 7.0.0(API 26) 实测 |
| DevEco Studio | 26.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.shimmer |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 文件 | 行数 | 依赖形态 | 处置 |
|---|---|---|---|
Shimmer.kt(全库唯一源文件) | 123 | 100% Compose API(composed、rememberInfiniteTransition、Brush、drawWithCache、onGloballyPositioned),零 expect/actual、零平台代码 | git mv 到 composeMain 原样保留,不编译 |
| 动画数学(散落在上述文件里) | — | 纯算术:tween 缓动、三角函数、色标表、alpha 缩放 | 提纯为 ShimmerMath.kt 进 commonMain |
| 默认值与配色 | — | 常量:明暗四色、2300ms、500dp、300dp、30° | 提纯为 ShimmerDefaults.kt 进 commonMain |
composeMain 隔离机制值得说明:它不是 KMP 默认源集,sourceSets 里不注册它,Gradle 就直接无视这个目录——上游源码一个字节不动地留在树里(git 历史保持 rename 追踪),等社区 CMP 与上游版本对齐后把依赖接回来即可编译。这比"改源码删 Compose 引用"干净得多。
1.4 页面边界
全局机制 33ms setInterval 全局时钟(Date.now() 差值),一个 elapsed 驱动所有卡片
Tab 1 参数渐变 社交骨架卡:每块 = transparentBaseColor 底层 + linearGradient(angleCss, stops) 顶层
Tab 2 PNG光栅 同几何转 shapes 调 frame kind,Image('data:image/png;base64,...') 显示,节流 100ms
Tab 3 参数配置 duration/angle/shadowWidth/travel 四滑条 + 深色 Switch + 双色 TextInput + 恢复默认
Tab 4 全局同步 2 全宽卡 + 2 半宽卡(不同页面 x),逐卡小字显示 dx 值
状态行 elapsed(ms) / offset(px) / ~fps(500ms 窗口估算),出错时显示 native error 原文
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区两条已审工具链线:旧线 Kotlin 2.2.21-1.0.0(qrose / haze 用,配 Gradle 8.14.1)、新线 2.4.20-0.1.0-07(KStore 用,配 Gradle 9.x)。选线原则是工具链跟着依赖走:本库数学内核零第三方依赖,不需要 kotlinx ohos 制品线,没有"读不了 2.4 元数据"的压力;而 ohosArm64() / ohosX64() 目标在旧线插件里久经验证。于是钉旧线:Kotlin 2.2.21-1.0.0,Gradle wrapper 从 8.14 升到 8.14.1(社区已审模板同款)。
settings.gradle.kts 把 eazytec 仓库写在最前(pluginManagement 与 dependencyResolutionManagement 双份,漏一处配置期直接 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()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_compose_shimmer_skeleton"
include(":compose-shimmer-skeleton")
project(":compose-shimmer-skeleton").projectDir = file("library")
include(":example:nativeApp")
gradle.properties 钉 JDK 与 KN 行为:
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=1.0.1-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
版本策略与社区一致:上游 1.0.1 + -ohos.1 后缀。
2.2 编译数学内核
library/build.gradle.kts 从 90 行(AGP + Compose + 发布 + 六个平台目标)收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖——这是本路线的特权
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
libs.versions.toml 只剩一个插件:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
实测:
./gradlew :compose-shimmer-skeleton:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :compose-shimmer-skeleton:jvmTest # 15 项全绿
2.3 测试:上游没有测试,就自己写测试当规格
上游仓库没有任何测试。数学内核的"没漂移"不能靠肉眼对截图,于是按上游 Shimmer.kt 源码逐值新写 15 项测试(见 6.2 节)——offset 起点 -500、2300ms 处回绕、色标 alpha 26/179、暗色四色……这些断言就是上游行为的规格书。测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
dev.seyfarth.composeshimmer/**(数学内核 ShimmerMath + ShimmerDefaults)
↓ offsetAt / gradientStops / arkStops / colors
Bridge.kt(@CName × 1,三 kind 扁平 JSON 进出)
↓ UTF-8 JSON
N-API shimmerRender(request)
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"kind":"params","elapsed":12345,"x":0,"width":896,"height":384,"dark":false,
"duration":2300,"shadowWidth":1330,"travel":798,"angle":30,
"baseColor":"","highlightColor":""}
{"kind":"frame", ...同上, "bgColor":"",
"shapes":[{"type":"rect","x":16,"y":16,"w":64,"h":64,"r":32},
{"type":"circle","x":16,"y":16,"d":64}]}
{"kind":"defaults"}
响应 : {"ok":true,"kind":"params","phase":0.42,"elapsedOut":12345,
"offset":123.4,"dx":123.4,"angleCss":120,
"baseColor":"#FF2C2C2C","highlightColor":"#FF3A3A3A",
"transparentBaseColor":"#402C2C2C",
"stops":[{"p":0.12,"c":"#00000000"},...]}
{"ok":true,"kind":"frame","width":896,"height":384,"bytes":12345,"pngBase64":"..."}
{"ok":true,"kind":"defaults","light":{...},"dark":{...},"duration":2300,
"shadowWidthDp":500,"travelDp":300,"angle":30,"url":"https://atomgit.com/oh-tpc/compose-shimmer-skeleton"}
错误 : {"ok":false,"error":"..."}
三个关键设计:其一,elapsed 是全局时钟(ArkTS 侧 Date.now() 差值),多个组件传同一个 elapsed、各自的页面 x——这就是上游 rememberInfiniteTransition 全局时间基 + positionInRoot().x 补偿在桥上的复刻;其二,params 响应的 stops 已按组件几何归一化,把"绝对渐变线段(起点 (dx,0)、长度 shadowWidth、角度 θ)"投影到 ArkUI 中心轴渐变语义上,ArkTS 不做任何二次数学;其三,angleCss = 90 + angle——Compose 角度从 +x 轴起算,ArkUI linearGradient 是 CSS 语义(0° 朝上顺时针),桥直接换算好。
3.2 上游数学契约(提取的"内核"到底是什么)
| 契约项 | 上游值 | 钉在哪 |
|---|---|---|
| 动画区间 | -shadowWidthPx → travelPx + shadowWidthPx | ShimmerMath.offsetAt |
| 缓动 | tween(durationMillis, EaseInCubic),RepeatMode.Restart 循环回绕 | offsetAt(frac³ + 取模) |
| 默认时长 | 2300ms | ShimmerDefaults.DURATION_MILLIS |
| 渐变色标 | [0:全透明, 0.3:高亮@0.1, 0.5:高亮@0.7, 0.7:高亮@0.1, 1:全透明] | gradientStops |
| 底色 | 基色 25% alpha 常驻 | transparentBase |
| 亮色默认 | base 0xFF2C2C2C / highlight 0xFF3A3A3A | ShimmerDefaults |
| 暗色默认 | base 0xFFE0E0E0 / highlight 0xFFF5F5F5 | ShimmerDefaults |
| 同步补偿 | dx = translateAnimation - positionInRoot().x | dxFor |
| 默认几何 | shadowBrush 500dp / travel 300dp / angle 30° | ShimmerDefaults |
内核提取的关键纪律:不手抄 Compose 调用,只提纯数学。ShimmerMath 里没有一行引用 Compose 类型,全部是 Float/Int 运算;上游 Color.Transparent、.copy(alpha=)、Brush.linearGradient 的语义被翻译成 ARGB 位运算与向量投影。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ShimmerRender")
fun shimmerRender(request: String): String {
return try {
val json = Json.parse(request) as? Json.Obj
?: throw IllegalArgumentException("request must be a JSON object")
when (json.str("kind").ifBlank { "params" }) {
"defaults" -> defaults()
"frame" -> frame(json)
else -> params(json)
}
} catch (t: Throwable) {
JsonWriter.obj(listOf("ok" to "false",
"error" to JsonWriter.str(t.message ?: t.toString())))
}
}
非法 JSON、width/height 超界(1…4096)、颜色串格式错,全部走同一个 ok:false 通道,ArkTS 侧状态行统一渲染。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string(每帧每卡一次)
▼
C++ N-API entry(libshimmer_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosshimmer.so)
│ ShimmerRender
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;每帧传整张位图又浪费(PNG 模式仅作对拍),所以选择"单 C ABI + 小 JSON"——params 一帧来回只有 ~300 字节。
4.2 Kotlin/Native 导出函数
@CName("ShimmerRender")
fun shimmerRender(request: String): String // params / frame / defaults 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ShimmerRender | shimmerRender(req) | offsetAt + gradientStops + arkStops(params)/ Rasterizer.frame + PngEncoder(frame)/ ShimmerDefaults(defaults) |
example/nativeApp/build.gradle.kts 声明双 ABI sharedLib 并把产物连同匹配 triple 的 libc++_shared.so 自动拷进 HAP 工程:
ohosArm64 { binaries.sharedLib { baseName = "ohosshimmer" } }
ohosX64 { binaries.sharedLib { baseName = "ohosshimmer" } }
tasks.register<Copy>("copyKnSoArm64") {
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) { include("libohosshimmer.so") }
from(cxxShared("aarch64-linux-ohos"))
into(rootProject.layout.projectDirectory.dir("example/harmonyApp/entry/libs/arm64-v8a"))
}
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"shimmerRender", nullptr, NapiShimmerRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosshimmer_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosshimmer_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
这里踩过本次适配最深的一个坑(详见 8.1 第 11 条):index.d.ts 一度写成 export default + ArkTS 默认导入,运行期每帧抛 Object is not callable——native so 模块的默认导入解析成模块命名空间对象而不是函数。修复就是回到 qrose 已验证形态:只声明具名导出,ArkTS 用具名导入。
export const shimmerRender: (request: string) => string;
import { shimmerRender } from 'libshimmer_napi.so'; // 具名导入,别用默认导入
4.4 CMake 和类型声明
add_library(ohosshimmer SHARED IMPORTED)
set_target_properties(ohosshimmer PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(shimmer_napi SHARED napi_init.cpp)
target_link_libraries(shimmer_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosshimmer)
OHOS_ARCH 为空或 libohosshimmer.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
第 5 阶段:ArkUI 宿主
5.1 全局时钟与逐帧参数
aboutToAppear(): void {
this.initGeometry(); // UIContext.vp2px 取密度,所有请求像素由 vp 换算
this.startAt = Date.now();
this.timerId = setInterval(() => { this.tick(); }, 33); // 33ms ≈ 30fps 全局时钟
}
private tick(): void {
this.elapsed = Date.now() - this.startAt; // 唯一时间基,所有卡片共用 => 全局同步
if (this.tab === 3) { // 全局同步 Tab:4 张卡各自调一次
this.syncParams = [/* 全宽×2 + 半宽×2,各传自己的页面 x */];
return;
}
this.params = this.renderParams(this.pagePad, this.cardW, this.cardH);
if (this.tab === 1 && Date.now() - this.lastPngAt >= 100) {
this.renderFrame(); // PNG 模式节流 100ms
}
}
private renderParams(xVp: number, wVp: number, hVp: number): ShimmerParams {
const req: RenderRequest = {
kind: 'params', elapsed: this.elapsed,
x: this.px(xVp), width: this.px(wVp), height: this.px(hVp),
dark: this.dark, duration: this.duration,
shadowWidth: this.px(this.shadowWidthDp), travel: this.px(this.travelDp),
angle: this.angle, baseColor: ..., highlightColor: ...
};
return JSON.parse(shimmerRender(JSON.stringify(req))) as ShimmerParams;
}
5.2 页面渲染与交互
每个骨架块是双层 Stack:底层矩形填 transparentBaseColor(native 返回的 25% alpha 底色),顶层同尺寸矩形挂 linearGradient({ angle: p.angleCss, colors: this.gradientColors(p) })——gradientColors 把响应 stops 映射成 Array<[ResourceColor, number]>。扫光"动"的本质是每帧 stop 位置在滑动,全部几何换算都在 Kotlin 侧完成,ArkTS 只做映射。
四个 Tab 自上而下:Tab1 骨架卡 + 仓库链接 + 状态行;Tab2 说明文案(“仅作视觉对拍、非高性能路径”)+ Image('data:image/png;base64,' + pngBase64);Tab3 四滑条 + 深色 Switch + 双色 TextInput(#RRGGBB 前置校验)+ 恢复默认(kind=defaults 回填滑条)+ 实时预览卡;Tab4 全宽双卡 + 半宽双卡(右边那张 x = pagePad + halfW + 12,与左边不同,演示 dx 补偿后波形仍同步)+ 每卡 dx 小字 + 社区链接。
5.3 对比度和错误边界
滑条与文字用高对比配色;ok:false 时错误原文进状态行(native error: …);Native 调用本身抛错(如导入形态错误)同样被 try/catch 收进状态行并打 hilog,便于排障。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosshimmer)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 1 + 三 kind 分发 + 错误通道
│ ├── MiniJson.kt 手写递归 JSON 解析/写出(不上 kotlinx.serialization,控制 so 体积)
│ ├── Rasterizer.kt 软件光栅化:形状掩码 + 25% 底色 + 渐变投影 + source-over
│ └── PngEncoder.kt 零依赖 PNG(zlib stored 块 + 自实现 adler32/crc32)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.shimmer、versionName 1.0.1-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/libohosshimmer_api.h
├── src/main/cpp/types/libshimmer_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :compose-shimmer-skeleton:jvmTest # 15 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
Windows 命令行跑 hvigor 有两个环境点:DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录;打包器需要 java,用 DevEco 自带 JBR(系统 PATH 没有 java 时报 spawn java ENOENT)。签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库。
hdc list targets
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-unsigned.hap # 模拟器可装未签名包
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.shimmer -m entry
四、完整代码对照
4.1 整体架构
dev.seyfarth.composeshimmer/**(数学内核,零依赖)
│ offsetAt / gradientStops / arkStops / transparentBase / colors
example.nativeapp/Bridge.kt + MiniJson + Rasterizer + PngEncoder
│ ShimmerRender(params / frame / defaults)
libshimmer_napi.so
│ shimmerRender
Index.ets
│ 33ms 全局时钟 + JSON parse + @State 更新
▼
ArkUI 四 Tab 页面(原生 linearGradient 扫光)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose_shimmer_skeleton |
gradle/libs.versions.toml | 只剩 KMP 插件,Kotlin 2.2.21-1.0.0 钉版 |
build.gradle.kts(根) | 版本 1.0.1-ohos.1、全局禁用 Ohos*Test* 任务、删 updateReadme |
library/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖 |
library/src/composeMain/.../Shimmer.kt | 上游 Modifier 源码原样保留(git mv,不编译) |
library/src/commonMain/.../ShimmerMath.kt | 动画/渐变数学内核(缓动、色标、dx 补偿、ArkUI 投影) |
library/src/commonMain/.../ShimmerDefaults.kt | 上游默认值与明暗配色 |
library/src/commonTest/.../ShimmerMathTest.kt | 15 项数学契约测试(新写,钉上游行为) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 单 C ABI、三 kind 分发、错误通道 |
nativeApp/.../Rasterizer.kt | 形状掩码 + 渐变投影 + source-over 整帧光栅化 |
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 | 鸿蒙适配主文档(23 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | ShimmerMath.offsetAt(elapsed, duration, W, T) | EaseInCubic + Restart 的扫掠偏移(上游 animateFloat 的数学) |
| 内核 | ShimmerMath.gradientStops(highlight) | 5 档色标(上游 colorStops 数组) |
| 内核 | ShimmerMath.arkStops(dx, w, h, W, angle, c) | 投影到 ArkUI 中心轴语义并归一化(≥2 个升序 stop) |
| 内核 | ShimmerMath.dxFor(x, offset) | 全局同步补偿(上游 positionInRoot().x) |
| 内核 | ShimmerMath.transparentBase(base) | 25% alpha 底色(上游 background(transparentBaseColor)) |
| Native | ShimmerRender | C ABI JSON 导出(params / frame / defaults) |
| N-API | shimmerRender(req) | 向 ArkTS 暴露能力(具名导出) |
| ArkTS | renderParams(x, w, h) | 每帧请求并刷新 linearGradient |
4.4 ArkTS 与 Kotlin 的边界
const raw: string = shimmerRender(JSON.stringify({
kind: 'params', elapsed: this.elapsed,
x: this.px(16), width: this.px(320), height: this.px(180),
dark: false, duration: 2300,
shadowWidth: this.px(500), travel: this.px(300), angle: 30,
baseColor: '', highlightColor: ''
}));
const p: ShimmerParams = JSON.parse(raw) as ShimmerParams;
// p.angleCss === 120;p.stops 已归一化,直接喂 linearGradient
val offset = ShimmerMath.offsetAt(spec.elapsed, spec.duration, spec.shadowWidth, spec.travel)
val dx = ShimmerMath.dxFor(spec.x, offset)
val stops = ShimmerMath.arkStops(dx, width, height, spec.shadowWidth, spec.angle, highlight)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:数学内核提取,而不是真编译整库
与 KStore 恰好相反:KStore 是引擎库(commonMain 纯 Kotlin),kotlinx ohos 制品线补齐后整库原样编译;shimmer 是 UI 库,123 行全部站在 Compose API 上,ohos 上没有 compose.ui 制品,"整库"无从编译。能提取的是数学(缓动、色标、同步模型),不能提取的是渲染(drawWithCache/Brush)——渲染交给 ArkUI 原生 linearGradient,数学交给 Kotlin/Native。路线选择由源码依赖形态决定,不由偏好决定。
决策 2:工具链走旧线 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
“工具链跟着依赖走”(KStore 经验)在本库给出相反答案:零第三方依赖就没有"读不了新元数据"的压力,ohosArm64() 在旧线上是 qrose/haze 实证过的组合。Gradle 从上游 8.14 升到 8.14.1 对齐社区已审模板。两条工具链线的经验不能互相套用——选线前先盘点依赖形态。
决策 3:单一 C ABI 三 kind,params 与 frame 分离
一个 ShimmerRender 入口三种 kind:params 每帧 ~300 字节往返,是高性能路径(ArkUI 原生渐变渲染,30fps);frame 整帧光栅化 PNG,只作逐像素对拍(节流 100ms,说明文案直接告诉用户它不是高性能路径);defaults 让 Demo 的"恢复默认"走真实 Native 调用。单入口让 N-API 层只维护一个符号,三 kind 的演进互不影响。
决策 4:渐变几何换算全部下沉到 Kotlin 侧
ArkUI linearGradient 与 Compose Brush.linearGradient 语义有三处错位:角度基准(CSS 0° 朝上 vs Compose +x 轴)、轴向锚定(组件中心 vs 任意起点)、stop 域([0,1] 归一化 vs 线段参数)。全部在 arkStops 里一次性换算(含渐变整段离屏时 5 个 stop 坍缩的兜底),ArkTS 侧零数学——错位知识单点维护,出问题只查一处。
决策 5:上游没有测试,就新写 15 项测试当规格
“数学没漂移"需要证据。按上游源码逐值新写 15 项 JVM 测试(offset 起点/回绕/单调、色标位置与 alpha 26/179、明暗四色、dx 补偿、arkStops 归一化不变量),它们既是交叉验证闸门,也是"上游行为规格书”——将来接回 Compose 版时,同一组断言对 composeMain 实现照样成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium,org.gradle.java.home 钉死)、DevEco Studio 26.0(E:\DE\DevEco Studio,含自带 JBR 与 SDK)、OpenHarmony 命令行工具(hvigor / hdc)与 DevEco 模拟器(Mate 90 Pro,HarmonyOS 7.0.0(API 26),x86_64)。
6.2 JVM 交叉验证
./gradlew :compose-shimmer-skeleton:jvmTest
15 项用例全部通过(新写,钉上游行为):
| 测试 | 钉住什么 |
|---|---|
offsetStartsAtNegativeShadowWidth | 周期起点 offset == -500(上游 initialValue) |
offsetAdvancesMonotonicallyWithinCycle | EaseInCubic 单调性(步进 23ms 扫全周期) |
offsetApproachesTravelPlusShadowWidthAtCycleEnd | 末端 (2299/2300)³ = 0.9987 → offset ≈ 798.3(不是 800!) |
offsetWrapsLikeRepeatModeRestart | 2300ms 处回绕到起点(RepeatMode.Restart) |
defaultsMatchUpstream / darkModeSwapsDefaultColors / explicitColorsOverrideThemeDefaults | 明暗四色与覆盖优先级 |
gradientStopsMatchUpstreamShape / stopAlphasMatchUpstream | 5 档位置与 alpha 26 / 179 / 26 |
transparentBaseKeepsColorQuarterAlpha | 底色 25% alpha(64/255) |
dxCompensatesGlobalX / endpointsFollowAngle | 同步补偿与渐变端点几何 |
colorAtInterpolatesStops | 色标插值与边界钳制 |
arkStopsStaySortedAndNormalized | 5 个 dx 档位下 stop 始终 [0,1] 严格升序 |
6.3 HAP 验证
:compose-shimmer-skeleton:compileKotlinJvm / jvmTest: BUILD SUCCESSFUL(15 tests green)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
→ arm64-v8a/libohosshimmer.so 3,777,024 B + libc++_shared.so 1,850,184 B
→ x86_64/libohosshimmer.so 3,477,488 B + libc++_shared.so 1,731,432 B
llvm-nm -D: ShimmerRender / libohosshimmer_symbols 均为全局 T 符号
BuildNativeWithNinja: finished(双 ABI 编出 libshimmer_napi.so)
CompileArkTS: finished(3.6s)
PackageHap: finished(命令行需 DevEco JBR 提供 java)
BUILD SUCCESSFUL
install AppMod finish
start ability successfully
6.4 功能用例与自动化验收
模拟器上用 uitest uiInput click 逐个点开四个 Tab,hilog 全程采样:
10-18 00:33:44.418 I A00000/ShimmerNapi: shimmerRender request bytes=173
10-18 00:33:44.453 I A00000/ShimmerNapi: shimmerRender request bytes=173 ← 每 ~35ms 一次
Tab[PNG光栅] 近期错误相关日志: 0
Tab[参数配置] 近期错误相关日志: 0
Tab[全局同步] 近期错误相关日志: 0
- 启动后 Tab1 骨架卡正常渲染,状态行
elapsed / offset / ~30fps持续刷新(33ms 定时器 → 30fps 是设计值); - NAPI 桥每帧调用成功(
ShimmerNapi日志刷屏,ShimmerPage错误日志为零); - Tab2 PNG 模式画面与 Tab1 视觉一致(同一几何、同一数学,两条渲染路径对拍);
- Tab3 拖动 duration/angle 滑条、切换深色,预览卡实时变化;恢复默认走
kind=defaults回填; - Tab4 半宽双卡 dx 不同、扫光波形同步——全局同步卖点成立;
- 修复导入形态前后对照:旧包每帧
Object is not callable,新包零错误(修复过程见 8.1 第 11 条)。
6.5 验证结论
数学内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙模拟器(API 26,x86_64)上成功安装并进入前台,四个 Tab 全部通过自动化点击验收,扫光动画以 ~30fps 连续播放(见下节效果图)。真机(arm64)流程与命令完全同构,签名包安装即可复验。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-shimmer-skeleton.git
cd compose-shimmer-skeleton
export JAVA_HOME="/path/to/jdk-21"
./gradlew :compose-shimmer-skeleton:jvmTest # 15 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
# Windows 命令行需先设 DEVECO_SDK_HOME 并把 DevEco 自带 JBR 的 bin 加入 PATH
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.shimmer -m entry
hdc shell hilog -x | grep -iE "ShimmerNapi|ShimmerPage"
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让数学内核真正编译成
ohosArm64/ohosX64动态库——但纯 Compose UI 库整库编不进去,源码依赖形态决定路线(提取内核)而不是反过来; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;composeMain隔离不要靠改源码删 Compose 引用:不注册源集即不编译,git mv保留上游文件与历史,接回时零冲突;- ArkTS 编译器不认
HorizontalAlign.Top——Row 的alignItems用VerticalAlign,Column 的用HorizontalAlign,两个枚举配错容器直接编译失败; - 全局
vp2px()已废弃且有抛错风险:用this.getUIContext().vp2px(1)+ try-catch,失败回落display.getDefaultDisplaySync().densityPixels(同样要 try-catch); - 对 native so 用默认导入会每帧抛
Object is not callable:index.d.ts写了export default、ArkTSimport shimmerRender from 'libshimmer_napi.so',默认导入解析成模块命名空间对象而不是函数。修法:index.d.ts只留export const shimmerRender,ArkTS 用具名导入import { shimmerRender } from 'libshimmer_napi.so',NAPI 只注册单属性——排障靠 hilog 里的真实异常原文,别盯着 Demo 的兜底文案猜; - EaseInCubic 末端不是 1:
(2299/2300)³ = 0.9987,末帧 offset ≈ 798.3 而非 800,测试断言写等式必挂; - ArkUI 渐变离屏时 5 个 stop 会全部坍缩到边缘(非法 stop 表):桥侧
arkStops兜底保证至少 2 个升序 stop(都透明,视觉正确); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - Windows 命令行打 HAP 报
spawn java ENOENT:打包器要java,把 DevEco 自带 JBR 的 bin 加进 PATH(注意 Git Bash 里 Windows 反斜杠拼 PATH 无效,要用 POSIX 形式路径); - Git Bash 里
hdc shell带设备侧路径(/data/local/tmp/...)会被 MSYS 路径转换改写成 Windows 路径:export MSYS_NO_PATHCONV=1后截图/传文件才正常; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();napi_get_value_string_utf8先量长再std::vector<char>拷贝; - 上游
README.md是 UTF-16LE 编码(根 build 的updateReadme任务硬编码):归档时转 UTF-8,该任务随根构建替换删除,否则自动化改写会写坏文件; - KN 链接期
api.cpp:448 -Wreturn-type警告是 CAdapter 生成代码,可忽略。
8.2 已知问题
- 鸿蒙侧没有 Compose Modifier:
Modifier.shimmerEffect()留在composeMain,HarmonyOS 只能走 NAPI 桥或 ArkTS 自绘;等社区 CMP 与上游版本对齐后接回; arkStops是几何投影近似:把绝对渐变线段投影到 ArkUI 中心轴语义;因渐变两端全透明,投影误差不可见,但不是逐像素等价(PNG 模式才是);- PNG 光栅化仅对拍用:zlib stored 未压缩块,帧大、Base64 有开销,不是高性能路径;
- 深色模式由请求方传
dark,桥不感知系统主题:调用方自行用onConfigurationUpdate切换并重发请求; - android / iOS / wasmJs / js 目标剥离(
kotlin-js-store留盘),Compose 编译器、AGP、发布插件一并剥离,本 fork 不发 Maven Central; - 真机(arm64)签名验收走 DevEco 自动签名流程,证书材料不进仓库。
8.3 未来优化方向
- 等鸿蒙社区 CMP 与上游 Compose 版本对齐后,把
composeMain接回commonMain,直接产出 Compose 目标,桥降级为兼容通道; - 上游 roadmap 的预置骨架组合(列表卡、文章卡、圆形头像行)可以在 ArkTS 侧做成组件库形态(
.har)发布; - params 响应加帧号与单调性校验字段,方便接入方做掉帧检测;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(arm64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 Shimmer.kt(123 行,100% Compose API)
→ 判定源码形态:纯 UI 库,不能真编译整库(与 KStore 相反)
→ 数学内核提取(offsetAt / gradientStops / dxFor / 默认值)进 commonMain
→ 工具链旧线 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(零依赖 => 无升线压力)
→ 根构建替换(cmp_qrose 模式)+ composeMain 隔离(git mv,不编译)
→ ohosArm64 / ohosX64 KLIB + 双 ABI libohosshimmer.so
→ C ABI(@CName × 1,三 kind)+ 归一化 stops / angleCss 下沉换算
→ C++ N-API(具名导出!DisposeString 纪律)
→ ArkTS 33ms 全局时钟 + 原生 linearGradient 渲染
→ ArkUI 四 Tab 模拟器验收(uitest 自动点击 + hilog 零错误)
9.2 封装层次
- 内核层:
ShimmerMath/ShimmerDefaults——缓动、色标、同步补偿、默认配色,零依赖纯 Kotlin; - Native 层:双 ABI 动态库,
ShimmerRender单符号输出 JSON(params / frame / defaults); - 光栅层:
Rasterizer+PngEncoder——形状掩码、渐变投影、source-over、零依赖 PNG,仅对拍用; - N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:33ms 全局时钟、逐卡参数请求、
linearGradient映射、四 Tab 状态管理; - DevEco 层:完成 CMake、HAP、签名、安装和模拟器运行。
9.3 三条经验
- UI 库的适配路线由"能不能编 Compose"决定:引擎库(KStore)制品线补齐后整库真编译,UI 库(shimmer)只能提取数学、渲染交给 ArkUI——同一个社区、两条相反路线,判据都是上游源码的依赖形态,先盘点再定路线;
- 上游没有测试时,测试就是你要写的规格:15 项按上游源码逐值新写的断言(-500 起点、798.3 末端、alpha 26/179、明暗四色)既是"没漂移"的证据,也是将来接回 Compose 版的回归闸门;
- 设备侧排障要拿第一手异常:Demo 的兜底文案(
shimmerRender(params) failed)不携带原因,hdc shell hilog里的Object is not callable才是真凶——native so 模块禁止默认导入这类知识,只有日志闭环才能沉淀成踩坑表。
9.4 适配成果
当前适配已完成:
compose-shimmer-skeleton1.0.1-ohos.1数学内核的双 ABI Kotlin/Native 编译(上游 Modifier 源码零改动留composeMain,内核零第三方依赖);- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号:ShimmerRender/libohosshimmer_symbols); - DevEco Stage ArkUI 四 Tab 验收页面(参数渐变、PNG 对拍、配置回填、全局同步);
- 15 项 JVM 交叉验证全绿(新写测试钉上游行为)+ HAP 构建(双 ABI 六件套 so)、模拟器安装和 uitest 自动化验收(hilog 零错误,~30fps);
- AtomGit 项目文档、OpenHarmony README(中英双份,CN 版 23 节)和本地效果图。
适配过程 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)