本文记录 compottie 接入 OpenHarmony 的完整过程,覆盖 CMP Lottie 动画库盘点、ohosArm64/ohosX64 双目标、Compose 1.12 鸿蒙工具链线、vendored keight 表达式引擎、无头帧渲染入口、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。

compottie 是 Compose Multiplatform 生态最完整的 Lottie 动画库,对外 API 是 LottieAnimation / rememberLottiePainter 两个 @Composable 入口,Lottie JSON 解析、图层/关键帧/遮罩/蒙版/渐变/表达式、dotLottie、网络加载、Compose Resources 加载全部收在 7 个 Gradle 模块里。本次适配与已过审的 cmp_qrose / ComposeCharts 的「composeMain 隔离」路线不同:社区 Maven 上已经出现带 ohosArm64/ohosX64 变体的 Compose 1.12 鸿蒙分支(1.12.0-0.1.0-09,配 Kotlin 2.4.20-0.1.0-07),而 compottie 2.3.3 的源码本来就是按 Kotlin 2.4 + Compose 1.12 写的——所以这次是库本体(解析 + 动画引擎 + Skia 渲染 + 表达式)零隔离直接编进 OpenHarmony,仅对三个无 ohos 制品的依赖做小刀替换,再通过 LottieFrameRenderer 无头渲染入口 + @CName 三符号桥暴露给 ArkTS;鸿蒙真机上渲染出的帧与 Android/iOS/Desktop 同源同款。

项目地址: AtomGit/oh-tpc/compottie

上游仓库: alexzhirkevich/compottie

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compottie 为 Compose 应用提供生产级 Lottie 能力:上游发布坐标 io.github.alexzhirkevich:compottie:2.3.3,compottie-core 单模块 187 个 Kotlin 文件(73 个纯 Kotlin + 114 个依赖 Compose),关键帧插值、图层树、表达式求值(纯 Kotlin 的 keight ECMAScript 解释器)、遮罩/蒙版/渐变/混合模式全部在 commonMain,渲染落在 Compose Canvas(Skia)上,支持 Android / iOS / JVM / JS / Wasm 五个目标。OpenHarmony 应用同样需要这套动画能力,但默认构建不会生成 OpenHarmony 的 Kotlin/Native 变体。

如果只在 ArkTS 里重写一个 Lottie 播放器,页面能动,却无法证明公共 Kotlin 动画引擎已经进入 OpenHarmony ARM64 设备完成编译、链接和执行,也无法保证同一份动画 JSON 在两端出同款画面。适配需要同时验证 KMP 动画内核、Compose/Skia 鸿蒙分支、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB 与动态库。
工具链断层上游 Kotlin 2.4.0 / Compose 1.12.1 / AGP 9.3.2 / Gradle 9.7.1;社区此前只有 Kotlin 2.2.21 + Compose 1.9.2 线(qrose/ComposeCharts 适配所用),把 2.3.3 源码全面降级工作量不可控。
三个依赖无 ohos 制品keight(表达式引擎)、kotlinx-atomicfu(锁)、androidx.collection(容器映射)在社区 Maven 上都没有可用的 ohos 变体。
渲染入口是 Composable上游正常渲染路径 rememberLottiePainter 需要 composition 和帧时钟,无头 sharedLib 里不可用。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在 KMP 目标配置、依赖替换和无头渲染桥接层。动画语义(解析、插值、图层、表达式、绘制)仍由公共 Kotlin 实现负责,ArkTS 只负责调用 Native、解码 PNG 和展示结果。

1.2 库提供的能力

真机 Demo 提供两个可观察场景(Tab):

  • 播放:11 个样本动画(矩形 / 虚线 / 多边形 / 重复器 / 渐变椭圆 / 弹球 / 遮罩 / 斜切描边 / 亮度蒙版 / 箭头 / 彩带)任选加载,「播放 / 暂停 / 重置」控制,速度 0.25–3x 滑条、渲染尺寸 160–480 滑条、进度滑条,实时 fps 计数(目标 20fps);
  • 单帧:任意进度拖动渲染 + 0% / 25% / 50% / 75% / 100% 五档快捷键,用于逐帧对拍 Android/iOS 渲染结果。

Native 侧返回的响应 JSON 包含:

