本文记录 ContactsComposeMultiplatform 接入 OpenHarmony 的完整过程,覆盖 KMP 公共应用层盘点、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。

ContactsComposeMultiplatform 是 Philipp Lackner 的 Compose Multiplatform 通讯录教程应用(Android + iOS),shared 模块 commonMain 中包含完整的 domain / data / presentation 三层。本次适配让这三层业务模型进入 OpenHarmony ARM64 运行时,ArkUI 页面作为真机验收宿主,Compose Multiplatform(CMP)应用的同类三层代码可以通过同一条 Kotlin/Native 链路复用。

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

上游仓库: philipplackner/ContactsComposeMultiplatform

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

ContactsComposeMultiplatform 为 CMP 提供一个"三层齐全"的真实应用样例:Contact 实体与 ContactValidator 校验器(domain 层)、ContactDataSource 数据源契约与 contact.sq 建表语句(data 层)、ContactListViewModel 状态机与事件面(presentation 层),再由 Compose UI 渲染。KMP 项目通常把这些能力放在 commonMain,由 Android、iOS 宿主共享。OpenHarmony 应用同样需要这套能力,但默认构建并不会生成 OpenHarmony 的 Kotlin/Native 变体。

如果只把示例页面改成 ArkTS,页面可以显示联系人,却无法证明公共业务模型已经在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证 KMP 模型、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。
工具链不一致上游是 Kotlin 1.8.20 / Compose 1.4.0;社区鸿蒙 CMP 是 1.9.2、HarmonyOS Kotlin 是 2.2.21-1.0.0,整体迁移等于重写并拖坏 Android / iOS。
依赖缺位SQLDelight 1.5.5 无 ohos driver,moko-mvvm 0.16.1 无 ohos 目标,kotlinx-datetime 0.4.0 同样缺失。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
异步语义差异上游用 Flow + combine 聚合状态、delay(300L) 等浮层动画;同步 C ABI 桥没有协程泵。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在 KMP 目标配置、C ABI/N-API 桥接和 ArkUI 验收层。业务语义仍由公共 Kotlin 实现负责,ArkTS 只负责调用 Native、解析 JSON 和渲染结果。

1.2 应用提供的能力

真机示例提供五个可观察场景:

  • 联系人列表: getContacts 按 firstName ASC 排序渲染;
  • 最近添加: getRecentContacts(20) 按 createdAt DESC 取前 20 条横向滑动展示;
  • 新增 / 编辑: 四字段表单(First name / Last name / Email / Phone number),保存前经 ContactValidator 校验,错误文案与上游逐字一致;
  • 删除: 详情浮层删除后,列表与最近添加两处同步消失;
  • 头像: PhotoViewPicker 选图,字节以 Base64 过桥,渲染为 PixelMap。

页面渲染的是 ContactListState 完整状态快照:

字段作用
contacts全量联系人,firstName ASC。
recentlyAddedContactscreatedAt DESC 前 20 条。
selectedContact详情浮层当前联系人。
newContact新增 / 编辑表单正在编辑的草稿。
isAddContactSheetOpen / isSelectedContactSheetOpen两个浮层的开关。
firstNameError 等 ×4四条校验错误文案。
totalContacts列表总数,渲染 My contacts (n)。

共享层还提供八项对账检查:首字母排序、最近添加限 20 且按时间倒序、四条错误文案逐字一致、空表单保存不落库、INSERT OR REPLACE 保号不重复、编辑刷新 createdAt、动画时序(cleanup 语义)和错误信封(未知动作返回 ok:false + error)。

1.3 实现适配

