本文记录 multiplatform-compose 接入 OpenHarmony 的完整过程,覆盖上游定位盘点、社区 Compose-for-OHOS 制品线选型、2022→2026 工具链断代下的 Demo 源码移植(Material 2 → Material 3)、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、CMake / N-API 桥接、ArkTS 宿主、HAP 构建和真机验收。

multiplatform-compose(cl3m/multiplatform-compose,compose-jb 分支)是 2021 年的 Compose Multiplatform 演示 App:同一份 Compose UI 跑在 Android 与 iOS 上——Scaffold + 抽屉导航 + 10 个 Material 演示页(按钮、文本样式、LazyColumn、布局、计数器、平台问候、图片、输入框、底部导航、长文滚动),并用 PreCompose 做导航、Ktor 拉网络图片。本次适配补上第三端:让这同一份 Compose UI 直接跑在 OpenHarmony 上。与社区早期路线(qrose / haze 的"零依赖内核提取"、KStore 的"纯 Kotlin 库真编译")有本质区别:这个项目没有可提取的非 UI 内核——全部 26 个 Kotlin 文件里 100% 是 Compose UI,因此走路线 C"全 Compose UI"(lazytable / Reorderable / Haze 同款):社区 Compose-for-OHOS 分支 1.9.2-1.1.0-13(skia 自绘后端)把真实的 Compose 场景树渲染进 ArkUI 宿主,界面上看到的一切都来自 Kotlin Compose 代码,不是 ArkTS 重写。

项目地址: AtomGit/oh-tpc/multiplatform-compose.git

上游仓库: cl3m/multiplatform-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

multiplatform-compose 的价值不在某个算法,而在"一份 Compose UI,多端运行"这个演示语义本身:上游用 JetBrains Compose Multiplatform 在 Android 与 iOS 上渲染同一份 commonMain UI,证明跨端 UI 可以不写两遍。OpenHarmony 应用同样需要这个证明——鸿蒙侧能不能不重写、直接复用这份 Compose 场景?但上游钉死在 2022 年的工具链(Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1 / jcenter 时代),与鸿蒙 Kotlin 2.2.21 线差了三个大版本,无法原样混编。

如果只把演示页面改成 ArkTS,页面可以显示按钮,却无法证明 Compose 编译器、场景树、skia 渲染管线在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:Compose 源码在 ohos 目标上的编译、社区 CMP 分支的 klib 解析、Native 动态库导出、ArkUI 宿主装载、生命周期桥接和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。
工具链断代上游 Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1 与鸿蒙工具链无法共存于一个 Gradle 构建;buildSrc/ 会被 Gradle 无条件自动构建,直接污染鸿蒙构建。
依赖矩阵Material 2、moe.tlaster:precompose(导航)、io.ktor(网络图)、material-icons-core 四件套全部没有 ohos 制品。
渲染边界Compose 场景树不能直接进 ArkUI,需要社区 CMP 分支 + skia 自绘 + ArkUIViewController 桥。
资源管线imageResource(id) 的资源管线在 ohos 线缺失,本地位图要走 cinterop / Canvas 替代。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:Demo 源码的 ohos 移植(升 API 不改语义)、根构建替换(cmp_qrose 模式)、libkn.so 的导出与 ArkTS 宿主桥接。UI 语义完全由 Compose 负责,ArkTS 只负责装载 controller、转发生命周期。

1.2 库提供的能力

Demo 是一个真实的 Compose Multiplatform 应用会话,抽屉导航 + 10 个演示页全部由 Compose 渲染:

Demo验收点
HelloPlatform居中显示 Hello, OpenHarmony (Kotlin/Native)!(证明 Platform actual 生效)
Lorem长文可滚动(verticalScroll),滚动流畅无花屏
LazyColumn列表懒加载 25 行 + 分隔线,快速滑动不丢帧
Counter+ / - 按钮改数字,状态在离开 / 返回页面后保留(CompositionLocal)
Layout彩色 Box / Column / Row / Spacer 按上游配色排布(绿 / 品红 / 黄 / 红 / 黑 / 蓝)
TextStyles蓝 / 斜体 / 加粗 / 25sp / 手写体 / 等宽 / 衬线 / 30sp / 红各一行,随暗色模式变色
Images上:200×300 橙色"笑脸"位图(Canvas 绘制);下:黑色占位框说明网络图待 Ktor ohos 引擎
Buttons6 种按钮:Simple / 带边框 / 圆角 / Outlined / Text / Big,全部可点
TextFields输入框可弹软键盘、实时回显 Text is ...、label / placeholder 正常
BottomNavigation底部三 Tab(⌂ Tab1/2/3)切换 HelloPlatform / Layout / Counter,选中态高亮

壳层提供 Scaffold + 抽屉导航(左上 ☰ 呼出)、橙色 Material 主题(亮 / 暗双 scheme)、暗色模式与安全区的 CompositionLocal 语义保留。这十个页面就是"Compose 能不能在鸿蒙上真跑"的完整验收面。

1.3 实现适配