字段作用
ok / error调用结果;Kotlin 异常永不跨语言边界,统一转 JSON 错误。
handle渲染会话句柄——解析一次、渲染多帧,切样本时先 Close 再 Open。
width / height / frameRate / durationFrames / durationMs动画元数据(由 compottie 解析模型直出),供 ArkTS 驱动播放时钟。
pngBase64指定进度的完整渲染帧 PNG(Skia 光栅化,真渲染管线)。

样本动画全部取自上游 example/shared/composeResources/files/,覆盖形状、渐变、遮罩(mask_add)、亮度蒙版(luma_matte)、预合成(view_arrow)、多图层粒子(confetti)等核心特性——同一份 JSON 在 Android 上由上游 compottie 渲染、在鸿蒙上由本适配渲染,画面应当同款。

1.3 实现适配

维度要求
代码复用7 个库模块(core / 主模块 / lite / dot / network / network-core / resources)全部为 ohosArm64/ohosX64 编译;上游 187 文件引擎零隔离、零算法改动。
平台目标仓库根 Gradle 即鸿蒙构建(沿用 cmp_qrose 目录规则),settings.gradle.kts 只 include 库模块、vendored keight 与 :example:nativeApp;上游 example:desktopApp/webApp/androidApp/shared 与 iosApp/ 留盘不参与构建。
桥接稳定三个 C ABI 符号 @CName("CompottieOpen"/"CompottieFrame"/"CompottieClose"),原始 Lottie JSON 直传 Open(不包壳,避免嵌套 JSON 解析),会话在 Native 侧以递归 pthread 锁串行化。
CMP 兼容走社区 Compose 1.12 鸿蒙分支(1.12.0-0.1.0-09)直接编译;rendererBackend=skia 保证无窗口 so 里的 CPU 光栅路径。
UI 完整双 Tab 覆盖播放 / 单帧两条链路,启动自动加载默认样本,每个参数至少一个可见控件与状态行回显。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。LottieAnimation Composable 仍由 Android/iOS/Desktop/Web 消费;鸿蒙 Demo 验证的是同一套动画引擎(含 Skia 渲染)在 OpenHarmony Native 链路里的正确性,ArkTS 通过 PNG 帧驱动画面。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 7 模块依赖闭包、平台源集与 OpenHarmony 交付物
第 2 阶段:工具链选线     ── -0.1.0 新线(Kotlin 2.4.20 + Compose 1.12 ohos 分支)、eazytec 置顶
第 3 阶段:依赖替换       ── vendored keight、CompottieLock 替 atomicfu、HashMap 替 collection
第 4 阶段:无头渲染入口   ── LottieFrameRenderer + expressionsEngine 公开 API
第 5 阶段:原生桥接       ── 三符号 C ABI、会话管理、C++ N-API、CMake 三件套
第 6 阶段:ArkUI 宿主     ── 双 Tab 播放/单帧工作台、HAP、签名与真机验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界

上游仓库根是完整的五平台工程:7 个库模块、example/(desktopApp / webApp / androidApp / shared / iosApp)、.github/ 发布流水线。鸿蒙适配不动其中任何一行:example 各模块与 iosApp/ 留在磁盘但不进 settings.gradle.kts,Gradle(Kotlin 侧)与 Hvigor(DevEco 侧)分开执行,example/harmonyApp 不是根项目的 Kotlin 子模块。

1.2 工具链矩阵
项值
KotlinHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven -0.1.0 线;官方 Kotlin 无 ohos 目标)
Compose1.12.0-0.1.0-09(社区分支,ohos 后端 rendererBackend=skia)
skiko0.150.1-0.1.0-06(由分支 compose 插件自动接线,不要手动声明依赖)
其余依赖serialization 1.9.1-1.1.0-02 / coroutines 1.10.2-1.1.0-02 / okio 3.17.0-0.1.0-07 / ktor 3.3.3-1.1.0-05 / datetime 0.7.1-1.0.0
Gradle9.7.1(沿用上游 wrapper)
JDK21+(本机 DevEco JBR 25)
DevEcocompatibleSdkVersion 6.0.1(21)(按本机已装 SDK 钉死)、hvigor modelVersion 5.1.0、abiFilters [arm64-v8a, x86_64]