维度要求
代码复用domain / data / presentation 三层逻辑全部位于 Kotlin,逐字对齐上游(校验器含正则与错误文案)。
平台目标独立 Gradle 工程编译 ohosArm64 + ohosX64,生成双 ABI 动态库。
桥接稳定使用少量 C ABI 和 UTF-8 JSON 完整状态快照,不传递 Kotlin 对象地址。
CMP 兼容根工程原样保留,Android / iOS 继续用 Kotlin 1.8.20 构建;ArkUI 只负责真机验收。
UI 完整列表、最近添加、新增 / 编辑浮层、详情浮层、校验错误展示和头像选择。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以直接复用同一套 Kotlin 三层模型;等社区 CMP 工具链与上游版本对齐后,Compose UI 层可整体上岗,桥与模型层不动。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点上游三层结构、工程边界和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── 独立 example Gradle 工程、双 ABI sharedLib 和产物拷贝
第 3 阶段:业务模型建模   ── Contact / ContactValidator / contact.sq 语义 / 状态机
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 完整状态快照渲染、浮层时序、头像链路和错误边界
第 6 阶段:示例与验证     ── JVM 对照测试、双 ABI 链接、HAP、签名、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
androidContactsMP/                上游 Android App(未改动)
iosContactsMP/                    上游 iOS App(未改动)
shared/                           上游 shared 模块:Compose UI + 三层逻辑(未改动)
example/                          鸿蒙适配独立 Gradle 工程(Gradle 与 Hvigor 分离)
  nativeApp/                      Kotlin 移植层 → ohosArm64/ohosX64 → libohoscontacts.so
  harmonyApp/                     DevEco Stage 工程:CMake NAPI + ArkTS UI → HAP