维度要求
代码复用上游 :test 共享库的 16 个源文件移植进 example/composeApp,升级到 Compose 1.9 / Material 3 API,逻辑零改动;仅 import 与废弃 API 替换。
平台目标example/composeApp 声明 ohosArm64 + ohosX64,binaries.sharedLib 产物 libkn.so + libkn_api.h,export 导出 compose-export 符号。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 androidApp/、iosApp/、test/ 留盘不进 settings.gradle.kts;buildSrc/ 删除(版本目录由 toml 接管)。
渲染稳定Compose 场景经社区 CMP 分支 + skiko-ohos 自绘进 ArkUI;controller 接管 onPageShow / onPageHide / onBackPressed 生命周期。
依赖纪律无 ohos 制品的依赖(M2 / precompose / ktor / icons)全部降级为等价 M3 或占位实现,不引入伪造网络栈。
UI 完整抽屉 + 10 个演示页全量保留,橙色主题与上游截图语义一致。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkTS 壳不是 UI 实现。除宿主壳(EntryAbility / Index.ets,社区模板)外,界面上看到的一切都来自 Kotlin Compose 代码;ArkTS 侧一行业务 UI 都没有。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、查重定位(无可提取内核)、确定交付物 = HAP + 双 ABI libkn.so
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 钉版
第 3 阶段:源码移植       ── 16 个 commonMain 文件升级到 1.9/M3 API,迁入 example/composeApp;M2→M3、导航状态化、Ktor 降级
第 4 阶段:ohos 目标打通  ── ohosArm64+ohosX64 sharedLib + export 导出 + resource cinterop + publish Copy 任务
第 5 阶段:ArkTS 宿主     ── hvigor 工程骨架 + compose.har + CMake NAPI 胶水 + Compose({controller, libraryName})
第 6 阶段:验证交付       ── 双 ABI link 通过 → so 落位 → DevEco 打 HAP → 自动签名 → 真机验收(10 个 Demo 逐项过)

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
multiplatform-compose/                rootProject.name = ohos_multiplatform_compose
├── settings.gradle.kts               eazytec 双仓置前;只 include :example:composeApp
├── build.gradle.kts                  根构建:三插件 apply false + group/version + 全局禁用 ohos 测试任务
├── gradle.properties                 JDK21 / version=0.0.1-ohos.1 / kotlin.native.* / rendererBackend=skia
├── gradle/libs.versions.toml         kotlin=2.2.21-1.1.0-05,compose=1.9.2-1.1.0-13,三插件别名
├── example/
│   ├── composeApp/                   ★ 鸿蒙 Demo(本 fork 的主体)
│   │   ├── build.gradle.kts          sharedLib baseName="kn" + export + resource cinterop + publish 任务
│   │   └── src/
│   │       ├── commonMain/kotlin/com/rouge41/kmm/compose/
│   │       │   ├── Platform.kt / Resources.kt           expect(沿上游包名)
│   │       │   └── test/                                App/Content/Theme + 10 个 Demo(M3 化)
│   │       └── ohosMain/
│   │           ├── kotlin/.../Platform.kt / Resources.kt          actual 实现
│   │           ├── kotlin/.../test/MainArkUIViewController.kt     @CName 场景入口
│   │           └── cinterop/                                      OH_ResourceManager 绑定(rawfile)
│   └── harmonyApp/                   ★ DevEco hvigor HAP 工程
│       ├── AppScope/app.json5        bundle=org.terminator.ohos.multiplatformcompose, 0.0.1-ohos.1
│       ├── build-profile.json5       compatibleSdkVersion 5.0.5(17),signingConfigs 留空
│       └── entry/
│           ├── libs/compose.har      社区 @cpf-kmp-cmp/compose 1.9.2-0.3.0(进 git)
│           ├── oh-package.json5      依赖 compose.har + libentry.so 类型
│           └── src/main/
│               ├── cpp/CMakeLists.txt    IMPORTED libkn.so + find_package(compose) + skikobridge
│               ├── cpp/napi_init.cpp     androidx_compose_ui_arkui_init + MainArkUIViewController 注册
│               ├── ets/entryability/     标准 UIAbility
│               ├── ets/pages/Index.ets   Compose({ controller, libraryName: 'entry' })
│               └── resources/            图标 / 字符串 / main_pages
├── androidApp/  iosApp/  test/       上游遗留(Android/iOS 壳与共享库),留盘不进构建
├── README.md / README.upstream.md    鸿蒙中文首页 / 上游原文备份
└── README.OpenHarmony.md / _CN.md    英文摘要 / 鸿蒙适配主文档(19 节)

职责边界一句话:Gradle 负责 Kotlin → libkn.so,hvigor 负责 so + ArkTS → HAP;两者唯一的连接点是 publishDebugBinariesToHarmonyApp 这个 Copy 任务。

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:composeApp,rootProject.name = "ohos_multiplatform_compose"。上游的 buildSrc/(老版本目录)整个删除——Gradle 会对 buildSrc 无条件自动构建,其内容只服务 2022 年的 Android 构建,留着必然污染鸿蒙构建;版本管理由 gradle/libs.versions.toml 接管。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.1.0-05(eazytec Maven)
Compose Multiplatform1.9.2-1.1.0-13(社区 OH 分支,skia 自绘后端)
skiko0.9.22.2-1.1.0-13(ohos 线,rendererBackend=skia)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(sharedLib,baseName=kn)
Gradle8.14.1(wrapper 升级;2.2.21 线经验值)
DevEco product5.0.5(17) 兼容,modelVersion 6.0.0
DevEco Studio6.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.multiplatformcompose
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version

gradlew 启动器需要自己的 JVM,org.gradle.java.home 管不到它——不设 JAVA_HOME 直接报 JAVA_HOME is not set,这是本工程最高频的踩坑点。

1.3 源码依赖度盘点
上游模块文件形态依赖处置
test/(共享库)26 个 Kotlin 文件,其中 Compose UI 约 100%,expect 仅 3 处(Platform / Resources / Extensions)Compose 1.2.1 M2 + precompose + ktor + icons16 个文件移植进 example/composeApp(M3 化);Extensions.kt(ByteArray.toImageBitmap)未移植——唯一消费者是 Ktor 网络图
androidApp/Android 壳AGP 7.3.1留盘不进 settings
iosApp/iOS 壳 + PodsXcode / cocoapods留盘不进 settings
buildSrc/老式版本目录服务旧 Android 构建删除(toml 接管)