settings.gradle.kts 里 eazytec 两个 Maven 仓(maven-public + harmonyos group)必须在 pluginManagement 与 dependencyResolutionManagement 里排最前,否则配置期直接 Unresolved reference: ohosArm64。

1.3 页面边界

页面只做三件事:选样本读 rawfile → 调 compottieOpen/compottieFrame/compottieClose → Base64 解码 PNG 出 PixelMap 展示,并用 durationMs 驱动播放时钟。所有解析、插值、绘制都在 Kotlin 侧,ArkTS 不重写任何动画逻辑。

第 2 阶段:工具链选线(技术难点)

2.1 两条工具链线的抉择

社区 Maven 上并存两条鸿蒙工具链线:

线KotlinCompose对 compottie 2.3.3 的代价
-1.x 线(qrose/ComposeCharts 所用)2.2.21-1.0.01.9.2上游按 Kotlin 2.4 + Compose 1.12 编写,全面降级源码工作量不可控
-0.1.0 线(本次所用)2.4.20-0.1.0-071.12.0-0.1.0-09与上游同代,零语言/API 降级;依赖闭包里三个缺口(见第 3 阶段)

-0.1.0 线的 compose 分支发布 foundation / ui / runtime / animation 的 ohosArm64+ohosX64 变体(kotlin-stdlib 元数据 2.3.20,可被 2.4.20 编译器消费),配套 compose-gradle-plugin 与 skiko overlay——这就是 qrose 适配文档里「等社区出现对齐的 compose.ui ohos 制品」等待的那个制品。

2.2 rendererBackend=skia 是硬前提

分支 compose 插件默认 rendererBackend=fusion-renderer:把 Skia 绘制路由到 OHOS fusion 图形服务。该服务面向 ComposeArkUIViewController 宿主场景,无窗口的 sharedLib 里没有对应连接,首次真实 draw 即 SIGSEGV(SEGV_ACCERR)——真机崩溃栈停在 CompottieFrame+156 之下的 Skia 调用。从 ui 制品的 META-INF/compose/ohos-render-backends.properties(supported=fusion-renderer,skia)确认合法取值后,gradle.properties 钉死 rendererBackend=skia:经典全量 Skia natives、CPU 光栅,构建日志确认 rendererBackend=skia, skikoOverlay=false。

2.3 目标裁剪

分支 compose 只发布 ohos/android/ios leaf(无 foundation-desktop),单一 Gradle 版本解析容不下 jvm 目标——库模块只保留 ohosArm64() + ohosX64();上游 android/ios/js/wasm 目标与 AGP、maven-publish 插件全部退出构建。

第 3 阶段:依赖替换(技术难点)

3.1 vendoring keight(表达式引擎)

keight-core(11 文件)+ keight(80 文件)是纯 Kotlin 的 ECMAScript 解释器(Lottie 表达式的底座),上游无 ohos 制品,社区只有过时的 0.0.04-0.1.0(仅 ohosArm64、API 落后两个版本)。做法:从 Maven Central 的 0.0.07 sources jar 原样引入 keight/、keight-core/ 两个子工程,加 jvm()+ohosArm64()+ohosX64();唯一源码改动是剥掉 ScriptEngine.kt 里 JVM 专属的 @Language IDE 注解;依赖按上游 POM 对齐(coroutines 分支版 + datetime 0.7.1-1.0.0 分支版)。Lottie 表达式能力一条不丢。

3.2 atomicfu → CompottieLock

kotlinx-atomicfu 在社区 Maven 上根本没有 ohos 制品。上游 5 个文件用它的 reentrantLock() + withLock(LruMap / MultiOwnerMutex / LottieCompositionResult / LottieStateMachine / DiskLruCache)。替换为 expect/actual 的 CompottieLock:JVM 侧 ReentrantLock,native 侧递归 pthread_mutex_t;withLock 做成 inline 扩展函数以保留上游大量非局部 return 的写法(这是最容易踩的一刀——普通成员函数会报 'return' is prohibited here)。

3.3 androidx.collection → HashMap 与源码集归位

BaseKeyframeAnimation 里唯一的 IntObjectMap 换成 HashMap<Int, Pair<K?, K?>>;jvm/js 目标裁掉后 jvmNativeMain/nonJvmMain 源集失联,其中 5 个平台 actual(IoDispatcher / PlatformImage / ThreadLocal / ZipFileSystem / FileSystem)移入 nativeMain,javaMain/jsMain/webMain/androidMain 留盘不参与编译。