example 是独立 Gradle 工程,不把 DevEco 工程作为根项目的 Kotlin 子模块,Gradle 和 Hvigor 可以分开执行;根工程的构建文件一个字节都不用改。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
Gradle8.14.1(example 自带 wrapper)
JDK21(org.gradle.java.home 已钉进 example/gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64
DevEco product5.1.0(18),runtimeOS HarmonyOS
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.contacts
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64

example/settings.gradle.kts 中两个 eazytec 仓库必须写在 gradlePluginPortal() / google() 之前,否则 HarmonyOS Kotlin 插件解析不到。

1.3 页面边界
最近添加区      横滑头像卡(createdAt DESC 前 20)
标题区          My contacts (n)
列表区          联系人行(firstName ASC)
操作区          FAB 新增 / Save / Edit / Delete
浮层区          新增编辑表单(四字段 + 校验错误)/ 联系人详情

页面启动时发送一次 seed 动作播种演示数据,之后每次交互都是"一个动作进、一份完整状态快照出"。

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

2.1 双目标 sharedLib
kotlin {
    applyDefaultHierarchyTemplate()

    compilerOptions {
        optIn.add("kotlin.experimental.ExperimentalNativeApi")
    }

    ohosArm64 {
        binaries.sharedLib { baseName = "ohoscontacts" }
    }
    ohosX64 {
        binaries.sharedLib { baseName = "ohoscontacts" }
    }
}

只有 nativeApp 一个模块开启 binaries.sharedLib,避免每个模块各吐一份带运行时的 so。链接任务 finalizedBy 两个 Copy 任务,把 libohoscontacts.so(实测 arm64-v8a 约 8.9 MB、x86_64 约 7.6 MB,含 Kotlin/Native 运行时)和匹配 triple 的 libc++_shared.so 拷进 harmonyApp/entry/libs/{arm64-v8a,x86_64}/。

2.2 产物链
example/nativeApp
  └─ build/bin/ohosArm64/debugShared/libohoscontacts.so
example/harmonyApp/entry/libs/arm64-v8a/
  ├─ libohoscontacts.so
  └─ libc++_shared.so

entry/libs/ 整体在 .gitignore 里——产物不进版本库,clean 之后重跑链接命令即可再生。

2.3 独立工程的依赖策略

移植层不依赖任何第三方库:Bridge.kt 手写 MiniJson,时钟用 POSIX gettimeofday,状态机是纯 Kotlin reducer。少一个需要和 HarmonyOS Kotlin 版本对齐的依赖,桥的 C 签名就多一分稳定。

第 3 阶段:业务模型建模

3.1 JSON 契约
ContactListStateMachine.onEvent(event)
        ↓ ContactListState
refreshContacts()(重读两个列表,等价上游 combine)
        ↓ 完整状态快照 JSON
C ABI ContactsDispatch(const char*)
        ↓
N-API contactsDispatch(request)
        ↓
ArkUI Index.ets(纯渲染)

页面不复制任何业务判断:排序、校验、浮层状态机的每一次迁移都由公共 Kotlin 代码生成。

3.2 结果数据类
data class Contact(
    val id: Long?,
    val firstName: String,
    val lastName: String,
    val email: String,
    val phoneNumber: String,
    val photoBase64: String?,
)

data class ContactListState(
    val contacts: List<Contact> = emptyList(),
    val recentlyAddedContacts: List<Contact> = emptyList(),
    val selectedContact: Contact? = null,
    val isAddContactSheetOpen: Boolean = false,
    val isSelectedContactSheetOpen: Boolean = false,
    val firstNameError: String? = null,
    val lastNameError: String? = null,
    val emailError: String? = null,
    val phoneNumberError: String? = null,
)

photoBytes: ByteArray? 改为 photoBase64: String?,是唯一一处字段级改动:C ABI 走 UTF-8 JSON,字节数组过不去。

3.3 校验与数据源语义

校验器逐字移植,错误文案一个字符不差:

val emailRegex = Regex("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}\$")
if (!emailRegex.matches(contact.email)) {
    result = result.copy(emailError = "This is not a valid email.")
}

SQLDelight 没有 ohos driver,内存实现严格复刻 contact.sq 四条语义:

SQL内存实现
SELECT * ORDER BY firstName ASCsortedWith(compareBy({ firstName }, { id }))
ORDER BY createdAt DESC LIMIT :amount时间倒序取前 N,另补一条 SQL 没写的约束:连续插入共享同一毫秒时 createdAt 强制严格递增
INSERT OR REPLACE编辑复用原 id 整行替换并刷新 createdAt(被编辑的联系人爬回"最近添加"顶部)
自增主键id=NULL 走自增,带 id 替换保号

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

4.1 三层桥接
ArkTS
  │ JSON string(一次动作)
  ▼
C++ N-API entry
  │ const char*
  ▼
Kotlin/Native C ABI
  │ ContactListStateMachine
  ▼
完整状态快照 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;HarmonyOS CAdapter 也不会把 @CName 业务函数挂到 ArkTS 模块上,所以 ArkTS 只 import CMake 包装库 libcontacts_napi.so。

4.2 Kotlin/Native 导出函数
@CName("ContactsDispatch")
fun contactsDispatch(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        val error = dispatchAction(json)
        stateJson(ok = error == null, error = error)
    } catch (t: Throwable) {
        stateJson(ok = false, error = t.message ?: t.toString())
    }
}

返回值是 Kotlin 字符串;C++ 创建 ArkTS 字符串后立即调用 libohoscontacts_symbols()->DisposeString 释放。Throwable 在桥内降级为错误 JSON——Kotlin/Native 异常跨语言边界会变成无信息崩溃。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"contactsDispatch", nullptr, NapiContactsDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

contactsDispatch 检查参数必须是字符串。N-API 只负责参数类型、字符串创建和释放,不实现业务语义。

4.4 CMake 和类型声明
add_library(ohoscontacts SHARED IMPORTED)
set_target_properties(ohoscontacts PROPERTIES
    IMPORTED_LOCATION ${KN_SO}
    IMPORTED_NO_SONAME TRUE)

add_library(contacts_napi SHARED napi_init.cpp)
target_link_libraries(contacts_napi PUBLIC
    libace_napi.z.so
    libhilog_ndk.z.so
    ohoscontacts)
export const contactsDispatch: (request: string) => string;

模块名 contacts_napi、so 名和 index.d.ts 三处一致;POST_BUILD 把 KN so 和 libc++_shared.so 拷到 NAPI 旁边,$ORIGIN rpath 保证 dlopen 可解析。

