开源鸿蒙平台 CMP 三方库「compose-placeholder」适配全流程
本文记录
compose-placeholder接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的依赖边界盘点、鸿蒙工具链钉版、"数学核心提取"路线选型、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-placeholder是 Accompanist placeholder 2023 年 7 月废弃后的接棒 fork:它提供一个 ComposeModifier.placeholder(visible, color, shape, highlight),在内容加载期间给任意组件盖上一层占位色块,再用 fade(呼吸淡入)/ shimmer(微光扫过)两种高亮动画安抚用户等待,切到真实内容时用 spring 交叉淡入过渡;Material 与 Material3 两个伴生模块给出一键式主题默认色。本次适配与存储类库(如 KStore)有本质区别:上游是 100% Compose UI 代码,Modifier、DrawModifierNode、Brush、InfiniteRepeatableSpec在鸿蒙侧没有任何制品可以依赖。因此适配路线不是"真编译整库",也不是"用 ArkTS 重写一个骨架屏组件",而是第三条路——把这个库真正值钱的数字层(动画时序、alpha 曲线、径向渐变几何、crossfade spring 物理、Material 颜色配方)原样提取成纯 Kotlin 的PlaceholderMath.kt编进ohosArm64/ohosX64动态库,作为单一事实来源;上游 Compose 源码一字不改地保留在composeMain留待社区工具链对齐后接回;ArkUI Canvas 演示页每帧向 Native 桥要常量和曲线,ArkTS 侧一个库常量都不手写。
项目地址: AtomGit/oh-tpc/compose-placeholder.git
上游仓库: eygraber/compose-placeholder
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-placeholder 在 Android / iOS / 桌面 / Web 上卖的是一个修饰符:placeholder(visible, color, shape, highlight) 在内容加载期间盖一层占位色,PlaceholderHighlight.Companion.fade / shimmer 提供两种高亮动画,visible 切换时用 spring 交叉淡入把骨架换成真实内容。KMP 项目把这些能力放在 commonMain,由各平台共享。OpenHarmony 应用同样需要"加载占位"这一最基础的 UX 模式,但上游构建(Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / eygraber conventions 插件 0.0.104 / Compose 1.12.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS 骨架屏,页面可以动起来,却无法证明上游的动画语义在 OpenHarmony ARM64 设备上完成编译、链接和执行——那只是一个"长得像"的复制品。适配需要同时验证核心数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / conventions 共享构建插件与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| Compose 依赖锁死 | 库源码 100% 站在 androidx.compose.* 上(Modifier / DrawModifierNode / Brush / Animatable / InfiniteRepeatableSpec);CPF 社区的 HarmonyOS CMP 发行线是 1.9.2,与上游 Compose 1.12 / Kotlin 2.4 对不齐,:placeholder 无法整体编译成 ohos KLIB。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 动画语义跨不了桥 | InfiniteRepeatableSpec 的 delay/Restart/Reverse 语义、spring 数值积分、Brush.radialGradient 都是 Kotlin/Compose 侧概念,ArkTS 侧没有等价物。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:数字层的 ohos 编译(PlaceholderMath.kt)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染交给 ArkTS Canvas,但渲染所用的每一个常量、每一条曲线、每一个颜色都来自 Native 桥对上游公式的实时求值。
1.2 库提供的能力
Demo 提供一个真实的骨架屏会话,对应桥的全部入口:
- 常量握手(defaults):一次调用返回版本号与全部库常量——fade/shimmer 的 delay、duration、重复模式,shimmer 峰值进度 0.6,Material 颜色配方的三个 alpha(0.1 / 0.3 / 0.75),crossfade 的层阈值 0.01 / 0.99 与 spring 默认参数(stiffness 400、dampingRatio 1.0),以及渐变停点 0 / 0.5 / 1;
- 进度求值(progress):给定高亮种类和流逝毫秒数,返回当帧 tween 进度(含 200ms 起始延迟与 Restart 锯齿 / Reverse 往返语义);
- 高亮求值(highlight):fade 返回当帧 alpha;shimmer 返回 alpha、径向渐变半径(
max(w, h) × progress × 2)、中心(块左上角)、三个 ARGB 停点色与停点偏移; - 交叉淡入(spring):给定初值、目标、流逝毫秒(可选 stiffness / dampingRatio / 初速度,缺省即 Compose
spring()默认),返回临界阻尼弹簧的闭式解; - 颜色配方(color):mode = placeholder(contentColor 10% alpha 合成到背景)/ fade(背景 0.3 alpha)/ shimmer(背景 0.75 alpha),ARGB 整数进出;
- 版本握手(placeholderVersion):返回
1.0.13-ohos.1,来自真实 Native 调用而非静态文字。
响应统一字段:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
progress / alpha / value | progress、highlight、spring 三个 op 的数值结果。 |
radius / centerX / centerY / colors / offsets | shimmer 渐变几何与停点(colors 为无符号 ARGB 十进制)。 |
argb | color op 的合成结果。 |
version / 全部常量组 | defaults op 的握手与常量下发。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 Placeholder.kt / PlaceholderHighlight.kt / UI 测试一字不改,整体移入 composeMain / composeTest 留盘;新增的唯一源文件 PlaceholderMath.kt 是上游公式的纯 Kotlin 镜像(同公式、同常量、同语义)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);placeholder-material / placeholder-material3 / sample(纯 Compose / 示例工程)留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 提取核心零第三方依赖(纯 Kotlin 标准库),工具链钉社区已审组合(Kotlin 2.2.21-1.0.0 + Gradle 8.14.1,qrose / wavy-slider 同款)。 |
| UI 完整 | 单页覆盖全部桥接入口:三种高亮模式、加载⇄内容交叉淡入、深浅主题、状态行、操作日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的
Modifier.placeholder在鸿蒙没有对应物;ArkTS Demo 用 Canvas 复现的是上游的数字语义(时序、曲线、几何、物理、颜色),每一项都由桥实时求值,而不是照抄数字。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 Compose 依赖边界、确认"数学核心提取"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 钉版
第 3 阶段:核心与桥协议 ── PlaceholderMath 提取(时序/alpha/渐变/spring/颜色)、五 op JSON 契约、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 骨架列表 Canvas、fade/shimmer/无三模式、spring 交叉淡入、深浅主题、日志
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
placeholder/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
placeholder/src/commonMain/ 新增:PlaceholderMath.kt(时序/alpha 曲线/渐变几何/spring/颜色配方)
placeholder/src/composeMain/ 上游 Placeholder.kt + PlaceholderHighlight.kt(保留源码,ohos 不编)
placeholder/src/composeTest/ 上游 Compose UI 测试(零改动留盘,不进构建)
placeholder-material/ Material 主题色包装(纯 Compose,留盘不进 settings)
placeholder-material3/ Material3 主题色包装(留盘)
sample/ 上游示例工程(留盘)
example/nativeApp/ Kotlin/Native 动态库(Bridge.kt + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 博客文章与真机效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :placeholder 与 :example:nativeApp,rootProject.name = "ohos_compose_placeholder"。上游的 conventions 共享构建插件、AGP classpath、detekt / dokka / vanniktech 发布链全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 C:\Users\Administrator\.jdks\jdk-21,已写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从上游 9.8.1 降级;2.2.21 旧线不支持 Gradle 9.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.placeholder |
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
placeholder(上游实现) | 2 | 100% androidx.compose.*(Modifier / DrawModifierNode / Brush / Animatable / InfiniteRepeatableSpec) | 整体移入 composeMain 留盘,ohos 不编译 |
PlaceholderMath.kt(新增) | 1 | 纯 Kotlin 标准库,零第三方 | 进 commonMain(jvm + 双 ohos 编译) |
placeholder-material / placeholder-material3 | 4 | MaterialTheme @Composable 颜色扩展 | 留盘;数值配方(0.1 / 0.3 / 0.75 alpha 与合成规则)并入 PlaceholderMath |
sample | 8 | Compose 示例 + image-loader | 留盘不进 settings |
expect 声明全库为零——不存在平台 actual 要补(对比 KStore 需要补 dispatcher actual)。本次适配唯一的新增源文件 PlaceholderMath.kt 不是 actual,而是上游公式的提取镜像:
// placeholder/src/commonMain/kotlin/com/eygraber/compose/placeholder/PlaceholderMath.kt
public object PlaceholderMath {
// Placeholder.kt → PlaceholderDefaults
public const val FADE_DELAY_MS: Int = 200 // tween(delayMillis = 200, durationMillis = 600)
public const val FADE_DURATION_MS: Int = 600
public const val FADE_REVERSE: Boolean = true // RepeatMode.Reverse
public const val SHIMMER_DELAY_MS: Int = 200 // tween(durationMillis = 1700, delayMillis = 200)
public const val SHIMMER_DURATION_MS: Int = 1700
public const val SHIMMER_REVERSE: Boolean = false // RepeatMode.Restart
public const val PROGRESS_FOR_MAX_ALPHA: Float = 0.6f
// placeholder-material → PlaceholderDefaults.color / fadeHighlightColor / shimmerHighlightColor
public const val COLOR_CONTENT_ALPHA: Float = 0.1f
public const val FADE_HIGHLIGHT_ALPHA: Float = 0.3f
public const val SHIMMER_HIGHLIGHT_ALPHA: Float = 0.75f
// PlaceholderNode.draw → 层绘制阈值
public const val LAYER_ALPHA_MIN: Float = 0.01f
public const val LAYER_ALPHA_MAX: Float = 0.99f
// Compose spring<Float>() 默认(crossfade 用)
public const val SPRING_STIFFNESS: Float = 400f // Spring.StiffnessMediumLow
public const val SPRING_DAMPING_RATIO: Float = 1f // Spring.DampingRatioNoBouncy
// + tweenProgress / fadeAlpha / shimmerAlpha / shimmerRadius / gradientOffsets /
// gradientColors / springValue / withAlpha / compositeOver / placeholderColor /
// fadeHighlightColor / shimmerHighlightColor(全部纯 Kotlin)
}
1.4 页面边界
标题区 PLACEHOLDER · OPENHARMONY / 版本握手行(Compose Multiplatform 骨架占位库 · ohosArm64/ohosX64)
控制区 高亮模式三选一(无 / 淡入 Fade / 微光 Shimmer)+ 加载状态切换(加载中 ⇄ 已加载)+ 主题(浅 / 深)
Canvas 演示区 5 行骨架列表(圆形头像 + 主标题横条 + 副标题横条,宽度交错);已加载态绘制真实文本;
交叉淡入由 spring op 驱动,高亮动画每帧向桥要 alpha / 半径 / 渐变色
状态行 当前模式 / 主题 / 可见性概览
日志区 带时间戳的操作日志(最新在前,上限 100 条;帧内桥调用按 700ms 采样记录)
关于区 版本握手、库简介、架构分层、仓库与社区链接(AtomGit)
页面启动时先调 placeholderVersion() 做握手,再经 defaults op 读入全部库常量并缓存。
第 2 阶段:目标与依赖打通
2.1 路线验证(本适配的成败手)
先验证"androidx.compose.* 在鸿蒙侧能不能解析"。社区 Maven(https://maven.eazytec-cloud.com)上有 HarmonyOS Kotlin 与 kotlinx 的 ohos 制品线,但 Compose 发行线只到 1.9.2——上游源码站在 Compose 1.12 / Kotlin 2.4 上,把 :placeholder 整体编译进 ohos 意味着重发行一整套 Compose,这不是本仓要做的事。三个结论:其一,Modifier.placeholder 的渲染实现鸿蒙侧带不过来,等社区 compose.ui ohos 制品与上游版本对齐后才能接回;其二,库的数字层(时序、曲线、几何、物理、颜色)是纯数学,与 Compose 类型系统解耦后可以 1:1 镜像进 commonMain;其三,提取后的核心零第三方依赖,不需要 KStore 那样的 kotlinx ohos 制品线四件套,工具链可以钉社区最保守的已审组合。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
提取核心零依赖,版本选择的约束只剩工具链自身的兼容性:社区已审的保守线是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / wavy-slider / Cloudy 用的),该线不支持 Gradle 9.x,上游 wrapper 9.8.1 必须降到 8.14.1。KStore 走的 2.4.20-0.1.0-07 + Gradle 9.x 线是因为它依赖的 kotlinx ohos 制品带 2.4 元数据——本仓核心无依赖,没有理由离开保守线,两条工具链线的经验不能互相套用。
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()
}
}
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_placeholder"
include(":placeholder")
include(":example:nativeApp")
2.3 编译提取核心
placeholder/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
gradle/libs.versions.toml 只剩一个 Kotlin 版本号(2.2.21-1.0.0)。实测:
.\gradlew.bat :placeholder:compileKotlinOhosArm64 :placeholder:compileKotlinOhosX64 # BUILD SUCCESSFUL
上游 Compose 源码(composeMain)与 UI 测试(composeTest)是未注册的源码集目录,Gradle 直接无视——它们以"零改动 + 留盘"的形态存在,等工具链对齐后把目录名改回来即可接回。
2.4 测试的处置:上游 UI 测试零改动 + 新增 20 项纯 Kotlin 测试
上游 commonTest 里的测试全部站在 Compose UI 测试设施上(CaptureToImage、截图断言),编不进无 Compose 依赖的构建。处置是两条纪律的组合:上游测试文件一字不动移入 composeTest 留盘;在 commonTest 新增 PlaceholderMathTest 20 项用例钉住提取的数学(见 6.2 节),测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:核心提取与桥协议建模(技术难点)
3.1 JSON 契约
com.eygraber.compose.placeholder.PlaceholderMath(上游公式提取,commonMain)
↓ 时序 / alpha 曲线 / 半径 / 颜色 / spring 闭式解
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API placeholderDispatch() / placeholderVersion()
↓
ArkUI Index.ets(单页滚动,Canvas 逐帧渲染)
请求与响应(六个 op):
{"op":"defaults"} → 版本 + 全部库常量(fade/shimmer 时序、0.6 峰值、
Material 三 alpha、层阈值、spring 默认、渐变停点)
{"op":"progress","kind":"fade","elapsedMs":1234}
→ {"ok":true,"progress":0.42}
{"op":"highlight","kind":"shimmer","progress":0.5,"highlightArgb":u,"widthPx":1080,"heightPx":240}
→ {"ok":true,"alpha":0.83,"radius":1200.0,
"centerX":0.0,"centerY":0.0,
"colors":[u0,u1,u2],"offsets":[0.0,0.5,1.0]}
{"op":"highlight","kind":"fade","progress":0.5}
→ {"ok":true,"alpha":0.5}
{"op":"spring","initial":0.0,"target":1.0,"elapsedMs":350.0}
→ {"ok":true,"value":0.87}(stiffness/dampingRatio/初速度可省略)
{"op":"color","mode":"placeholder","backgroundArgb":u,"contentArgb":u}
→ {"ok":true,"argb":u}(mode 还有 "fade" / "shimmer")
失败 → {"ok":false,"error":"..."}
3.2 提取的五组数学(上游公式 ↔ 提取实现逐一对照)
时序:上游 PlaceholderDefaults.fadeAnimationSpec = infiniteRepeatable(tween(delayMillis = 200, durationMillis = 600), RepeatMode.Reverse)、shimmerAnimationSpec = infiniteRepeatable(tween(durationMillis = 1700, delayMillis = 200), RepeatMode.Restart)。提取为 tweenProgress(elapsedMs, delayMs, durationMs, reverse):延迟期恒 0;Restart 是锯齿(played % duration / duration);Reverse 是三角波(周期翻倍、后半程镜像)。两个 tween 都无 easing,线性即原语义。
alpha 曲线:Fade.alpha(progress) = progress;Shimmer.alpha 在 progress <= progressForMaxAlpha(默认 0.6)时 lerp(0, 1, progress / pMax)、之后 lerp(1, 0, (progress - pMax) / (1 - pMax))。提取实现补充了退化输入的饱和处理(pMax <= 0 或 >= 1 时钳制而不是像原始公式那样除零出 NaN——正常路径公式逐字一致)。
渐变几何:Shimmer.brush 是 Brush.radialGradient(colors = [highlight(0f), highlight, highlight(0f)], center = Offset(0, 0), radius = (max(w, h) * progress * 2).coerceAtLeast(0.01f))。提取为 shimmerRadius(width, height, progress) + gradientColors(highlightArgb)(透明-高亮-透明三停点)+ gradientOffsets()(三色无偏移时均分 0 / 0.5 / 1)。
crossfade 物理:上游 placeholderFadeAnimationSpec = spring(),Compose Float spring 默认 dampingRatio = 1.0(NoBouncy)、stiffness = 400f(StiffnessMediumLow)。ArkTS 侧不需要复刻 Compose 的数值积分——临界阻尼弹簧有闭式解 x(t) = target + (x0 - target)(1 + ωt)e^(-ωt) + v0·t·e^(-ωt)(ω = √k)。提取的 springValue 同时实现欠阻尼 / 临界 / 过阻尼三档解析解,默认参数下与 Compose 数值解同物理。层绘制语义一并提取:alpha < 0.01 不画、> 0.99 免 saveLayer 直接满 alpha 画。
Material 颜色配方:placeholder-material 的三个默认色是纯函数——color = contentColor.copy(0.1f).compositeOver(backgroundColor)、fadeHighlightColor = backgroundColor.copy(alpha = 0.3f)、shimmerHighlightColor = backgroundColor.copy(alpha = 0.75f)(Material 与 Material3 同源)。提取为 ARGB int 域的 withAlpha / compositeOver / placeholderColor / fadeHighlightColor / shimmerHighlightColor(sRGB src-over:src + dst × (1 - srcAlpha),alpha 通道 8-bit 量化)。
3.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("PlaceholderDispatch")
fun placeholderDispatch(request: String): String = try {
val json = MiniJson.parse(request)
when(json.str("op")) {
"defaults" -> defaults()
"progress" -> progress(json)
"highlight" -> highlight(json)
"spring" -> spring(json)
"color" -> color(json)
else -> err("unknown op \"${json.str("op")}\"")
}
} catch(t: Throwable) {
err(t.message ?: t.toString()) // {"ok":false,"error":"..."}
}
非法 JSON、缺字段、NaN / Infinity、未知 op、progressForMaxAlpha 越界,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进日志区。
第 4 阶段:原生桥接
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libplaceholder_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosplaceholder.so)
│ PlaceholderDispatch / PlaceholderVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制动画语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("PlaceholderVersion")
fun placeholderVersion(): String = "1.0.13-ohos.1"
@CName("PlaceholderDispatch")
fun placeholderDispatch(request: String): String // 六 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖核心 |
|---|---|---|
PlaceholderDispatch | placeholderDispatch(req) | 时序 / alpha / 渐变 / spring / 颜色 全部数学 |
PlaceholderVersion | placeholderVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"placeholderDispatch", nullptr, NapiPlaceholderDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"placeholderVersion", nullptr, NapiPlaceholderVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string(version 无参)、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosplaceholder_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosplaceholder_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosplaceholder SHARED IMPORTED)
set_target_properties(ohosplaceholder PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(placeholder_napi SHARED napi_init.cpp)
target_link_libraries(placeholder_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosplaceholder)
OHOS_ARCH 为空或 libohosplaceholder.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const placeholderDispatch: (request: string) => string;
export const placeholderVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 常量握手和帧循环
aboutToAppear(): void {
this.version = placeholderVersion(); // 版本握手:真实 Native 调用
loadDefaults(); // defaults op 读入全部库常量并缓存
this.refreshPalette(); // color op 计算三色,主题切换时重算
this.startFrameLoop(); // displaySync 优先,失败降级 setInterval(16ms)
}
每帧的桥调用序列:progress op 按 kind 取当帧进度(一次)→ spring op 取骨架层与内容层两个 alpha(各一次,elapsed = now - toggleMs,initial 取切换瞬间的当前值)→ highlight op 取高亮参数(fade 一次复用到所有块;shimmer 逐块一次,因为半径依赖块尺寸)。ArkTS 侧不 hardcode 0.6 / 400 / 0.01 等任何库常量——全部来自 defaults 缓存;桥整体不可用时才启用镜像 PlaceholderMath 的紧急 fallback。
5.2 页面交互
控制区自上而下:高亮模式三选一(切换即重启高亮计时)、加载状态切换(触发 spring 交叉淡入)、主题切换(重算占位色与高亮色)。Canvas 演示区 5 行骨架(圆形头像 + 双横条,宽度交错模拟上游 sample 的 ListItems 截图);"已加载"态下行内容是真实文本,与骨架按 spring alpha 交叉淡入,并遵守 0.01 / 0.99 层语义(低于 0.01 不画,高于 0.99 满 alpha 画)。shimmer 用 createRadialGradient(圆心 = 块左上角,对应桥返回的 centerX / centerY = 0)叠画。日志区每条带 [HH:mm:ss] 时间戳,→ 行请求摘要、← 行响应摘要,最新在前、上限 100 条;帧内桥调用按 700ms 采样记录避免刷屏。
5.3 对比度和错误边界
深色主题下骨架用亮灰占位、微光高亮为银灰渐变,浅色主题相反;操作按钮语义色与状态行联动。Native 调用全部 try/catch 收进日志区,错误按限频记录,页面永不因桥故障崩溃。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosplaceholder)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.placeholder、versionName 1.0.13-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/libohosplaceholder_api.h
├── src/main/cpp/types/libplaceholder_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(950 行)
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
.\gradlew.bat :placeholder:jvmTest `
:example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp(不是仓库根目录),构建 HAP 后:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.placeholder -m entry
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
四、完整代码对照
4.1 整体架构
com.eygraber.compose.placeholder.PlaceholderMath(上游公式提取,零依赖)
│ 时序 / alpha 曲线 / 半径 / 停点 / spring 闭式解 / 颜色配方
Bridge.kt + MiniJson
│ PlaceholderDispatch / PlaceholderVersion
libplaceholder_napi.so
│ placeholderDispatch / placeholderVersion
Index.ets
│ JSON parse + Canvas 逐帧渲染
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose_placeholder、material/material3/sample 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单版本钉版 |
build.gradle.kts(根) | 版本 1.0.13-ohos.1、全局禁用 Ohos*Test* 任务 |
placeholder/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零第三方依赖 |
placeholder/src/commonMain/.../PlaceholderMath.kt | 唯一新增源文件(上游数学的纯 Kotlin 镜像) |
placeholder/src/composeMain/ | 上游 Placeholder.kt / PlaceholderHighlight.kt(零改动留盘) |
placeholder/src/composeTest/ | 上游 Compose UI 测试(零改动留盘) |
placeholder/src/commonTest/.../PlaceholderMathTest.kt | 新增 20 项纯 Kotlin 交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、六 op 分发、MiniJson、错误通道 |
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 验收页面(三模式 / crossfade / 双主题 / 日志) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(16 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 核心 | PlaceholderMath.tweenProgress | fade/shimmer 的 delay + Restart/Reverse 循环进度 |
| 核心 | PlaceholderMath.fadeAlpha / shimmerAlpha | 两种高亮的当帧 alpha(shimmer 峰值 0.6) |
| 核心 | PlaceholderMath.shimmerRadius / gradientColors / gradientOffsets | 径向渐变几何与三停点 |
| 核心 | PlaceholderMath.springValue | crossfade 弹簧闭式解(默认 stiffness 400 / ζ=1) |
| 核心 | PlaceholderMath.placeholderColor / fadeHighlightColor / shimmerHighlightColor | Material 默认色配方(ARGB 域) |
| Native | PlaceholderDispatch / PlaceholderVersion | C ABI JSON 导出 |
| N-API | placeholderDispatch(req) / placeholderVersion() | 向 ArkTS 暴露能力 |
| ArkTS | tick() / drawFrame() | 帧循环取数 + Canvas 渲染 |
4.4 ArkTS 与 Kotlin 的边界
// 每帧微光:进度和高亮全部来自 Native,ArkTS 只做 Canvas 绘制
const progress = JSON.parse(placeholderDispatch(JSON.stringify({
op: 'progress', kind: 'shimmer', elapsedMs: now - this.highlightStartMs
}))).progress;
const hi = JSON.parse(placeholderDispatch(JSON.stringify({
op: 'highlight', kind: 'shimmer', progress,
highlightArgb: this.shimmerHighlightArgb, widthPx: block.w, heightPx: block.h
})));
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, hi.radius);
// hi.colors / hi.offsets 逐停点 addColorStop,ctx.globalAlpha = hi.alpha * layerAlpha
// 桥内求值:与上游 Shimmer.alpha / Shimmer.brush 同公式
val alpha = PlaceholderMath.shimmerAlpha(p, pMax)
val radius = PlaceholderMath.shimmerRadius(widthPx, heightPx, p)
val colors = PlaceholderMath.gradientColors(highlightArgb)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:提取数学核心,而不是整体编译或 ArkTS 重写
鸿蒙侧没有 androidx.compose 制品,整体编译不可行;用 ArkTS 手写一个骨架屏组件则意味着动画语义与上游彻底脱钩——每个常量、每条曲线都成了"抄写",上游修一个 0.6 峰值或 1700ms 时长,鸿蒙侧毫无感知。提取路线把上游公式镜像进 commonMain 并用 20 项 JVM 测试钉死,PlaceholderMath.kt 成为鸿蒙侧行为的唯一事实来源;上游 Compose 源码零改动留盘,社区工具链对齐后可整体接回,届时提取层自动降级为参考实现。
决策 2:工具链钉保守线 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
提取核心零第三方依赖,不存在 KStore 那种"制品元数据反推版本"的强制力,因此选择社区已反复审核的保守组合(qrose / wavy-slider / Cloudy 同款)。代价是 wrapper 从 9.8.1 降到 8.14.1——对不发 Maven Central 的 fork 无实质影响。
决策 3:根构建替换(cmp_qrose 模式),material / material3 / sample 留盘
上游根构建(conventions 共享插件 0.0.104 / AGP 9.4.1 / detekt / dokka / vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。placeholder-material 与 placeholder-material3 是 MaterialTheme @Composable 包装,鸿蒙没有对应物,留盘不进 settings.gradle.kts;但它们的数值配方(0.1 / 0.3 / 0.75 alpha 与 src-over 合成)是纯函数,已并入 PlaceholderMath——Demo 的深浅主题色即来自这套配方的实时求值。
决策 4:spring 用闭式解而不是数值积分
Compose 的 SpringAnimation 是逐步数值积分,闭式解 target + (x0 - target)(1 + ωt)e^(-ωt) 与之同物理但无状态:ArkTS 每帧只需传 (initial, target, elapsed) 就能得到确定性的值,切换瞬间捕获初值即可,不需要在 ArkTS 侧维护积分器状态,也天然免疫帧率波动。欠阻尼 / 过阻尼档位一并实现(测试验证欠阻尼过冲)。
决策 5:颜色在 ARGB int 域做 8-bit 量化
桥协议用无符号 ARGB 十进制整数传色,compositeOver 因此在 8-bit 量化域实现(如 0x80 alpha = 128/255 而非 0.5)。与 Compose float Color 存在末位舍入差异,测试以量化后的精确值钉住(如 contentColor 10% over white = 0xFFE8E8E8),差异在注释中说明而不是掩盖。
决策 6:上游测试零改动 + 新增测试钉数学
上游 Compose UI 测试一字不动移入 composeTest 留盘,保证接回工具链后测试照跑;新增的 20 项纯 Kotlin 测试跑的就是提取后的公式。交叉验证的有效性来自"上游文件零改动"与"提取公式有独立测试"两端同时成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(win32 10.0.22000 x64)、JDK 21(C:\Users\Administrator\.jdks\jdk-21)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :placeholder:jvmTest
20 项用例全部通过(tests="20" failures="0" errors="0"):
| 测试组 | 用例数 | 钉住什么 |
|---|---|---|
| 时序常量 | 2 | fade 200/600/Reverse、shimmer 200/1700/Restart、峰值 0.6 与上游逐字一致 |
| tween 进度 | 4 | 延迟期恒 0、Restart 锯齿跨周期换算、Reverse 三角波、越界钳制 |
| fade / shimmer alpha | 4 | fade=progress;shimmer 峰值前后线性升降、对称点配对、退化输入不出 NaN |
| 渐变几何 | 2 | 半径 = max(w,h)×p×2(含 0.01 下限与进度钳制)、三停点色与偏移 |
| spring | 4 | 临界阻尼单调收敛不过冲、端点精确、上升下降对称、欠阻尼过冲后回稳 |
| 颜色 | 4 | withAlpha 换 alpha 通道、src-over 合成(半透明/不透明/全透明三档)、深浅主题配方 |
6.3 HAP 验证
:placeholder:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:placeholder:jvmTest: 20 项全部通过
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo 自动回填)
llvm-nm -D: PlaceholderDispatch / PlaceholderVersion / libohosplaceholder_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libplaceholder_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosplaceholder.so arm64 约 3.6MB、x86_64 约 3.3MB(另各配一份约 1.8MB / 1.7MB 的 libc++_shared.so),HAP 内每个 ABI 三件套(libplaceholder_napi.so / libohosplaceholder.so / libc++_shared.so)齐备。
6.4 功能用例
- 启动后标题区出现版本握手
placeholderVersion() → 1.0.13-ohos.1(真实 Native 调用,非静态文字),副标题带Kotlin/Native ohosArm64/ohosX64目标说明; - defaults op 一次读入全部库常量,页面任何数值不在 ArkTS 硬编码;
- 切"微光 Shimmer":骨架块上银灰微光以 200ms 延迟起步、1700ms 一轮从块左上角逐帧生长扫过(径向渐变半径随 progress 增大),无限锯齿循环;
- 切"淡入 Fade":占位色以 200ms 延迟、600ms 一轮往返呼吸(Reverse 语义),无渐变;
- 切"已加载":骨架与真实文本按 spring 交叉淡入(约 300~400ms 视觉收敛),期间两层 alpha 遵守 0.01 / 0.99 层语义;切回"加载中"反向过渡;
- 切深色主题:占位色 / 高亮色立即经 color op 重算(深色下为亮灰骨架 + 银灰微光),背景与文字色联动;
- 日志区按时间戳滚动记录 defaults / progress / highlight / spring / color 调用摘要(帧内调用 700ms 采样),
←行可见ok:true与关键数值; - 向桥发非法 op / 非法参数:返回
ok:false原文进日志,页面不崩。
6.5 验证结论
核心数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,深色主题 + 微光模式下骨架列表的动画时序、渐变几何与主题配色均由 Kotlin/Native 侧实时求值驱动(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-placeholder.git
cd compose-placeholder
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
.\gradlew.bat :placeholder:compileKotlinOhosArm64 :placeholder:compileKotlinOhosX64 # klib
.\gradlew.bat :placeholder:jvmTest # 20 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp 构建签名 HAP 后:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.placeholder -m entry
hdc shell hilog | grep PlaceholderNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让核心真正编译成
ohosArm64/ohosX64动态库;对纯 Compose UI 库,这一步的前提是想清楚"哪些东西编得过去"; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 2.2.21 旧线不支持 Gradle 9.x,上游 wrapper 9.8.1 必须降到 8.14.1(gradle-wrapper.jar 与 gradlew 脚本一并替换);
- Windows 下
gradlew.bat报JAVA_HOME is not set时,除了命令行$env:JAVA_HOME,更稳的是把org.gradle.java.home写进gradle.properties; - 上游 Compose 源码必须移出
commonMain(本仓移入composeMain):未注册的源码集目录 Gradle 会无视,留在原地则 jvm/ohos 编译直接炸androidx.compose引用; - 上游 Compose UI 测试同样必须移出(
composeTest),commonTest只放纯 Kotlin 测试——"测试零改动"与"新构建可编译"靠目录纪律同时成立; - 根构建替换不是删插件那么简单:conventions 插件在根
build.gradle.kts里集中 apply,模块构建文件要整个重写为纯 KMP,libs.versions.toml同步收敛; - 每帧多次 JSON 桥调用(progress + 2×spring + N×highlight)要 try/catch + 日志限频采样,否则 60fps 下日志区刷爆 UI;
- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组,勾选 DevEco 自动签名后由 IDE 回填完整配置,证书绝不进入仓库;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - DevEco Studio 打开的是
example/harmonyApp子目录,不是仓库根目录——根是 Gradle KMP 工程,DevEco 期望 hvigor。
8.2 已知问题
- ohos 侧无 Compose
ModifierAPI:composeMain里的上游实现等社区compose.uiohos 制品与 CMP 1.12 对齐后接回,届时PlaceholderMath降级为参考实现; placeholder-material/placeholder-material3的@Composable颜色扩展不进构建,其数值配方已并入PlaceholderMath(0.1 / 0.3 / 0.75 alpha 与 src-over 合成);- 自定义
PlaceholderHighlight(用户自备Brush的那种)不跨桥:桥只覆盖上游内置的 fade / shimmer 两种; - android / ios / js / wasmJs 目标剥离(源文件留在树里),发布、dokka、detekt 一并剥离,本 fork 不发 Maven Central;
displaySync不可用的设备降级setInterval(16ms)驱动帧循环;- 颜色在 ARGB 8-bit 量化域合成,与 Compose float
Color存在末位舍入差异(测试已钉住量化值); - HAP 安装必须使用与 bundle name
org.terminator.ohos.placeholder匹配的签名。
8.3 未来优化方向
- 跟踪 CPF 社区 Compose ohos 制品线,对齐后把
composeMain接回构建,ArkTS Canvas 降级为无 Compose 环境的 fallback; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
- 桥补一个"自定义高亮"逃生通道(alpha/brush 参数化),覆盖上游自定义
PlaceholderHighlight的部分场景。
九、总结
9.1 核心难点回顾
上游 compose-placeholder(100% Compose Modifier 实现)
→ 确认 androidx.compose 无 ohos 制品、社区 CMP 1.9.2 与上游 1.12 对不齐
→ 选定"数学核心提取"路线(时序 / alpha 曲线 / 渐变几何 / spring 物理 / 颜色配方)
→ 根构建替换(cmp_qrose 模式)+ Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 保守线
→ PlaceholderMath.kt 进 commonMain,ohosArm64 / ohosX64 KLIB
→ C ABI(@CName × 2)+ 六 op JSON 契约
→ C++ N-API(DisposeString 纪律)
→ ArkTS Canvas 逐帧取数渲染(零硬编码常量)
→ ArkUI 真机单页验收(三模式 / crossfade / 双主题 / 日志 / 版本握手)
9.2 封装层次
- 核心数学层:
PlaceholderMath——上游时序、alpha 曲线、渐变几何、spring 闭式解与 Material 颜色配方的纯 Kotlin 镜像; - 留盘层:
composeMain/composeTest/ material 模块 / sample——上游原件零改动,等工具链对齐接回; - Native 层:双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、defaults 常量缓存、帧循环取数、Canvas 绘制、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 纯 Compose UI 库的适配切口是"数字层":
Modifier带不过鸿蒙,但时序常量、alpha 曲线、渐变几何、弹簧物理、颜色配方全是与 UI 框架解耦的纯数学——把它们提取成commonMain的单一事实来源并用 JVM 测试钉死,比"ArkTS 重写"或"硬等 Compose 制品"都更能立刻交付可验证的价值; - 工具链版本跟着约束走而不是跟着新鲜度走:核心零依赖时,社区已审的保守组合(Kotlin 2.2.21 + Gradle 8.14.1)就是最优解;有制品元数据强制时(如 KStore 的 2.4.20 线)再升级——两条线的经验不能互相套用;
- "数学没漂"的证明 = 上游文件零改动 + 提取公式有独立测试 + ArkTS 零硬编码常量:三者同时成立,真机上的每一帧动画才可归因到上游公式,而不是 ArkTS 侧的某次手抄。
9.4 适配成果
当前适配已完成:
- compose-placeholder
1.0.13-ohos.1(基于上游已发布1.0.12之后的 master@64ff938)核心数学的双 ABI Kotlin/Native 编译(上游 Compose 源码与 UI 测试零改动留盘,新增PlaceholderMath.kt一个源文件); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),六 op JSON 契约覆盖时序 / 进度 / 高亮 / spring / 颜色全部能力; - DevEco Stage ArkUI 单页验收页面(950 行):无 / 淡入 / 微光三模式、加载⇄内容 spring 交叉淡入、深浅主题、操作日志与版本握手;
- 20 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)、上游原文存档与本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐



所有评论(0)