3.4 一个隐蔽的 skiko 重复问题

分支 compose 插件会把 skiko(api + fusion-renderer overlay)自动接进主依赖图。任何模块手动再加全量 skiko 依赖都会撞 KLIB loader: same 'unique_name=org.jetbrains.skiko:skiko';同时每个 KMP 模块(哪怕没有 @Composable 代码)都必须 apply org.jetbrains.compose + kotlin.plugin.compose,否则下游会同时收到 api 与 full 两份 skiko。结论:全模块 apply compose 插件、全仓库不手写 skiko 依赖。

第 4 阶段:无头渲染入口(技术难点)

4.1 为什么必须新增公开 API

上游渲染入口 rememberLottiePainter 是 Composable(需要 composition、帧时钟、rememberTextMeasurer),而内部实现 LottiePainterImpl 是 internal——外部模块无法构造。照 cmp_qrose 加 Qrose.encodeQr 公开入口的先例,在 compottie-core 新增:

public class LottieFrameRenderer(
    public val composition: LottieComposition,
    /* 上游 rememberLottiePainter 的同名渲染开关 */
    expressionEngineFactory: ExpressionsEngineFactory? = null,
    enableExpressions: Boolean = expressionEngineFactory != null,
    ...
) {
    public fun render(progress: Float, width: Int, height: Int): ImageBitmap
}

render 逐行镜像 LottiePainterImpl.onDraw 的绘制路径:composition.progressToFrame(progress) → AnimationState.onFrame → CompositionLayer.draw,用公开的 CanvasDrawScope 画进 ImageBitmap;TextMeasurer 通过 skiko 的 createFontFamilyResolver() 无头构造(headlessTextMeasurer expect/actual);表达式开启时在 init 里镜像上游 prepareExpressions 预计算。

4.2 expressionsEngine() 公开工厂

上游把 keight 引擎工厂内联在 Composable 默认参数里。compottie 模块新增 expressionsEngine() 公开函数把它提出来——LottieFrameRenderer(enableExpressions = true, expressionEngineFactory = expressionsEngine()) 即可带表达式渲染,与 rememberLottiePainter 默认引擎完全同款。

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

5.1 三符号会话式桥接
ArkTS compottieOpen(json) / compottieFrame(req) / compottieClose(req)
  ← libcompottie_napi.so(CMake 现编,ArkTS 唯一入口)
  → C++ JsToUtf8 → CompottieOpen/Frame/Close (const char*)
  → LottieComposition.parse + LottieFrameRenderer + Skia PNG 编码
  → JSON {ok, handle, width, height, frameRate, durationFrames, durationMs, pngBase64}
  → C++ 侧拷贝为 JS string 后立即 libohoscompottie_symbols()->DisposeString(raw)

HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS 模块,所以必须有自己的 CMake NAPI 包装层。协议设计两个要点:原始 Lottie JSON 直传 CompottieOpen(不包壳——桥内 MiniJson 是扁平解析器,嵌套 JSON 字符串会破坏引号状态机);会话式(解析一次、handle 复用、多帧渲染),会话表由全局递归 pthread 锁串行化,AtomicInt 自增句柄。PNG 走 Skia 编码 + Base64(C ABI 以 NUL 结尾,裸二进制不安全)。

5.2 CMake 三件套

OHOS_ARCH(DevEco 注入)选 entry/libs/<abi>/;IMPORTED 引入 KN so + $ORIGIN rpath + POST_BUILD 把 libohoscompottie.so、libc++_shared.so 拷到 libcompottie_napi.so 旁边——每个 ABI 三份 so 缺一不可,少一件装机报 9568347。KN so 不存在时 CMake 直接 FATAL_ERROR 提示先跑 linkDebugShared。

第 6 阶段:ArkUI 宿主与交付

6.1 工程结构

DevEco Stage 工程(example/harmonyApp,打开的是它而不是仓库根):AppScope / entry 双 oh-package、signingConfigs: [] 不带证书、entry 模块 CMake 路径与双 abiFilters、ets/pages/Index.ets 双 Tab 工作台(启动自动加载默认样本——加载按钮必须放在样本列表上方,否则会被长列表挤出首屏)、cpp/types/libcompottie_napi 类型声明包(oh-package.json5 的 name 必须与 so 名一致)。