第 5 阶段:ArkUI 宿主

5.1 快照加载与动作分发
import { contactsDispatch } from 'libcontacts_napi.so';

this.state = this.bridge.seed();          // aboutToAppear 播种
this.state = this.bridge.addNew();        // FAB
this.state = this.bridge.setField('email', v);  // 输入框 onChange
this.state = this.bridge.save();          // 保存(错误文案随快照回来)

解码层是手写的 typed 辅助函数(asString / asNumber / asBool / decodeContact),全链路禁 any——解码层错了编译期就该炸,不是运行期。

5.2 浮层时序

两个浮层用 bindSheet 实现。上游 ContactListViewModel 在 Delete / Dismiss / Save 后 delay(300L) 等浮层收起动画播完再清 selectedContact / newContact;同步桥不能睡眠,因此协议里增加 ohos 专属 cleanup 动作,ArkTS 在 onDisappear 后延时发送:

private onDetailSheetClosed(): void {
  if (this.state.isAddContactSheetOpen) {
    return; // 详情正在切换为编辑表单,不能误关
  }
  this.dispatch(() => this.bridge.dismiss());
  setTimeout(() => {
    this.dispatch(() => this.bridge.cleanup());
  }, 350);
}
5.3 头像链路和错误边界

选图走 photoAccessHelper.PhotoViewPicker,fileIo 读字节、Base64 过桥;渲染方向 Base64Helper().decodeSync() → image.createImageSource() → createPixelMap(),缓存于 Map 并整体重新赋值触发刷新。所有照片 API 用 try/catch 包裹,失败只打 hilog 不打断 UI;校验失败时输入框描边与占位符变错误色,下方小字原样显示 Kotlin 返回的错误文案。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/src/ohosMain/kotlin/
│   ├── contacts/domain/           Contact / ContactDataSource / ContactValidator
│   ├── contacts/data/             InMemoryContactDataSource
│   ├── contacts/presentation/     ContactListState / Event / StateMachine
│   └── example/nativeapp/         Bridge.kt(@CName + MiniJson)
└── harmonyApp/entry/src/main/
    ├── cpp/                       CMakeLists.txt / napi_init.cpp / types/
    ├── ets/                       entryability / model / pages/Index.ets
    └── resources/
6.2 构建、签名和安装
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64

DevEco Studio 打开 example/harmonyApp,File > Project Structure > Signing Configs 勾选 Automatically generate signature 生成调试证书后 Run;命令行等价物:

hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.contacts

build-profile.json5 仓库内只保留 "signingConfig": "default" 占位,签名材料(证书与密码)由 DevEco 写在本机 ~/.ohos/config/,不进入 AtomGit。

四、完整代码对照

4.1 整体架构

contacts/{domain,data,presentation}
    │ Contact / Validator / contact.sq 语义 / 状态机
    ▼
Bridge.kt
    │ @CName("ContactsDispatch") + MiniJson
    ▼
libcontacts_napi.so
    │ contactsDispatch(request): string
    ▼
Index.ets
    │ typed 解码 + 状态快照渲染
    ▼
ArkUI 真机页面

4.2 文件清单

文件职责
example/settings.gradle.kts独立工程配置,eazytec 仓库在前
example/gradle.propertiesJDK 21 路径 + KN 开关
nativeApp/build.gradle.kts双 ABI sharedLib + Copy 任务
.../contacts/domain/ContactValidator.kt逐字移植的校验器
.../contacts/data/InMemoryContactDataSource.ktcontact.sq 四条语义的内存实现
.../contacts/presentation/ContactListStateMachine.ktmoko-mvvm ViewModel 的同步 reducer
.../example/nativeapp/Bridge.ktC ABI、JSON 编解码
harmonyApp/.../cpp/napi_init.cppN-API 导出和参数检查
harmonyApp/.../ets/model/ContactsBridge.etstyped 解码层
harmonyApp/.../ets/pages/Index.etsArkUI 页面