expect 盘点结论:上游 expect 只有 Platform(平台名)、Resources(本地位图)、Extensions(Ktor 字节流转位图)三处平台胶水,其余 100% 是 Compose UI——没有可提取的非 UI 内核,这直接决定路线选择(见第 3.1 节与决策 1)。

1.4 页面边界
顶部栏         M3 Scaffold,TopAppBar + 左上 ☰(抽屉开关,文本字形)
抽屉           M3 ModalNavigationDrawer + NavigationDrawerItem × 10(Demo 列表)
主题           橙色主色 M3 lightColorScheme / darkColorScheme(上游 M2 配色迁移)
内容区         10 个 Demo 页按选中项切换(状态导航)
生命周期       onPageShow / onPageHide / onBackPressed 由 ArkTS 转发 controller

页面启动时 ArkTS 先 nativeApi.MainArkUIViewController() 拿到场景控制器,再交给 Compose 组件渲染——首帧出现前没有任何 ArkTS 业务 UI。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 Compose 的 ohos 制品。用 .module 元数据逐个验证:

org.jetbrains.compose:compose (Gradle 插件): 1.9.2-1.1.0-13
  runtime / foundation / material3 / ui / components-resources: ohosArm64/ohosX64 klib 齐全
org.jetbrains.compose.export:export: 1.9.2-1.1.0-13
  导出 ComposeArkUIViewController 等 ArkUI 桥符号(KN sharedLib 导出用)
skiko-ohos: 0.9.22.2-1.1.0-13
  skia 自绘后端(rendererBackend=skia)

五个关键结论:其一,社区 CMP 分支在 1.9.2-1.1.0-13 线上 klib 齐全,"全 Compose UI"可行;其二,Material 3 有 ohos klib、Material 2 没有——Demo 必须从 M2 移植到 M3;其三,moe.tlaster:precompose 无 ohos 变体(中央仓元数据只是镜像),导航要换实现;其四,io.ktor 无 ohos 引擎,网络图降级;其五,material-icons-core 无 ohos klib,两个图标换文本字形。制品线一确认,"不重写 UI"的唯一路线就是社区 CMP 分支。

2.2 工具链钉版(为什么是 2.2.21-1.1.0-05 + 8.14.1)

社区在 Compose-for-OHOS 1.9.2-1.1.0-13 线上配套的 Kotlin 是 2.2.21 定制线;Gradle 用 8.14.1(2.2.21 线经验值,KStore 那条 2.4.20 线才允许 Gradle 9.x,两条线的经验不能互相套用)。settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-releases/")
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-releases/")
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_multiplatform_compose"

// 上游模块(androidApp、test、iosApp)留盘但不 include:
// 它们钉死在 2022 工具链(Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1),不参与鸿蒙构建。
include(":example:composeApp")

根构建收敛为极简 ohos 构建:

plugins {
    alias(libs.plugins.kotlin.multiplatform) apply false
    alias(libs.plugins.jetbrains.compose) apply false
    alias(libs.plugins.compose.compiler) apply false
}

allprojects {
    group = "com.rouge41.kmm.compose"
    version = providers.gradleProperty("version").orElse("0.0.1-ohos.1").get()
}

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

gradle.properties 钉住 KN 与渲染后端属性:kotlin.native.ignoreDisabledTargets=true、kotlin.native.cacheKind=none、rendererBackend=skia、ohosSkikoVersion=0.9.22.2-1.1.0-13、version=0.0.1-ohos.1。

2.3 编译 ohos 目标

example/composeApp/build.gradle.kts 是本 fork 的技术主体:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.jetbrains.compose)
    alias(libs.plugins.compose.compiler)
}

kotlin {
    jvmToolchain(21)

    listOf(ohosArm64(), ohosX64()).forEach { ohosTarget ->
        ohosTarget.binaries.sharedLib {
            baseName = "kn"
            export(libs.compose.multiplatform.export)   // 导出 ArkUI 桥符号
            linkerOpts("-lz")
        }
        ohosTarget.compilations.getByName("main").apply {
            cinterops.create("resource") {              // OH_ResourceManager(rawfile)
                defFile(file("src/ohosMain/cinterop/resource.def"))
                includeDirs(file("src/ohosMain/cinterop/include"))
            }
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
        }
        val ohosMain by creating {
            dependsOn(commonMain.get())
            dependencies { api(libs.compose.multiplatform.export) }
        }
        val ohosArm64Main by getting { dependsOn(ohosMain) }
        val ohosX64Main by getting { dependsOn(ohosMain) }
    }
}

export(...) 是关键:KN sharedLib 默认不导出依赖里的符号,不 export 的话 libkn.so 里找不到 ComposeArkUIViewController,宿主侧 dlsym 直接失败。实测(本机 4 分 16 秒):

:example:composeApp:linkDebugSharedOhosArm64   BUILD SUCCESSFUL
:example:composeApp:linkDebugSharedOhosX64     BUILD SUCCESSFUL
:example:composeApp:publishDebugBinariesToHarmonyApp  BUILD SUCCESSFUL

debugShared 未 strip 时 libkn.so arm64 约 83 MB、x86_64 约 80 MB(含 Compose 运行时与 skiko,HAP 打包时 release strip 后显著缩小)。

2.4 产物落位(Gradle 与 hvigor 的唯一连接点)
// 为 debug/release 两档各注册一个 Copy 任务,把 KN 产物发布进 HAP 工程
arrayOf("debug", "release").forEach { type ->
    val capitalized = type.replaceFirstChar { it.uppercase() }
    tasks.register<Copy>("publish${capitalized}BinariesToHarmonyApp") {
        group = "harmony"
        dependsOn("link${capitalized}SharedOhosArm64", "link${capitalized}SharedOhosX64")
        duplicatesStrategy = DuplicatesStrategy.INCLUDE
        into(rootProject.file("example/harmonyApp"))
        from("build/bin/ohosArm64/${type}Shared/libkn_api.h") { into("entry/src/main/cpp/include/arm64-v8a/") }
        from("build/bin/ohosArm64/${type}Shared/libkn.so")    { into("entry/libs/arm64-v8a/") }
        from("build/bin/ohosX64/${type}Shared/libkn_api.h")   { into("entry/src/main/cpp/include/x86_64/") }
        from("build/bin/ohosX64/${type}Shared/libkn.so")      { into("entry/libs/x86_64/") }
    }
}