6.2 构建、签名和安装
# Kotlin 侧双 ABI(copyKnSo* 自动落位 entry/libs/<abi>/)
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# HAP(cli-tools hvigor 读 HOS_SDK_HOME;DevEco 内置 hvigor 读 DEVECO_SDK_HOME,两套别混用)
export HOS_SDK_HOME="$HOME/ohos/command-line-tools/sdk"
cd example/harmonyApp && hvigorw assembleHap -p product=default -p buildMode=debug

# 签名:DevEco Studio 打开 example/harmonyApp(不是仓库根目录!)
# File → Project Structure → Signing Configs 自动签名(证书不入库)
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.compottie

四、完整代码对照

4.1 整体架构

compottie-core / compottie / -lite / -dot / -network* / -resources
    │ 上游 7 模块零隔离编译(ohosArm64 + ohosX64,引擎算法零改动)
keight-core/ + keight/(vendored 0.0.07)
    │ 纯 Kotlin ECMAScript 解释器,仅剥一个 JVM 注解
    ▼
LottieFrameRenderer.kt + CompottieLock.kt + HeadlessTextMeasurer.kt(新增)
    │ 无头渲染公开入口:progress → ImageBitmap(CanvasDrawScope 镜像上游 draw 路径)
    ▼
Bridge.kt(ohosMain)
    │ @CName("CompottieOpen"/"CompottieFrame"/"CompottieClose")(三个 C ABI 符号)
    ▼
libcompottie_napi.so
    │ 字符串转换 + DisposeString
    ▼
Index.ets
    │ Base64 → PixelMap + durationMs 驱动播放时钟
    ▼
ArkUI 真机双 Tab 播放/单帧工作台

4.2 文件清单

文件职责
settings.gradle.ktseazytec 双仓置顶、include 库 + keight + nativeApp、上游 example 留盘
gradle.propertiesversion=2.3.3-0.1.0-01、native 三开关、rendererBackend=skia
gradle/libs.versions.toml-0.1.0 线全套(Kotlin/Compose/skiko/serialization/coroutines/okio/ktor/datetime)
各模块 build.gradle.ktsohosArm64()+ohosX64()、skiko 组归 native、全模块 apply compose 插件
keight-core/ keight/vendored 0.0.07(sources jar 原样引入)
compottie-core/.../CompottieLock.kt ×3expect/actual 可重入锁(inline withLock)
compottie-core/.../LottieFrameRenderer.kt无头渲染公开入口
compottie/.../ExpressionsEngine.ktexpressionsEngine() 公开工厂
nativeApp/.../Bridge.kt三符号 C ABI + 会话管理 + Skia PNG 编码
harmonyApp/.../napi_init.cppN-API 导出、字符串转换与释放
harmonyApp/.../CMakeLists.txtIMPORTED KN so + $ORIGIN rpath + 三件套拷贝
harmonyApp/.../Index.etsArkUI 双 Tab 工作台(自动加载 + fps 计数)
README.OpenHarmony.md / _CN.md适配说明(中文版 16 节主文档)

4.3 关键 API 对照

层次API作用
KMPLottieComposition.parse(json)上游公开解析入口(真实复用)
KMPLottieComposition.width/height/frameRate/durationFrames动画元数据直出 ArkTS 播放时钟
新增LottieFrameRenderer.render(progress)无头渲染任意进度帧为 ImageBitmap
新增expressionsEngine()keight 表达式引擎公开工厂
NativeCompottieOpen/Frame/Close会话生命周期 + 帧 PNG(JSON 协议)
N-APIcompottieOpen/compottieFrame/compottieClose向 ArkTS 暴露的三个函数
ArkTSrenderFrame / startPlay组装请求、解码 PixelMap、定时器驱动进度

4.4 ArkTS 与 Kotlin 的边界

const info = JSON.parse(compottieOpen(json));
// → { ok: true, handle: 1, width: 512, height: 512,
//     frameRate: 60, durationFrames: 180, durationMs: 3000 }

const frame = JSON.parse(compottieFrame(JSON.stringify({
  handle: 1, progress: 0.42, width: 320, height: 320,
})));
// → { ok: true, width: 320, height: 320, pngBase64: '...' }