4.3 关键 API 对照

层次API作用
KMPContactValidator.validateContact四字段校验,产出逐字错误文案
KMPInMemoryContactDataSource列表 / 最近添加 / 插入 / 删除
KMPContactListStateMachine.onEvent事件驱动的状态迁移
NativeContactsDispatch请求进、完整状态快照出
Nativelibohoscontacts_symbols()->DisposeString释放 Kotlin 字符串
N-APIcontactsDispatch向 ArkTS 暴露唯一入口
ArkTSapplyState快照整体赋值 + 浮层镜像同步

4.4 ArkTS 与 Kotlin 的边界

fun contactsDispatch(request: String): String  // Kotlin:动作 → 快照
this.state = this.bridge.select(id);           // ArkTS:快照 → 渲染

两者之间只传输字符串和 JSON,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:根工程不动,独立 example/ 工程

上游 Compose 1.4.0 与社区鸿蒙 CMP 1.9.2 两个时代的 API 面对不齐;整体迁移会拖坏 Android / iOS。Gradle 与 Hvigor 分离的布局让鸿蒙构建完全不碰根工程。

决策 2:只有 nativeApp 出 sharedLib,双 ABI 都要

库模块出 klib、示例模块出 so,避免运行时被打包多份。真机是 arm64、DevEco 模拟器是 x86_64,HAP 只带单 ABI 会安装失败(错误码 9568347)。

决策 3:内存数据源复刻 SQL 语义

SQLDelight 无 ohos driver,但排序、限流、INSERT OR REPLACE、自增这四条语义是可对账的行为契约,逐条在内存实现里复刻,接口面 ContactDataSource 为将来接关系型存储预留。

决策 4:JSON 完整状态快照作为跨语言契约

一次动作进、一份快照出,ArkTS 是状态机的纯渲染器,不存在两端状态漂移的生存空间;以后字段变化只动序列化器,C 签名不动。

决策 5:ArkUI 作为验收宿主,cleanup 替代 delay(300L)

页面验证 OpenHarmony Native 链路;上游两处动画等待收敛为一个 ohos 专属事件,由 ArkTS 在浮层动画结束后发送,时序语义与上游一致。

六、测试与验证

6.1 测试环境

本次验证使用 macOS、JDK 21(Android Studio JBR)、DevEco Studio 5.1.0(18)、OpenHarmony ARM64 Native SDK 和华为真机(arm64-v8a,DevEco 自动签名)。

6.2 共享层全流程对照测试

链接 ohos 之前,同一份三层代码 + Bridge 先在 JVM 上跑完整用户旅程:seed → 空表单保存出四条逐字错误 → 填字段保存 → 详情 → 编辑保存(联系人爬回最近添加顶部)→ 删除 → cleanup,共 40 项断言全部通过,每个响应都经过严格 JSON 语法校验。这次对照测试抓出过两个真实缺陷(序列化键名被写成无效字符串语句导致 JSON 缺键;种子数据插入顺序与时间倒序方向相反),正是它存在的意义。

== 8. firstName ASC ordering ==
  PASS  contacts sorted by firstName ASC: [Alan, Dennis, Grace, Katherine, Linus, Margaret]
ALL PASSED

6.3 HAP 验证

Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
BUILD SUCCESSFUL
entry-default-signed.hap(双 ABI,六个 so 三件套齐全)
nm: ContactsDispatch / libohoscontacts_symbols 双 ABI 可见

6.4 功能用例

  1. 首屏播种 6 位演示联系人,最近添加第一位是 Grace Hopper;
  2. 列表按 firstName ASC 排序,标题显示 My contacts (6);
  3. 空表单点保存,四条错误文案逐字出现,联系人数不变;
  4. 填合法数据保存,浮层收起,列表与最近添加顶部同时出现新联系人;
  5. 详情浮层 Edit 进入编辑,改完保存后该联系人爬回最近添加顶部;
  6. 详情浮层 Delete,联系人从两处消失;
  7. 新增时选头像,照片以 Base64 过桥,行内与浮层都能显示。