libkn.so 与 libkn_api.h 必须来自同一次 link(ABI 目录一一对应),CMake 按 ABI 选头文件——so 是产物不进 git,每次构建可复现。

第 3 阶段:Demo 源码移植(技术难点)

3.1 路线选择:为什么是"全 Compose UI"

社区已过审的适配有三条主路线:

路线 A:内核提取(qrose / Cloudy / Sketchy)路线 B:纯 Kotlin 库真编译(KStore)路线 C:全 Compose UI(lazytable / Reorderable / Haze,本 fork)
适用对象库里有纯 Kotlin 内核(编码器、求解器)库本身零 UI、依赖 kotlinx 制品线库 / 应用的价值就是 Compose UI 本身
做法Compose 源码停放不编译,内核编成 so,ArkTS / Canvas 重绘commonMain 原样编译成 ohos KLIB,ArkTS 只做验收宿主Compose 源码直接为 ohos 目标编译,社区 CMP 分支 + skia 在设备上渲染真实场景
UI 保真视觉近似重实现无 UI 面同一份代码同一份渲染

本项目 26 个 Kotlin 文件里只有 3 个 expect/actual 算平台胶水,其余 100% 是 Compose UI——走路线 A 等于把整个 App 用 ArkTS 重写,既丢掉"一份 Compose UI 跑多端"的演示语义,也放大改动面;制品盘点(第 2.1 节)确认 CMP 1.9.2-1.1.0-13 ohos 线齐全后,路线 C 是唯一不需要重写 UI 的做法。

3.2 Material 2 → Material 3 逐文件 diff

上游 Demo 写在 Compose 1.2 / M2 上,M2 无 ohos klib、M3 有,按 M3 API 移植(语义不变):

文件改动
App.ktPreCompose NavHost → 状态导航;M2 Scaffold / ModalDrawer → M3 ModalNavigationDrawer / Scaffold;Icons.Default.Menu → 文本字形 ☰
Theme.ktM2 lightColors / darkColors → M3 lightColorScheme / darkColorScheme(橙色主色保留)
Content.kt去掉 Ktor import(本来没用到);暗色 / 安全区 CompositionLocal 原样保留
Buttons.ktM2 → M3 API(ButtonDefaults.buttonElevation 等),注释保留
BottomNavigationDemo.ktM2 BottomNavigation / Item → M3 NavigationBar / Item;NavHost → 状态切换;Icons.Default.Home → ⌂
LazyColumnDemo.ktDivider → HorizontalDivider
TextFields.ktimport 换 M3,API 兼容
Images.kt去掉 Ktor 下载;AsyncImage 变占位说明;本地图走 Canvas 位图
HelloPlatform / Layout / Lorem / TextStyles / Counter.kt仅 import 从 M2 换 M3,逻辑零改动
Platform.kt(expect)原样;ohos actual 返回 OpenHarmony (Kotlin/Native)
Resources.kt(expect)原样;ohos actual 用 Canvas 画 200×300 位图
Extensions.kt(ByteArray.toImageBitmap)未移植(唯一消费者是 Ktor 网络图,ohos 无引擎)
3.3 无制品依赖的降级策略
// 上游:PreCompose NavHost + rememberNavigator() 跨页导航
// 本 fork:mutableStateOf<Demo> 状态切换,抽屉交互语义保留
var currentDemo by remember { mutableStateOf<Demo>(Demo.HelloPlatform) }

ModalNavigationDrawer(drawerContent = { /* 10 个 NavigationDrawerItem */ }) {
    Scaffold(topBar = { TopAppBar(title = { Text(currentDemo.name) },
                       navigationIcon = { TextButton(onClick = { drawerOpen = true }) { Text("☰") } }) }) {
        when (currentDemo) { Demo.HelloPlatform -> HelloPlatform() /* ... */ }
    }
}

降级三原则:导航语义保留(抽屉开合、页面切换一个不少)、不引入伪造依赖(网络图显示占位说明而不是假图)、不为一两个图标 vendor 整库(icons 换字形)。Images 页本地位图用 Canvas 现画(ImageBitmap 语义保留),资源管线就绪后可换 rawfile 真图。

3.4 ohos actual 与场景入口
// example/composeApp/src/ohosMain/.../Platform.kt
actual val platform: Platform = object : Platform {
    override val name: String get() = "OpenHarmony (Kotlin/Native)"
}

// example/composeApp/src/ohosMain/.../test/MainArkUIViewController.kt
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: CPointer<OpaquePointer>?): CPointer<OpaquePointer>? {
    initMainHandler(env)                              // KN 协程主线程挂到 ArkTS 线程
    return ComposeArkUIViewController(env) { Content() }   // compose-export 提供
}

Content() 是上游共享库的入口组合函数(Theme → App),鸿蒙侧原样复用——这就是"同一份 Compose UI"的直接证据。resource cinterop 提供 OH_ResourceManager 绑定(raw_file.h / raw_file_manager.h),为 compose 资源管线预留 rawfile 入口。

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