const bytes = new util.Base64Helper().decodeSync(frame.pngBase64);
const source = image.createImageSource(bytes.buffer);
const pm = await source.createPixelMap();  // Image(pm) 直接展示
// Kotlin 侧:解析、插值、图层、表达式、绘制全部由上游引擎完成
val composition = LottieComposition.parse(json)
val renderer = LottieFrameRenderer(
    composition = composition,
    enableExpressions = true,
    expressionEngineFactory = expressionsEngine(),
)
val frame = renderer.render(progress = 0.42f)  // → Skia 光栅化 ImageBitmap

两者之间只传输 JSON 与 PNG,不传输 Kotlin 对象;ArkTS 用 durationMs 步进 progress,其余一概不参与。

五、关键决策说明

决策 1:上 -0.1.0 新工具链线,而不是降级到 1.9.2 线

-1.x 线(Kotlin 2.2.21 + Compose 1.9.2)是 qrose/ComposeCharts 的过审路线,但 compottie 2.3.3 按 Kotlin 2.4 + Compose 1.12 编写,降级等于重写一遍上游。-0.1.0 线与上游同代,适配工作量从「全面源码降级」变为「三个依赖小刀替换」——这是本次适配成立的根本前提。

决策 2:库本体直接编译,而不是 composeMain 隔离

qrose 当年隔离 Compose 是因为社区没有 ohos 的 compose.ui;现在有了。7 个模块 187 个文件零隔离编进鸿蒙,渲染走的是上游真实 draw 管线(CompositionLayer → DrawScope → Skia),而不是软件光栅化近似——「代码复用」从「公式镜像」升级为「整条引擎」。代价是依赖闭包更重(skiko + compose 全家桶进 so),debug 产物 84MB(HAP 打包剥离后单 ABI 约 32MB)。

决策 3:vendoring keight,而不是砍掉表达式

表达式是 compottie 的差异化能力,砍掉等于降级产品。keight 0.0.07 是 80 个纯 Kotlin 文件、零 cinterop,vendoring 成本接近零;社区旧 fork(0.0.04、仅 arm64)API 落后两个版本不能用。vendored 源码与上游逐字节一致(除一个注解),以后社区出统一 fork 时一行 build 配置换回去。

决策 4:新增无头渲染公开入口,而不是 friend-path 黑魔法

LottiePainterImpl 是 internal,理论上可用 -Xfriend-path 越权访问,但那依赖编译器内部行为且随版本漂移。照 qrose 加 Qrose.encodeQr 的先例新增 LottieFrameRenderer + expressionsEngine() 两个公开 API,镜像上游 draw 路径,行为可审计、可在上游文档里对拍。

决策 5:rendererBackend=skia,而不是修 fusion-renderer 崩溃

真机首帧 SIGSEGV 的根因是 fusion-renderer 后端面向 ArkUI 宿主、无窗口 so 里没有图形服务连接。修不动(在 skiko overlay 内部),但插件本身提供 skia 后端(经典全量 Skia、CPU 光栅)——ohos-render-backends.properties 里 supported=fusion-renderer,skia 白纸黑字。一行配置换回一条完全满足无头渲染的路径,真机复测零崩溃、fps 20 稳定。

六、测试与验证

6.1 测试环境

本次验证使用 macOS(arm64)、DevEco JBR 25、HarmonyOS Kotlin 2.4.20-0.1.0-07、Compose 1.12.0-0.1.0-09、Gradle 9.7.1、命令行工具链 HarmonyOS SDK 6.0.1(API 21)、DevEco Studio 5.1.0 Release 与鸿蒙真机 HUAWEI Mate 60 Pro(arm64-v8a,签名安装)。

6.2 编译与链接验证

./gradlew :keight-core:compileKotlinOhosArm64 :keight:compileKotlinOhosArm64 \
  :compottie-core:compileKotlinOhosArm64 :compottie:compileKotlinOhosArm64 \
  :compottie-lite:compileKotlinOhosArm64 :compottie-dot:compileKotlinOhosArm64 \
  :compottie-network-core:compileKotlinOhosArm64 :compottie-network:compileKotlinOhosArm64 \
  :compottie-resources:compileKotlinOhosArm64 \
  :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

