开源鸿蒙平台 CMP 三方库「compose-multiplatform-AnimatedImage」适配全流程
本文记录
compose-multiplatform总仓库中的三方组件库components/AnimatedImage接入 OpenHarmony 的完整过程,覆盖 CMP 组件库盘点、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。
components/AnimatedImage是 Compose Multiplatform 的动图组件:上游commonMain以expect class AnimatedImage/loadAnimatedImage(path)/@Composable animate(): ImageBitmap承诺"一串带时长的帧,每帧重组一次"。本次适配让这条帧序列模型进入 OpenHarmony ARM64 运行时,ArkUI 页面作为真机验收宿主,Compose Multiplatform(CMP)应用可以通过同一个组件语义在鸿蒙侧消费逐帧结果。
项目地址: AtomGit/oh-tpc/compose-multiplatform-AnimatedImage
上游仓库: JetBrains/compose-multiplatform
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
components/AnimatedImage 来自 JetBrains 的 Compose Multiplatform 总仓库,制品名 components-animated-image。在 CMP 桌面应用里,AnimatedImage(path) + animate() 两个调用即可装载并逐帧播放动图:桌面 actual 用 Skia Codec 解码,codec.frameCount 给出帧数,缺省帧时长按 DEFAULT_FRAME_DURATION = 100 兜底,rememberInfiniteTransition 驱动帧号无限迭代,每渲染一帧重组一次调用方。鸿蒙应用同样需要这套能力,但组件的 actual 依赖 androidx.compose.ui 与 org.jetbrains.skia,默认构建不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示动画,却无法证明组件的帧序列模型已经在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证模型层、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB 与动态库。 |
| 总仓库无根构建 | 上游是约 30 个互相独立的 Gradle 工程(Kotlin 2.4.20 / Gradle 9.x / AGP 9.x),鸿蒙插件一个都不认。 |
| 工具链不一致 | 鸿蒙侧社区 CMP 是 1.9.2 线(HarmonyOS Kotlin 2.2.21-1.0.0 + skiko-ohosarm64),与本快照 Compose 1.12-alpha 对不齐。 |
| actual 依赖 Compose | 组件的 Skia/Compose actual 无法在鸿蒙工具链下编译,必须走模型层复用。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在帧序列模型、C ABI/N-API 桥接和 ArkUI 验收层。动画语义仍由组件模型负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
示例提供四个可观察场景(对应 Demo 的四个 Tab):
- AnimatedImage(核心):
kind=animated按帧号确定性出一帧 PNG,对应上游animate(): ImageBitmap每帧返回一图; - 概览自检:真实执行总仓库
ui-tooling-preview的PreviewParameterProvider接口,验证 KLIB 层; - SplitPane:分栏布局模型,比例/方向改变时与
dividerPos几何对账; - 重组模型:状态变化时只有输入变化的节点"重组",其余跳过,演示 Compose 重组语义。
桥返回的 RenderOk 结果包含:
| 字段 | 作用 |
|---|---|
ok / error | 成功标志与错误信息,桥不把异常抛过 C 边界。 |
kind | info、splitpane、animated、recompose 四个稳定标识。 |
frame / frames / pattern | 当前帧号、总帧数、帧序列模式(orbit/pulse/wave)。 |
width / height / bytes | 出图尺寸与 PNG 字节数,供确定性对账。 |
pngBase64 | 当前帧 PNG 的 Base64,ArkTS 解码为 PixelMap 显示。 |
recomposed / total / nodes | 重组模型的逐节点状态(recomposed/skipped)。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 模型复用 | AnimatedImage 帧序列语义(帧数/帧时长/无限迭代/每帧一图)在 KN 侧确定性复现。 |
| 平台目标 | 根构建为 ohos/ui-tooling-preview 与 example/nativeApp 增加 ohosArm64/ohosX64,生成双 ABI 动态库。 |
| 桥接稳定 | 使用单个 C ABI(@CName("CmpRender"))和 UTF-8 JSON,不传递 Kotlin 对象地址。 |
| CMP 兼容 | CMP 应用复用同一组件语义;ArkUI 只负责真机验收。 |
| UI 完整 | 支持帧模式切换、帧数切换、逐帧滑条、播放/暂停和错误展示。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以在 Compose 1.9.2 线 ohos 发行版上使用同一组件语义;本仓库跟踪的是 1.12 快照的模型层交付。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点总仓库独立工程、组件源集和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── 新建根构建、加入 ohosArm64/ohosX64、srcDir 引用 KLIB
第 3 阶段:组件模型建模 ── 帧序列/分栏/重组三种模型、JSON 契约和确定性对账
第 4 阶段:原生桥接 ── C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 四 Tab 页面、播放定时器和 PixelMap 生命周期
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
components/AnimatedImage/ 上游被适配组件(expect API + Skia actual,零改动)
ohos/ui-tooling-preview/ 总仓库唯一零依赖 KMP 库 -> ohos klib(srcDir 引用,不复制)
example/nativeApp/ Kotlin/Native 动态库(CmpRender 桥 + 光栅化 + PNG)
example/harmonyApp/ DevEco Stage、N-API 和 ArkTS 四 Tab 页面
doc/ 适配全流程博客与真机效果图
根构建是新建的 settings.gradle.kts,不把任何上游独立工程 include 进来;Gradle(鸿蒙侧)和 Hvigor(HAP 侧)分开执行,上游工程继续用自己的 Kotlin 2.4.20 工具链独立构建。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| Gradle | 8.14.1(wrapper 钉死) |
| JDK | 21 |
| OpenHarmony 目标 | ohosArm64 + ohosX64 |
| DevEco compatibleSdkVersion | 5.1.0(18) |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.cmp |
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
java -version
注意:DevEco 自带 JBR 是 JDK 25,Gradle 8.14.1 不支持,命令行构建要独立指向 JDK 21。
1.3 页面边界
标题区 COMPOSE-MULTIPLATFORM-ANIMATEDIMAGE / CMP 组件模型 Demo
Tab 区 概览 / SplitPane / AnimatedImage(核心)/ 重组模型
控制区 帧模式(orbit/pulse/wave)、帧数(12/24/36)、帧滑条、播放/暂停
图像区 Image(PixelMap) 展示当前帧 PNG
状态区 kind / 尺寸 / png 字节数 / 元数据行 / 错误文本
页面启动时通过 kind=info 自检工具链与 KLIB,切到 AnimatedImage Tab 即出第 0 帧。
第 2 阶段:目标与依赖打通
2.1 新建根构建并加入 ohos 目标
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
}
}
include(":ohos:ui-tooling-preview")
include(":example:nativeApp")
ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件,插件仓库必须先写 eazytec Maven 再写 gradlePluginPortal(),否则配置期直接 Unresolved reference: ohosArm64。
2.2 KLIB:srcDir 引用上游源码
kotlin {
applyDefaultHierarchyTemplate()
jvm()
ohosArm64()
ohosX64()
sourceSets {
val commonMain by getting {
kotlin.srcDir(rootProject.layout.projectDirectory
.dir("components/ui-tooling-preview/library/src/commonMain/kotlin"))
}
val nonAndroidDir = rootProject.layout.projectDirectory
.dir("components/ui-tooling-preview/library/src/nonAndroidMain/kotlin")
val jvmMain by getting { kotlin.srcDir(nonAndroidDir) }
val ohosMain by getting { kotlin.srcDir(nonAndroidDir) }
}
}
总仓库里唯一 commonMain 零依赖的 KMP 库是 ui-tooling-preview(@Preview / PreviewParameterProvider API)。用 srcDir 引用而不是复制,上游工程一个字节都不用改——这是"适配构建与上游构建物理隔离"的关键一步。
2.3 构建动态库
./gradlew :ohos:ui-tooling-preview:compileKotlinJvm
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
linkDebugShared* 产出 libohoscmp.so(arm64 ≈7.6MB、x86_64 ≈6.7MB,含 Kotlin/Native 运行时),并由 copyKnSo* 任务把匹配 triple 的 libc++_shared.so 一起拷进:
example/harmonyApp/entry/libs/arm64-v8a/libohoscmp.so
example/harmonyApp/entry/libs/arm64-v8a/libc++_shared.so
example/harmonyApp/entry/libs/x86_64/libohoscmp.so
example/harmonyApp/entry/libs/x86_64/libc++_shared.so
第 3 阶段:组件模型建模
3.1 JSON 契约
Index.ets
↓ cmpRender(JSON.stringify(request))
CmpRender(const char*)
↓ MiniJson 解析 + 模型计算 + 光栅化
frameN 的确定性 PNG
↓ Base64 JSON
N-API cmpRender()
↓
ArkUI Image(PixelMap)
页面不复制动画业务判断,每一帧都由 Kotlin/Native 代码生成。
3.2 帧序列模型(核心)
上游 animate() 的三要素——帧数、每帧时长、无限迭代——映射到桥协议:
private fun renderAnimated(json: MiniJson): String {
val frames = json.int("frames", 24).coerceIn(8, 48) // 对应 codec.frameCount
val frame = json.int("frame", 0).coerceIn(0, frames - 1)
val pattern = json.str("pattern").ifBlank { "orbit" }
...
val t = frame.toFloat() / frames
when (pattern) {
"pulse" -> { /* 三环相位扩散 */ }
"wave" -> { /* 正弦条形 */ }
else -> { /* orbit:卫星绕环带拖尾 */ }
}
canvas.drawSevenSegment(16, size - 34, 3,
frame.toString().padStart(2, '0'), theme.accent, theme.paneB)
canvas.fillRect(0, size - 4, (size * frame / (frames - 1)), 4, theme.accent)
...
}
同一 (pattern, frame, frames, theme) 输入永远产出同一字节的 PNG——animate() 的"每帧返回一张确定的图"在鸿蒙侧就是对账基线。播放节奏由 ArkTS 定时器驱动(演示档 200ms,对应上游缺省帧时长 100ms 的量级),桥保持无状态。
3.3 对照模型
splitpane 复现分栏几何并返回 dividerPos 像素值;recompose 接收新旧两组状态,逐节点给出 recomposed/skipped;info 真实执行上游接口:
class DemoPreviewParameters : PreviewParameterProvider<String> {
override val values: Sequence<String> = sequenceOf(
"Compose", "Multiplatform", "OpenHarmony", "ohosArm64", "ohosX64", "@Preview",
)
}
previewCount = 6 由上游 nonAndroidMain 的默认实现 values.count() 在设备上算出——KLIB 层不是摆设。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry (libcmp_napi.so)
│ const char*
▼
Kotlin/Native C ABI (libohoscmp.so)
│ CmpRender
▼
UTF-8 JSON + DisposeString
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 函数挂到 ArkTS 模块上:libohoscmp.so 里 nm 得到 CmpRender,但 ArkTS 直接 import 它什么都看不到。所以必须 CMake 再包一层 NAPI。
4.2 Kotlin/Native 导出函数
@CName("CmpRender")
fun cmpRender(request: String): String {
return try {
val json = MiniJson.parse(request)
when (json.str("kind").lowercase()) {
"info" -> renderInfo(json)
"splitpane" -> renderSplitPane(json)
"animated" -> renderAnimated(json)
"recompose" -> renderRecompose(json)
else -> renderInfo(json)
}
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
}
单入口、JSON 进 JSON 出,失败路径也在 JSON 里返回,Kotlin 异常永远不跨过 C 边界。
4.3 C++ N-API 方法与内存释放
static void DisposeKnString(const char *raw) {
if (raw == nullptr) {
return;
}
libohoscmp_ExportedSymbols *symbols = libohoscmp_symbols();
if (symbols != nullptr && symbols->DisposeString != nullptr) {
symbols->DisposeString(raw);
}
}
const char *raw = CmpRender(request.c_str());
napi_value out = nullptr;
napi_status st = napi_create_string_utf8(env, raw, NAPI_AUTO_LENGTH, &out);
DisposeKnString(raw);
KN 返回的字符串必须用 libohoscmp_symbols()->DisposeString 释放,不能 free,也不能让 ArkTS 管。N-API 层只做参数类型检查、字符串创建和释放。
4.4 CMake 和类型声明
add_library(ohoscmp SHARED IMPORTED)
set_target_properties(ohoscmp PROPERTIES
IMPORTED_LOCATION "${KN_SO_DIR}/libohoscmp.so"
IMPORTED_NO_SONAME TRUE)
add_library(cmp_napi SHARED napi_init.cpp)
target_link_libraries(cmp_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohoscmp)
export const cmpRender: (request: string) => string;
abiFilters 同时写 arm64-v8a 和 x86_64;HAP 里每个 ABI 目录三件套(libcmp_napi.so / libohoscmp.so / libc++_shared.so)缺一不可。
第 5 阶段:ArkUI 宿主
5.1 调桥与出图
import { cmpRender } from 'libcmp_napi.so';
const raw: string = cmpRender(JSON.stringify({
kind: 'animated', frame: this.frame, frames: this.framesCount,
pattern: this.pattern, theme: this.theme
}));
const parsed: RenderOk = JSON.parse(raw) as RenderOk;
this.decodePng(parsed.pngBase64);
private decodePng(b64: string): void {
const helper = new util.Base64Helper();
const bytes: Uint8Array = helper.decodeSync(b64);
const sliced = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
const src = image.createImageSource(sliced);
src.createPixelMap().then((pm: image.PixelMap) => {
const old = this.pixelMap;
this.pixelMap = pm;
old?.release();
});
}
旧的 PixelMap 换新前 release(),连续播放不涨内存。
5.2 页面交互
AnimatedImage Tab 支持三种帧模式、三档帧数、逐帧滑条和播放/暂停;播放用 setInterval 每 200ms 推进 frame = (frame + 1) % frames,对应上游 rememberInfiniteTransition 的无限迭代。其余三个 Tab 复用同一个 generate() 状态机。
5.3 对比度和错误边界
选中 Tab 与控制按钮使用深色底白字,浅色按钮使用深色文字,状态行使用中灰。生成期间 generating 置位防止重入;桥返回 ok:false 时错误文本显示在状态区,页面不崩。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/src/ohosMain/kotlin/Bridge.kt CmpRender + 四种模型
├── nativeApp/src/ohosMain/kotlin/Rasterizer.kt 纯 Kotlin 画布 + 七段数码管
├── nativeApp/src/ohosMain/kotlin/PngEncoder.kt zlib stored PNG
└── harmonyApp/
├── entry/src/main/cpp/CMakeLists.txt
├── entry/src/main/cpp/napi_init.cpp
├── entry/src/main/cpp/types/libcmp_napi/index.d.ts
└── entry/src/main/ets/pages/Index.ets
6.2 构建、签名和安装
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
~/ohos/command-line-tools/bin/hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
真机安装需要签名:DevEco Studio 打开 example/harmonyApp,Signing Configs 勾选自动生成(build-profile.json5 已写 "signingConfig": "default")。
hdc list targets
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.cmp
四、完整代码对照
4.1 整体架构
components/AnimatedImage(上游 expect API,零改动)
│ 帧序列语义:frameCount / 帧时长 / 无限迭代 / 每帧一图
▼
Bridge.kt(example/nativeApp)
│ CmpRender:info / splitpane / animated / recompose
▼
libcmp_napi.so
│ cmpRender(request: string): string
▼
Index.ets
│ JSON parse + PixelMap + 定时器
▼
ArkUI 真机页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 鸿蒙根构建:eazytec 仓库、只 include 两个 ohos 模块 |
ohos/ui-tooling-preview/build.gradle.kts | jvm + ohosArm64 + ohosX64,srcDir 引用上游源码 |
example/nativeApp/.../Bridge.kt | @CName("CmpRender")、四种模型、MiniJson |
example/nativeApp/.../Rasterizer.kt | 纯 Kotlin 画布、七段数码管、五套色板 |
example/nativeApp/.../PngEncoder.kt | zlib stored PNG,零外部依赖 |
example/harmonyApp/.../napi_init.cpp | N-API 导出、参数检查、DisposeString |
example/harmonyApp/.../CMakeLists.txt | 导入 KN so、包装 libcmp_napi.so、双 ABI |
example/harmonyApp/.../Index.ets | ArkUI 四 Tab 页面 |
doc/img/animated_realdevice.jpeg | 真机效果图 |
README.OpenHarmony_CN.md | 中文主文档(路线、协议、踩坑、验收) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 上游 | AnimatedImage.animate(): ImageBitmap | 每帧返回一图(鸿蒙侧 = 每次调用返回一帧 PNG) |
| 上游 | codec.frameCount | 帧数(= frames) |
| 上游 | DEFAULT_FRAME_DURATION = 100 | 缺省帧时长(演示档 200ms 定时器) |
| KLIB | PreviewParameterProvider.count/values | 概览页真实执行的上游接口 |
| Native | CmpRender | 单入口 JSON 桥 |
| N-API | cmpRender | 向 ArkTS 暴露唯一函数 |
| ArkTS | generate() / decodePng() | 调桥、解码、刷新 PixelMap |
4.4 ArkTS 与 Kotlin 的边界
const raw: string = cmpRender(JSON.stringify({
kind: 'animated', frame: this.frame, frames: this.framesCount, pattern: this.pattern
}));
@CName("CmpRender")
fun cmpRender(request: String): String
两者之间只传输 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:新建根构建,不并入上游工程
上游约 30 个独立工程钉在 Kotlin 2.4.20 / Gradle 9.x,合并即配置期死锁。根构建只 include 鸿蒙相关模块,上游工程零改动、仍可独立构建。
决策 2:AnimatedImage 走模型层复用,不编 Compose actual
组件 actual 依赖 androidx.compose.ui / org.jetbrains.skia,鸿蒙工具链(CMP 1.9.2 线)对不齐 1.12 快照。帧序列模型(帧数/帧时长/迭代/每帧一图)在 KN 侧确定性复现,ArkTS 只消费 PNG——与社区已过审的 Vico / Reorderable / cmp_qrose 同一条路。
决策 3:KLIB 用 srcDir 引用,源码零复制
ui-tooling-preview 是总仓库唯一零依赖 KMP 库,作为"本快照源码集可被鸿蒙工具链消化"的编译级证明;srcDir 引用让适配构建与上游构建共享源码、互不污染。
决策 4:单入口 JSON 作为跨语言契约
一个 @CName、一个 cmpRender、扁平 JSON 协议。C 签名 const char* -> const char* 永不变,ArkTS 侧 cmpRender(string): string 永不变,扩展字段只加 JSON 键。
决策 5:ArkUI 作为验收宿主,CMP 复用组件语义
当前页面验证 OpenHarmony Native 链路;等 1.12 线 ohos 制品落地,同一模型语义可直接接回 Skia 真解码路径。
六、测试与验证
6.1 测试环境
本次验证使用 macOS arm64、JDK 21(homebrew openjdk@21 21.0.12.1)、DevEco Studio 5.1.0 Release 与配套 command-line-tools(hvigorw/ohpm/hdc)、HarmonyOS Kotlin 2.2.21-1.0.0,真机为 HUAWEI HAD-W24,设备序列号 3QC0124C20001268,系统 HarmonyOS 7.0.0.107(arm64-v8a)。
6.2 编译与链接验证
./gradlew :ohos:ui-tooling-preview:compileKotlinJvm
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
nm -gU example/harmonyApp/entry/libs/arm64-v8a/libohoscmp.so # CmpRender / libohoscmp_symbols
JVM 与 ohos 双目标编译同一份上游源码;dividerPos = margin + avail × f 与 同输入同字节 PNG 构成两条确定性对账线。
6.3 HAP 验证
CompileArkTS: finished
BuildNativeWithCmake / BuildNativeWithNinja: finished
PackageHap: finished
SignHap: finished (DevEco 自动签名)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
unsigned debug HAP ≈6.56MB,内含双 ABI 三件套 so 与 ets/modules.abc;真机走自动签名后的 entry-default-signed.hap。
6.4 功能用例
- 启动后自动执行
kind=info自检,六行元数据即时显示(previewCount=6); - AnimatedImage Tab 切换 orbit / pulse / wave 三种帧模式,画面与状态行同步刷新;
- 切换 12 / 24 / 36 帧后帧号归零、播放停止,逐帧滑条与底部帧号读数一致;
- 播放/暂停正常,连续播放期间状态行
png=字节数稳定在 262488B(256×256 帧); - SplitPane 拖动比例、切方向/主题,
dividerPos与公式对账; - 重组模型只对输入变化的节点标强调框,状态行给出
重组 n/5与逐节点串; - 非法输入(如缺字段)由桥回
ok:false,页面状态区显示错误文本,应用不崩。
6.5 验证结论
KLIB、Native 动态库、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在 HUAWEI HAD-W24 上成功安装并进入前台,AnimatedImage 帧序列在真机逐帧播放,状态行展示了真实尺寸、字节数与元数据。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为 “compose-multiplatform · OpenHarmony”,副标题标注 “CMP 组件模型 Demo · Kotlin/Native ohosArm64/ohosX64”;
- 顶部 Tab 选中 AnimatedImage;
- 帧模式选中
pulse(三环相位扩散),帧数选中 36 帧,帧滑条位于 30/35; - 画面中央为当前帧渲染结果,左下角七段帧号读数,底部进度条随帧推进;
- 状态行显示
生成成功 animated 256x256 png=262488B——每帧字节数与确定性对账一致; - 播放按钮可随时启停连续播放。
7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-multiplatform-AnimatedImage.git
cd compose-multiplatform-AnimatedImage
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
./gradlew :ohos:ui-tooling-preview:compileKotlinJvm
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
~/ohos/command-line-tools/bin/hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.cmp
八、遗留问题与改进方向
8.1 踩坑复盘
- 上游总仓库不能直接加
ohosArm64()——Kotlin 2.4.20 / Gradle 9 与鸿蒙插件不兼容,必须新建根构建物理隔离; - DevEco 自带 JBR(JDK 25)跑不动 Gradle 8.14.1,命令行构建要独立 JDK 21;
pluginManagement仓库顺序错误会在配置期报Unresolved reference: ohosArm64,eazytec Maven 必须在gradlePluginPortal()之前;- KN so 必须与同 triple 的
libc++_shared.so一起拷入 HAP libs,只拷主 so 会dlopen失败; - ArkTS 只能 import CMake 包装的
libcmp_napi.so,CAdapter 不会把@CName业务符号挂给 ArkTS; - KN 返回字符串必须经
DisposeString释放,不能free; - Kotlin 源文件漏
import(本次实际踩到kotlin.math.PI)在 ohos 编译期立刻暴露,编译期修掉即可。
8.2 已知问题
- 帧内容为生成式演示帧(orbit/pulse/wave),不是真实动图文件解码;接真解码要等 1.12 线 ohos 制品;
- PNG 使用 zlib stored 未压缩块,256×256 单帧 ≈262KB,连续播放每帧过 NAPI 的 Base64 约 350KB;
- 组件的 Compose/Skia actual 不在此 fork 编译,ohos 上的 CMP 运行时由社区 1.9.2 线承担;
ui-tooling-previewAPI 在上游已标@Deprecated(迁往 androidx 线),它是本快照唯一零依赖 KMP 库,作为编译级证明保留;- 演示帧时长 200ms 为定值,未透出每帧独立时长字段。
8.3 未来优化方向
- 桥协议增加
delays字段,逐帧时长对齐framesInfo[i].duration语义; - 在 KN 侧实现纯 Kotlin 的 GIF 解码(LZW + 帧处置),替换生成式演示帧;
- 待社区 1.12 线 ohos 制品落地,把
animate()接回 Skia 真解码与 Compose 场景; - 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
- 补充模拟器(arm64/x86_64)与不同 API 版本的验收矩阵。
九、总结
9.1 核心难点回顾
compose-multiplatform 总仓库(无根构建,Kotlin 2.4.20)
→ 取出 components/AnimatedImage 的帧序列模型
→ 新建根构建 + ohosArm64/ohosX64
→ C ABI(CmpRender)
→ C++ N-API(libcmp_napi.so)
→ ArkTS JSON 解析 + PixelMap
→ ArkUI 真机逐帧播放
9.2 封装层次
- 模型层:AnimatedImage 帧序列语义(帧数/帧时长/迭代/每帧一图)确定性复现;
- KLIB 层:总仓库唯一零依赖库
ui-tooling-preview编译为 ohos klib; - Native 层:生成双 ABI 动态库并通过单 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理 Tab 状态、播放定时器、
PixelMap生命周期和错误文本; - DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配总仓库里的组件,先新建独立的鸿蒙根构建,再用 srcDir/模型引用复用上游,绝不污染上游工程;
- 用单入口 JSON 和少量 C ABI 代替跨语言对象传递,并明确字符串释放责任;
- 分别记录编译、链接、符号、HAP 构建和真机运行,用"同输入同字节 PNG"这类确定性对账避免把编译成功误认为设备链路可用。
9.4 适配成果
当前适配已完成:
components/AnimatedImage帧序列模型的 OpenHarmony 交付(双 ABIlibohoscmp.so+libcmp_napi.so);- 总仓库唯一零依赖 KMP 库
ui-tooling-preview的ohosArm64/ohosX64KLIB; - 单入口 C ABI 与 C++ N-API 桥接;
- DevEco Stage ArkUI 四 Tab 验收页面(AnimatedImage 为核心);
- HAP 构建、自动签名、设备安装和 HAD-W24 真机验证;
- AtomGit 项目文档、OpenHarmony README、本博客与本地效果图(
doc/img/)。
适配过程 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)