本文记录 compose-placeholder 接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的依赖边界盘点、鸿蒙工具链钉版、"数学核心提取"路线选型、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-placeholder 是 Accompanist placeholder 2023 年 7 月废弃后的接棒 fork:它提供一个 Compose Modifier.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 / valueprogress、highlight、spring 三个 op 的数值结果。
radius / centerX / centerY / colors / offsetsshimmer 渐变几何与停点(colors 为无符号 ARGB 十进制)。
argbcolor 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 C:\Users\Administrator\.jdks\jdk-21,已写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 9.8.1 降级;2.2.21 旧线不支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.placeholder
$env:JAVA_HOME = "C:\Users\Administrator\.jdks\jdk-21"
java -version
1.3 源码依赖度盘点
模块文件依赖形态处置
placeholder(上游实现)2100% androidx.compose.*(Modifier / DrawModifierNode / Brush / Animatable / InfiniteRepeatableSpec)整体移入 composeMain 留盘,ohos 不编译
PlaceholderMath.kt(新增)1纯 Kotlin 标准库,零第三方进 commonMain(jvm + 双 ohos 编译)
placeholder-material / placeholder-material34MaterialTheme @Composable 颜色扩展留盘;数值配方(0.1 / 0.3 / 0.75 alpha 与合成规则)并入 PlaceholderMath
sample8Compose 示例 + 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 方法覆盖核心
PlaceholderDispatchplaceholderDispatch(req)时序 / alpha / 渐变 / spring / 颜色 全部数学
PlaceholderVersionplaceholderVersion()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.tomlKotlin 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.kt2 个 C ABI、六 op 分发、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(三模式 / crossfade / 双主题 / 日志)
README.OpenHarmony_CN.md鸿蒙适配主文档(16 节)

4.3 关键 API 对照

层次API作用
核心PlaceholderMath.tweenProgressfade/shimmer 的 delay + Restart/Reverse 循环进度
核心PlaceholderMath.fadeAlpha / shimmerAlpha两种高亮的当帧 alpha(shimmer 峰值 0.6)
核心PlaceholderMath.shimmerRadius / gradientColors / gradientOffsets径向渐变几何与三停点
核心PlaceholderMath.springValuecrossfade 弹簧闭式解(默认 stiffness 400 / ζ=1)
核心PlaceholderMath.placeholderColor / fadeHighlightColor / shimmerHighlightColorMaterial 默认色配方(ARGB 域)
NativePlaceholderDispatch / PlaceholderVersionC ABI JSON 导出
N-APIplaceholderDispatch(req) / placeholderVersion()向 ArkTS 暴露能力
ArkTStick() / 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"):

测试组用例数钉住什么
时序常量2fade 200/600/Reverse、shimmer 200/1700/Restart、峰值 0.6 与上游逐字一致
tween 进度4延迟期恒 0、Restart 锯齿跨周期换算、Reverse 三角波、越界钳制
fade / shimmer alpha4fade=progress;shimmer 峰值前后线性升降、对称点配对、退化输入不出 NaN
渐变几何2半径 = max(w,h)×p×2(含 0.01 下限与进度钳制)、三停点色与偏移
spring4临界阻尼单调收敛不过冲、端点精确、上升下降对称、欠阻尼过冲后回稳
颜色4withAlpha 换 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 功能用例

  1. 启动后标题区出现版本握手 placeholderVersion() → 1.0.13-ohos.1(真实 Native 调用,非静态文字),副标题带 Kotlin/Native ohosArm64/ohosX64 目标说明;
  2. defaults op 一次读入全部库常量,页面任何数值不在 ArkTS 硬编码;
  3. 切"微光 Shimmer":骨架块上银灰微光以 200ms 延迟起步、1700ms 一轮从块左上角逐帧生长扫过(径向渐变半径随 progress 增大),无限锯齿循环;
  4. 切"淡入 Fade":占位色以 200ms 延迟、600ms 一轮往返呼吸(Reverse 语义),无渐变;
  5. 切"已加载":骨架与真实文本按 spring 交叉淡入(约 300~400ms 视觉收敛),期间两层 alpha 遵守 0.01 / 0.99 层语义;切回"加载中"反向过渡;
  6. 切深色主题:占位色 / 高亮色立即经 color op 重算(深色下为亮灰骨架 + 银灰微光),背景与文字色联动;
  7. 日志区按时间戳滚动记录 defaults / progress / highlight / spring / color 调用摘要(帧内调用 700ms 采样),← 行可见 ok:true 与关键数值;
  8. 向桥发非法 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让核心真正编译成 ohosArm64 / ohosX64 动态库;对纯 Compose UI 库,这一步的前提是想清楚"哪些东西编得过去";
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21 旧线不支持 Gradle 9.x,上游 wrapper 9.8.1 必须降到 8.14.1(gradle-wrapper.jar 与 gradlew 脚本一并替换);
  4. Windows 下 gradlew.bat 报 JAVA_HOME is not set 时,除了命令行 $env:JAVA_HOME,更稳的是把 org.gradle.java.home 写进 gradle.properties;
  5. 上游 Compose 源码必须移出 commonMain(本仓移入 composeMain):未注册的源码集目录 Gradle 会无视,留在原地则 jvm/ohos 编译直接炸 androidx.compose 引用;
  6. 上游 Compose UI 测试同样必须移出(composeTest),commonTest 只放纯 Kotlin 测试——"测试零改动"与"新构建可编译"靠目录纪律同时成立;
  7. 根构建替换不是删插件那么简单:conventions 插件在根 build.gradle.kts 里集中 apply,模块构建文件要整个重写为纯 KMP,libs.versions.toml 同步收敛;
  8. 每帧多次 JSON 桥调用(progress + 2×spring + N×highlight)要 try/catch + 日志限频采样,否则 60fps 下日志区刷爆 UI;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组,勾选 DevEco 自动签名后由 IDE 回填完整配置,证书绝不进入仓库;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. DevEco Studio 打开的是 example/harmonyApp 子目录,不是仓库根目录——根是 Gradle KMP 工程,DevEco 期望 hvigor。

8.2 已知问题

  • ohos 侧无 Compose Modifier API:composeMain 里的上游实现等社区 compose.ui ohos 制品与 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 三条经验

  1. 纯 Compose UI 库的适配切口是"数字层":Modifier 带不过鸿蒙,但时序常量、alpha 曲线、渐变几何、弹簧物理、颜色配方全是与 UI 框架解耦的纯数学——把它们提取成 commonMain 的单一事实来源并用 JVM 测试钉死,比"ArkTS 重写"或"硬等 Compose 制品"都更能立刻交付可验证的价值;
  2. 工具链版本跟着约束走而不是跟着新鲜度走:核心零依赖时,社区已审的保守组合(Kotlin 2.2.21 + Gradle 8.14.1)就是最优解;有制品元数据强制时(如 KStore 的 2.4.20 线)再升级——两条线的经验不能互相套用;
  3. "数学没漂"的证明 = 上游文件零改动 + 提取公式有独立测试 + 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。

参考文档

Logo

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

更多推荐