7 个库模块 + 2 个 vendored 模块双 ohos 目标全绿;llvm-readelf --dyn-syms 确认 CompottieOpen / CompottieFrame / CompottieClose 均为 FUNC GLOBAL DEFAULT;libcompottie_napi.so 的 NEEDED 只有短名(libace_napi.z.so / libhilog_ndk.z.so / libohoscompottie.so / libc++_shared.so),无本机绝对路径。

6.3 HAP 验证

CompileArkTS: finished
PackageHap: finished(debug,双 ABI)
BUILD SUCCESSFUL

命令行构建产出 entry-default-unsigned.hap ≈ 62MB(debug 未 strip 的 KN so 含 Skia/Compose 全量符号,打包器剥离后 arm64 约 32MB + x86_64 约 29MB),包内双 ABI 三件套齐全;DevEco 自动签名产出 entry-default-signed.hap 真机安装成功。

6.4 功能用例(真机实测)

  1. 启动即自动加载 rect.json,状态行 加载成功 rect.json,信息行 尺寸 512x512 · 帧率 60 · 总帧数 180 · 时长 3.00s——元数据由 compottie 解析模型直出;
  2. 「播放」Tab 点播放:fps 计数稳定在 20(实测 91 次帧请求、50ms 间隔精确命中),进度滑条随动画推进,暂停/重置可用;速度 2.3x 时动画明显加速;
  3. 「单帧」Tab 拖动进度滑条逐帧渲染,0%/25%/50%/75%/100% 快捷键逐档出图;
  4. 样本切换 mask_add:compottieClose(旧会话)→ compottieOpen(2226 字节)→ 首帧,会话生命周期干净;画面为浅蓝三角形 + 深蓝遮罩块的双层合成,遮罩(SaveLayer + BlendMode)路径正确;
  5. 全程 hilog 无 加载失败 / 渲染失败 / 崩溃信号;切换 rendererBackend=skia 前的同包首帧渲染稳定复现 SIGSEGV(对照实验确认根因)。

6.5 验证结论

Native 动态库(双 ABI + libc++)、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了 compottie 真实引擎渲染的 Lottie 动画帧(矩形、遮罩样本目检通过),播放链路 fps 20 稳定,动画元数据与 JSON 内容一致。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compottie.git
cd compottie
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
  :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp,自动签名后:
export HOS_SDK_HOME="$HOME/ohos/command-line-tools/sdk"
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.compottie

八、遗留问题与改进方向

8.1 踩坑复盘

  1. fusion-renderer 首帧 SIGSEGV:无窗口 sharedLib 里默认渲染后端会走到图形服务的坏连接,gradle.properties 必须钉 rendererBackend=skia——这是全链路唯一一个崩溃级坑,且只在真机首次真实 draw 时暴露(编译、链接、parse 全都不报错);
  2. skiko unique_name 重复:分支 compose 插件自动接线 skiko 后,手动加全量 skiko 依赖、或某模块漏 apply compose 插件,都会让下游 KLIB 冲突;
  3. 无 desktop leaf:分支 compose 不发布 foundation-desktop,库模块带 jvm 目标必炸——JVM 交叉验证只能在上游仓库做;
  4. atomicfu 的 withLock 是 inline:替换锁实现时必须保持 inline 扩展函数形态,否则上游非局部 return 全部编译失败;
  5. explicitApi 严格模式对 internal 声明也要求显式类型;
  6. hvigor 双环境变量:cli-tools 版读 HOS_SDK_HOME、DevEco 内置版读 DEVECO_SDK_HOME,混用报 SDK component missing;compatibleSdkVersion 必须与本机已装 SDK 精确匹配(本机 6.0.1(21));
  7. API 21 ArkTS 严格模式:Base64Helper.decodeSync 是实例方法、JSON.parse 需 as Record<string, Object> 显式转换、throw 不接受任意类型;
  8. Demo UX:加载按钮放在样本列表下方会被挤出首屏,用户「看不到入口」会误判为渲染失败——改为启动自动加载 + 按钮上移。