4.1 渲染链路(一行调用栈)
ArkTS Index.ets
  └─ nativeApi.MainArkUIViewController()            // libentry.so (CMake NAPI 胶水)
       └─ @CName("MainArkUIViewController")          // libkn.so (Kotlin/Native 导出)
            ├─ initMainHandler(env)                  // KN 协程主线程挂到 ArkTS 线程
            └─ ComposeArkUIViewController(env) { Content() }   // compose-export 提供
                 └─ Compose 场景树(App → Theme → 10 个 Demo)
  └─ Compose({ controller, libraryName: 'entry' })   // @cpf-kmp-cmp/compose 的 ArkUI 组件
       └─ skia 自绘(skiko-ohos 0.9.22.2-1.1.0-13)→ 屏幕

ArkTS 永远不直接 import libkn.so,只 import libentry.so;libkn.so 由 CMake 以 IMPORTED 目标链进 libentry.so,dlopen 时互相可见。controller 返回后同时接管生命周期:onPageShow / onPageHide / onBackPressed 都转发给它。

4.2 C++ N-API 注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    androidx_compose_ui_arkui_init(env, exports);   // compose.har 的 ArkUI 初始化入口
    napi_property_descriptor desc[] = {
        {"MainArkUIViewController", nullptr, MainArkUIViewController,
         nullptr, nullptr, nullptr, napi_default, nullptr},
        /* ...渲染节点回调注册(nativeCreateRootNode / nativeSetPixelRatio 等)... */
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr,
    .nm_register_func = Init, .nm_modname = "entry",   // 必须与 libraryName:'entry' 配对
    .nm_priv = ((void*)0), .reserved = {0},
};

MainArkUIViewController 回调只做一件事:把 N-API env 透传给 KN 导出函数并 reinterpret 返回的 controller——所有场景构建都在 Kotlin 侧。

4.3 CMake:IMPORTED + skikobridge
if(CMAKE_OHOS_ARCH_ABI STREQUAL "x86_64" OR OHOS_ARCH STREQUAL "x86_64")
    set(LIBKN_ABI_DIR "x86_64")
else()
    set(LIBKN_ABI_DIR "arm64-v8a")
endif()

find_package(compose)                                # compose.har 提供的桥包
add_library(entry SHARED napi_init.cpp)

add_library(ComposeApp SHARED IMPORTED)
set_target_properties(ComposeApp PROPERTIES
    IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${LIBKN_ABI_DIR}/libkn.so")
target_link_libraries(entry PUBLIC ComposeApp)
target_link_libraries(entry PUBLIC compose::skikobridge)   # 先链 libkn.so,再链 skikobridge
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so
                             libdeviceinfo_ndk.z.so librawfile.z.so
                             ${EGL-lib} ${GLES-lib} ${libace-lib} ${libnapi-lib} ${libuv-lib} libc++_shared.so)

链接顺序有讲究:libkn.so(Compose 场景)在前、compose::skikobridge(skia → ArkUI 的桥)在后,反了会出未解析符号。abiFilters: [arm64-v8a, x86_64] 双 ABI,每个 ABI 三件套(libentry.so / libkn.so / libc++_shared.so)缺一不可。

第 5 阶段:ArkTS 宿主

5.1 Index.ets(全部 ArkTS 业务代码)
import { ArkUIViewController, Compose } from '@cpf-kmp-cmp/compose';
import nativeApi from 'libentry.so';

@Entry
@Component
struct Index {
  private controller: ArkUIViewController | undefined = undefined;
  @State errorMessage: string = 'Native module not ready';

  aboutToAppear() {
    // 带防御:libentry.so 未编译 / module 名不配对时给出可读错误而不是白屏
    try {
      this.controller = nativeApi.MainArkUIViewController();
      if (!this.controller) this.errorMessage = 'Controller creation failed (returned null)';
    } catch (e) {
      this.errorMessage = 'Exception creating controller: ' + JSON.stringify(e);
    }
  }

  build() {
    Column() {
      if (this.controller) {
        Compose({
          controller: this.controller,
          libraryName: 'entry',                          // 与 CMake nm_modname 配对
          onBackPressed: () => this.controller!.onBackPress()
        })
      } else {
        Text(this.errorMessage)
      }
    }.width('100%').height('100%')
  }

  onPageShow() { this.controller?.onPageShow(); }
  onPageHide() { this.controller?.onPageHide(); }
  onBackPressed(): boolean { return this.controller ? this.controller.onBackPress() : false; }
}
5.2 compose.har 的 vendor 纪律