6.5 验证结论

Kotlin/Native 双 ABI 动态库、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在华为真机上成功安装并进入前台,页面展示了真实的三层模型行为:排序、限流、逐字校验文案、浮层状态机和头像链路。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-multiplatform-AnimatedImage.git
cd compose-multiplatform-AnimatedImage/example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,配置自动签名后 Run,或:
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.contacts

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须真正编译 ohosArm64 动态库;
  2. N-API 不负责业务判断,C++ 只做类型检查、字符串转换和释放;
  3. Native 返回串交给 N-API 后必须调用 DisposeString,不能 free;
  4. Kit 命名随 SDK 版本变化:SDK 5.1.0(18) 中 photoAccessHelper 由 @kit.MediaLibraryKit 导出,文件 API 是 fileIo 而不是 fs,以 $DEVECO_SDK_HOME 下 @kit.*.d.ts 导出面为准;
  5. Kotlin 里字符串字面量写成独立语句会被编译器当死代码消除,序列化键名必须走 append("...") 调用——JVM 对照测试在链接前抓住了这个 JSON 缺键缺陷;
  6. 签名材料只存在本机 ~/.ohos/config/,仓库内保留占位配置;
  7. 浮层内容收起中途闪没,是 cleanup 发早了,必须等动画结束。

8.2 已知问题

  • 内存数据源不跨进程重启持久化,杀进程重开回到种子数据;
  • 照片以 Base64 存内存不落文件系统,大图会随快照反复过桥;
  • firstName 排序用 Kotlin 字符串比较,ASCII 场景与 SQLite BINARY collation 一致,多语种名字会有差异;
  • 无搜索——上游也没有,补了反而制造功能面对不齐的对账噪音;
  • CAdapter 链接期有一条 api.cpp clang 警告,是鸿蒙 Kotlin 生成的胶水代码,非业务代码。

8.3 未来优化方向

  • 用关系型存储或 Preferences 实现 ContactDataSource 的持久化变体,接口面已预留;
  • 照片先落盘再存引用,Base64 只走选择器到磁盘一段;
  • 等社区 CMP 工具链与上游版本对齐后,退役 ArkTS 渲染层,shared/commonMain 的 Compose UI 直接上岗;
  • 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
  • 补充不同设备 ABI、API 版本的验收矩阵。

九、总结

9.1 核心难点回顾

KMP commonMain 三层业务模型
    → ohosArm64/ohosX64 Kotlin/Native
    → C ABI(ContactsDispatch)
    → C++ N-API(libcontacts_napi.so)
    → ArkTS typed 解码
    → ArkUI 真机页面

9.2 封装层次

  • KMP 层:提供 Contact、ContactValidator、数据源语义和 ContactListState 状态机;
  • Native 层:生成双 ABI 动态库并通过唯一 C ABI 输出完整状态快照;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理快照解码、浮层时序、头像缓存和错误展示;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先让三层模型在 JVM 全流程对照测试通过,再链接 ohos 动态库——序列化缺陷在链接前就该被抓出来;
  2. 用 JSON 完整状态快照和少量 C ABI 代替跨语言对象传递,并明确字符串释放责任;
  3. 分别记录共享层测试、双 ABI 链接、HAP 构建和真机运行,避免把编译成功误认为设备链路可用。

9.4 适配成果

当前适配已完成:

  • domain / data / presentation 三层的 ohosArm64 + ohosX64 双 ABI 动态库;
  • 校验器逐字对齐、contact.sq 四条语义逐条复刻、事件面只多一个可撤掉的 cleanup;
  • JVM 对照测试 40 项断言全部通过的验收基线;
  • DevEco Stage ArkUI 验收页面(列表、最近添加、双浮层、校验、头像);
  • HAP 构建、自动签名、设备安装和华为真机验证;
  • 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

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