8.2 已知问题

  • 无 jvm 目标:分支 compose 无 desktop leaf,同一 Gradle 解析容不下 stock JVM compose;JVM 侧对拍请在上游仓库跑同一样本;
  • Demo 是 PNG 帧驱动(约 20fps):验证的是真渲染管线,不是 60fps 产品形态;
  • 文字图层经无头 TextMeasurer + 系统字体 resolver,未做逐字验收(样本刻意不含文字);
  • 动画内嵌图片/字体资产未接(LottieAssetsManager 未传入 Demo);compottie-dot / compottie-network 编译通过但未上机;
  • debug so 含 Skia/Compose 全量符号体积大(84MB),HAP 打包剥离后单 ABI 约 32MB,生产建议 linkReleaseShared + strip。

8.3 未来优化方向

  • 接入 ComposeArkUIViewController 宿主方案,让 LottieAnimation 在 ArkUI 页面直接跑,PNG 桥降级为 fallback——工具链已就绪;
  • 透出 LottieAssetsManager(内嵌图片/字体)与 dotLottie 演示;
  • 文字图层逐字验收 + 字体 resolver 压力测试;
  • vendored keight 换社区统一 fork(待其升到 0.0.07 API);
  • 在 CI 中加入 Native 链接、ELF 符号检查、HAP 构建与 rendererBackend 配置守卫;
  • release so strip + PNG 帧率自适应(按 durationMs 与设备性能)。

九、总结

9.1 核心难点回顾

compottie 2.3.3(7 模块,Kotlin 2.4 + Compose 1.12 源码)
    → -0.1.0 工具链线(Kotlin 2.4.20-0.1.0-07 + Compose 1.12.0-0.1.0-09 ohos 分支)
    → 三依赖小刀替换(vendored keight / CompottieLock / HashMap)
    → LottieFrameRenderer 无头渲染入口(镜像上游 draw 路径)
    → C ABI(CompottieOpen/Frame/Close 会话式三符号)
    → C++ N-API(libcompottie_napi.so)
    → ArkTS Base64 → PixelMap + durationMs 播放时钟
    → ArkUI 真机双 Tab 工作台(fps 20 稳定)

9.2 封装层次

  • KMP 层:上游 7 模块零隔离编译,解析/插值/图层/表达式/绘制算法零改动;
  • 新增层:LottieFrameRenderer + expressionsEngine() 把 Composable 渲染能力翻译成无头 API;
  • Native 层:双 ABI 动态库以三个 C ABI 符号输出会话与 PNG 帧;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理双 Tab 状态、播放时钟、PixelMap 生命周期(旧帧 release);
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 工具链线选对,适配工作量降一个数量级:qrose 时代的「隔离 Compose + 软件光栅化」是 1.9.2 线下的无奈之举;-0.1.0 线出现后,「库本体直接编译」成为可能——选线之前先扫一遍社区 Maven 的 -0.1.0 后缀制品,别急着动源码;
  2. 崩溃要追到后端配置层:SIGSEGV 指向 Skia 调用栈时,先查渲染后端(fusion-renderer vs skia)而不是怀疑自己的绘制代码;制品内嵌的 ohos-render-backends.properties 是权威配置来源;
  3. 会话式桥接配元数据直出:Open 一次解析多帧渲染 + durationMs 元数据驱动 ArkTS 时钟,让「动画语义」全部留在 Kotlin 侧,ArkTS 只做宿主——PNG 帧序列足以验收渲染正确性,接入方式以后可以换,C 签名永不变。

9.4 适配成果

当前适配已完成:

  • 根构建改造的 OpenHarmony Gradle 工程(-0.1.0 工具链线、上游 example 留盘不进构建);
  • 7 个库模块 + 2 个 vendored keight 模块的双 ABI Kotlin/Native 编译(libohoscompottie.so arm64/x64 + 匹配 libc++);
  • 无头渲染公开 API(LottieFrameRenderer / expressionsEngine())与三符号会话式 C ABI + C++ N-API 包装(libcompottie_napi.so);
  • rendererBackend=skia 关键修复(真机 SIGSEGV 根因消除,有切换前后对照实验);
  • DevEco Stage ArkUI 双 Tab 验收页面(11 个上游样本、自动加载、fps 计数);
  • HAP 构建(unsigned ≈ 62MB debug 双 ABI)、签名、Mate 60 Pro 真机安装与运行验证(fps 20、遮罩样本目检通过);
  • AtomGit 项目文档(README.OpenHarmony.md / README.OpenHarmony_CN.md)与真机效果图。

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