@cpf-kmp-cmp/compose(1.9.2-0.3.0,约 1.1 MB)以 compose.har 形式随仓 vendor:桥包是源,进 git;KN so 是产物,不进 git(社区模板惯例,cmp_qrose / lazytable 同款)。.gitignore 里 *.har 全局忽略、仅对 entry/libs/compose.har 反转例外;entry/libs/arm64-v8a| 与 x86_64 下的 libkn.so 由 publishDebugBinariesToHarmonyApp 每次构建回填。签名材料(signingConfigs 带 material 的完整配置)绝不进仓,仓库内保持空数组,勾选 DevEco 自动签名后由 IDE 回填。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── composeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=kn)+ export + resource cinterop + publish 任务
│   └── src/
│       ├── commonMain/.../test/              16 个移植源文件(App/Content/Theme + 10 Demo)
│       └── ohosMain/
│           ├── kotlin/.../MainArkUIViewController.kt   @CName 场景入口
│           └── cinterop/                     resource.def + raw_file*.h(OH_ResourceManager)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(DevEco 自动签名回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.multiplatformcompose、0.0.1-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/compose.har                  @cpf-kmp-cmp/compose(进 git)
        ├── libs/{arm64-v8a,x86_64}/          publish 任务回填的 libkn.so(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so + skikobridge
        ├── src/main/cpp/napi_init.cpp        N-API 导出 + androidx_compose_ui_arkui_init
        ├── src/main/cpp/include/<abi>/       publish 任务回填的 libkn_api.h(gitignore)
        ├── src/main/cpp/types/libentry/      Index.d.ts + oh-package.json5
        └── src/main/ets/pages/Index.ets      Compose({controller, libraryName:'entry'})
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:composeApp:publishDebugBinariesToHarmonyApp    # 双 ABI link + so 落位

然后 DevEco Studio 打开 example/harmonyApp(注意是 harmonyApp 目录):sync(首次自动 ohpm install)→ File → Project Structure → Signing Configs 勾选 Automatically generate signature(华为账号登录,证书写本机 ~/.ohos/config,不进仓库)→ Build → Build Hap(s)。

hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.multiplatformcompose -m entry

真机 = arm64;DevEco 模拟器 = x86_64,双 ABI 已保证不报安装错误 9568347。

四、完整代码对照

4.1 整体架构

com.rouge41.kmm.compose.test/**(上游 Demo,移植进 example/composeApp)
    │ App(抽屉 + 状态导航)/ Content / Theme / 10 个 Demo
MainArkUIViewController.kt
    │ @CName("MainArkUIViewController") + initMainHandler
libkn.so(KN sharedLib,export compose-export)
    │ ComposeArkUserController(env) { Content() }
libentry.so
    │ androidx_compose_ui_arkui_init + MainArkUIViewController 注册
@cpf-kmp-cmp/compose(compose.har)
    │ Compose({ controller, libraryName: 'entry' })
Index.ets
    │ 生命周期转发(onPageShow / onPageHide / onBackPressed)
    ▼
skia 自绘(skiko-ohos 0.9.22.2-1.1.0-13)→ ArkUI 屏幕

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_multiplatform_compose、上游模块移出
gradle/libs.versions.tomlKotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 + 三插件别名
build.gradle.kts(根)版本 0.0.1-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK21 / KN 属性 / rendererBackend=skia / ohosSkikoVersion
example/composeApp/build.gradle.kts双 ABI sharedLib、export 导出、resource cinterop、publish Copy 任务
example/composeApp/src/commonMain/**16 个移植源文件(M3 化,逻辑零改动)
example/composeApp/src/ohosMain/**/MainArkUIViewController.kt@CName 场景入口 + initMainHandler
example/composeApp/src/ohosMain/cinterop/**OH_ResourceManager 绑定(rawfile 预留)
example/harmonyApp/entry/src/main/cpp/CMakeLists.txtIMPORTED 引入 KN so、compose::skikobridge 链接
example/harmonyApp/entry/src/main/cpp/napi_init.cppN-API 导出、androidx_compose_ui_arkui_init
example/harmonyApp/entry/src/main/ets/pages/Index.etsArkTS 宿主(装载 + 生命周期转发)
README.OpenHarmony_CN.md鸿蒙适配主文档(19 节)

4.3 关键 API 对照

层次API作用
ComposeContent()上游入口组合函数(Theme → App),ohos 原样复用
ComposeModalNavigationDrawer / NavigationBarM3 抽屉与底部导航(替代 M2 ModalDrawer / BottomNavigation)
ComposelightColorScheme / darkColorScheme橙色 M3 主题(替代 M2 lightColors / darkColors)
KN 导出@CName("MainArkUIViewController")C ABI 场景入口(env 透传,controller 返回)
KN 导出initMainHandler(env)KN 协程主线程挂到 ArkTS 线程
compose-exportComposeArkUIViewController(env) { Content() }构建 Compose 场景控制器
N-APIandroidx_compose_ui_arkui_initcompose.har 的 ArkUI 桥初始化
ArkTSCompose({ controller, libraryName })把 controller 交给 ArkUI 组件渲染
ArkTSonPageShow / onBackPressed生命周期转发给 controller

4.4 ArkTS 与 Kotlin 的边界

// ArkTS 侧:只拿 controller、只转发生命周期,一行业务 UI 都没有
this.controller = nativeApi.MainArkUserController();
Compose({ controller: this.controller, libraryName: 'entry',
          onBackPressed: () => this.controller!.onBackPress() })
// Kotlin 侧:上游场景原样进 ohos,@CName 入口只有 3 行
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: CPointer<OpaquePointer>?): CPointer<OpaquePointer>? {
    initMainHandler(env)
    return ComposeArkUIViewController(env) { Content() }
}

两者之间只传 env 与 controller 指针,不传 JSON、不传 Kotlin 对象——这是路线 C 与 KStore 那种 JSON 桥的根本区别:UI 场景整体活在 Kotlin 侧,ArkTS 只是宿主。

五、关键决策说明

决策 1:走路线 C(全 Compose UI)而不是内核提取

本项目 26 个文件 100% 是 Compose UI,没有 qrose 的编码器、KStore 的存储引擎那样的"纯 Kotlin 内核"可提取;走路线 A 等于全部 ArkTS 重写,丢掉演示语义还放大改动面。制品盘点确认 CMP 1.9.2-1.1.0-13 ohos 线齐全后,路线 C 是唯一"不重写 UI"的做法——16 个小文件升 API 的移植成本远小于重写。

决策 2:移植进 example/composeApp,不原地升级 :test

上游模块留盘不进 settings 是社区目录规则(cmp_qrose 确立);原地升级会把 2022 老工具链(AGP 7.3.1 / jcenter)强拖进鸿蒙构建,两个工具链互斥。buildSrc/ 直接删除——Gradle 会无条件自动构建它,版本管理由 gradle/libs.versions.toml 接管。

决策 3:Material 2 → Material 3

M2 无 ohos klib、M3 有(lazytable 已验证);M3 是社区线的事实标准。改动集中在 import 与废弃 API(NavigationDrawerItem / NavigationBar / HorizontalDivider / buttonElevation),10 个 Demo 的逻辑零改动,橙色主色保留。

决策 4:PreCompose NavHost → 状态导航 + M3 ModalNavigationDrawer

precompose 无 ohos 制品。抽屉交互语义完整保留(开合、选中态、页面切换),只是路由实现从 NavHost 换成 mutableStateOf<Demo>——对 Demo 而言两者可观察行为一致;precompose 出制品后可换回(改动集中在 App.kt / BottomNavigationDemo.kt)。

决策 5:compose.har 进 git,libkn.so 不进

社区模板惯例:桥包是源(@cpf-kmp-cmp/compose 1.9.2-0.3.0,约 1.1 MB,ohpm 解析依赖),KN so 是产物(debugShared 单 ABI 约 80 MB,每次构建可复现)。.gitignore 全局忽略 *.so / *.har,仅对 entry/libs/compose.har 反转例外;签名材料绝不进仓。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Adoptium jdk-21.0.12.101-hotspot)、DevEco Studio 6.0.0 Release(HarmonyOS SDK 兼容 5.0.5(17),模拟器系统镜像 HarmonyOS 7.0.0 / API 26)、OpenHarmony 命令行工具(hvigor / ohpm / hdc)与鸿蒙真机(arm64)。

6.2 编译与链接验证

:example:composeApp:linkDebugSharedOhosArm64           BUILD SUCCESSFUL
:example:composeApp:linkDebugSharedOhosX64             BUILD SUCCESSFUL
:example:composeApp:publishDebugBinariesToHarmonyApp   BUILD SUCCESSFUL(4m16s)
  → entry/libs/arm64-v8a/libkn.so(约 83 MB,debugShared 未 strip)
  → entry/libs/x86_64/libkn.so(约 80 MB,debugShared 未 strip)
  → entry/src/main/cpp/include/<abi>/libkn_api.h

链接日志里 i: 级的 unlinked class symbol 'org.jetbrains.skiko/ExternalRenderNode' 提示是社区 CMP 分支 fusion renderer 的信息级输出(对应融合渲染特性,本 Demo 用 skia 自绘后端不走该路径),不影响产物与运行。

6.3 HAP 验证

BuildNativeWithCmake: finished(双 ABI 编出 libentry.so,链 libkn.so + skikobridge)
CompileArkTS: finished
PackageHap: finished(双 ABI 三件套 so 进包)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

DevEco 打开 example/harmonyApp 后 sync → 自动签名 → Build Hap 全链路通过。

6.4 功能用例(真机逐项验收)

  1. 启动进入 Compose 界面,顶部栏显示 ☰ + 当前 Demo 名,首帧无白屏;
  2. 抽屉开合正常,10 个 Demo 逐项可切换;
  3. HelloPlatform 居中显示 Hello, OpenHarmony (Kotlin/Native)!(Platform actual 生效);
  4. Layout 页彩色 Box / Column / Row / Spacer 按上游配色排布(绿 / 品红 / 黄 / 红 / 黑 / 蓝);
  5. LazyColumn 25 行懒加载 + 分隔线,快速滑动不丢帧;
  6. Counter 的 + / - 改数字,状态保留;
  7. Buttons 6 种按钮全部可点;
  8. TextFields 弹软键盘、实时回显;
  9. BottomNavigation 三 Tab 切换,选中态高亮;
  10. 返回键经 onBackPressed 转发 controller,默认行为退出应用。

6.5 验证结论

Compose 编译器、场景树、社区 CMP 分支 klib、KN 动态库导出、CMake / N-API 桥、ArkTS 宿主和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,抽屉导航与 10 个 Material 演示页全部由同一份 Compose 代码渲染(见下节效果图)——"一份 Compose UI 跑 Android + iOS + OpenHarmony"的三端证明成立。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/multiplatform-compose.git
cd multiplatform-compose
export JAVA_HOME="/path/to/jdk-21"                 # Windows: $env:JAVA_HOME="...jdk-21..."
./gradlew :example:composeApp:publishDebugBinariesToHarmonyApp   # 双 ABI link + so 落位
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.multiplatformcompose -m entry
hdc shell hilog | grep Compose

八、遗留问题与改进方向

8.1 踩坑复盘

  1. gradlew.bat 报 JAVA_HOME is not set:启动器需要自己的 JVM,org.gradle.java.home 管不到它——先 set JAVA_HOME=...JDK21 再跑 gradlew;
  2. 配置期 Unresolved reference: ohosArm64:eazytec 双仓没放在 pluginManagement / dependencyResolutionManagement 最前,置前即解;
  3. M2 API 编译不过(ListItem(text=)、rememberScaffoldState、BottomNavigation…):compose M2 无 ohos 变体,只能依赖 M3,按 M3 API 移植;
  4. 图标 import 失败:material-icons 无 ohos klib,文本字形(☰ / ⌂)替代,不为一两个图标 vendor 整库;
  5. KN sharedLib 找不到 ComposeArkUIViewController:默认不导出依赖符号,必须 export(libs.compose.multiplatform.export);
  6. CMake 链接顺序:libkn.so(IMPORTED)在前、compose::skikobridge 在后,反了出未解析符号;
  7. ArkTS 拿不到 controller(nativeApi is undefined):CMake nm_modname 必须是 entry,与 Compose({libraryName:'entry'}) 配对;
  8. 启动白屏 / 黑屏:多为 libkn.so 与 libentry.so 不来自同一次构建——重跑 publish 后再 assembleHap,hdc shell hilog | grep Compose 定位;
  9. x86_64 模拟器装 HAP 报 9568347:HAP 缺当前 ABI 的 so,abiFilters 双 ABI 且每个 ABI 三件套缺一不可;
  10. buildSrc/ 必须删除而不是留盘:Gradle 无条件自动构建它,老版本目录会把 2022 工具链拖进鸿蒙构建;
  11. 上游模块被误拖进构建:settings 显式 include 了它们——本 fork 只 include :example:composeApp;
  12. 根构建出现 ohos Test 任务失败:鸿蒙 Kotlin 跑不了这些测试,根 build 全局禁用 *Ohos*Test* 任务(沿用社区模板);
  13. DevEco 打开错目录无法运行:必须打开 example/harmonyApp(HAP 工程),打开 example/composeApp(Gradle 模块)或仓库根目录都没有运行配置;
  14. 签名材料由 DevEco 自动写入 build-profile.json5:仓库内保持空数组,证书绝不进 AtomGit。

8.2 已知问题

  • DarkMode / SafeArea 未接系统真值:上游在 iOS 用 Swift 回调注入安全区和暗色模式;ohos 侧 safeAreaState 恒为零 Padding、darkmodeState 取 isSystemInDarkTheme() 运行时返回,视觉上不影响 Demo;
  • 网络图片不可用:Images 页 AsyncImage 显示占位说明(Ktor 无 ohos 引擎制品),本地图由 Canvas 现画承担;
  • 无返回键拦截:上游靠抽屉导航未注册返回栈,ohos 返回键由 ArkTS onBackPressed 转发(默认行为退出应用);
  • M3 视觉代际差异:按钮 / Scaffold / 抽屉按 Material 3 规范渲染,与上游 2021 年 M2 截图存在代际差异,橙色主色保留;
  • 上游 Android / iOS 构建不在本仓 settings 中:想构建上游原始双端 Demo,git checkout 上游提交或临时把 :androidApp / :test 加回 settings(需自带旧工具链,与鸿蒙构建互斥);
  • HAP 安装必须使用与 bundle name org.terminator.ohos.multiplatformcompose 匹配的签名。

8.3 未来优化方向

  • precompose 出 ohos 制品后,把状态导航换回 NavHost(改动集中在 App.kt / BottomNavigationDemo.kt);
  • ktor 或其他网络栈出 ohos 引擎后,恢复 AsyncImage 真下载;
  • 把 DarkMode / SafeArea 接上 ArkTS 回调(EntryAbility 监听 onConfigurationUpdate → native 通知 KN 更新 CompositionLocal);
  • compose resources 管线就绪后,用 rawfile 真·资源图替换 Canvas 位图(cinterop 已预留);
  • 视社区节奏评估升级到 1.12.0-0.1.0-07 线(Kotlin 2.4.20),届时 M2 / material-icons 制品或已补齐,可回迁更接近上游的写法;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充模拟器(x86_64)验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 :test 共享库(Compose 1.2 / M2 / PreCompose / Ktor)
    → 查重定位:无可提取内核 → 路线 C 全 Compose UI
    → 制品盘点:CMP 1.9.2-1.1.0-13 ohos 线齐全 / M2、precompose、ktor、icons 无制品
    → 工具链钉版:Kotlin 2.2.21-1.1.0-05 + Gradle 8.14.1 + JDK 21(eazytec 双仓置前)
    → 根构建替换(cmp_qrose 模式)+ buildSrc 删除
    → 16 个源文件移植:M2→M3、导航状态化、Ktor 占位、图标字形化
    → ohosArm64 / ohosX64 sharedLib + export 导出 + resource cinterop
    → CMake IMPORTED + skikobridge + N-API(androidx_compose_ui_arkui_init)
    → ArkTS 宿主(Compose({controller, libraryName}) + 生命周期转发)
    → DevEco HAP → 自动签名 → 真机验收(10 个 Demo 逐项过)

9.2 封装层次

  • Compose 层:上游原版的 App / Content / Theme 与 10 个 Demo(M3 化,逻辑零改动);
  • 导出层:@CName("MainArkUIViewController") + initMainHandler + compose-export 的 ComposeArkUserController;
  • Native 层:双 ABI libkn.so(export 桥符号)+ libkn_api.h;
  • N-API 层:libentry.so 注册导出函数、初始化 ArkUI 桥;
  • ArkTS 层:装载 controller、转发生命周期、错误兜底显示;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:CMP ohos 线在 1.9.2-1.1.0-13 齐全是路线 C 的前提,M2 / precompose / ktor / icons 的缺位决定了移植面——路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走、按线区分经验:Compose 1.9.2-1.1.0-13 线配 Kotlin 2.2.21-1.1.0-05 + Gradle 8.14.1;KStore 那条 2.4.20 线才允许 Gradle 9.x——两条线的经验不能互相套用;
  3. 验收以"真跑起来的 Demo"为准:UI 库 / Demo 型项目的交付物是可安装的 HAP 与真机上逐项可点的演示页,"编译通过"只占验收链路的三分之一——链接、落位、签名、安装、运行、十个页面逐项过才算闭环。

9.4 适配成果

当前适配已完成:

  • 上游 Demo 的双 ABI Kotlin/Native 编译(16 个源文件 M3 化移植,逻辑零改动;publishDebugBinariesToHarmonyApp 实测 BUILD SUCCESSFUL,双 ABI libkn.so + libkn_api.h 自动落位);
  • 社区 Compose-for-OHOS 分支 1.9.2-1.1.0-13(skia 自绘)在演示 App 上的完整落地;
  • @CName 场景入口 + export 导出 + CMake / N-API 桥(androidx_compose_ui_arkui_init + skikobridge 链接);
  • DevEco hvigor HAP 工程(vendored compose.har、双 ABI 三件套 so、ArkTS 宿主 + 生命周期转发);
  • 真机(arm64)安装、运行与逐项验收:抽屉导航 + 10 个 Material 演示页全部由同一份 Compose 代码渲染(见第七节效果图);
  • AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 19 节)和本地效果图。

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

更多推荐