开源鸿蒙平台 KMP/CMP 三方库「智感握姿」适配全流程
本文记录
kmp-smart-grip接入 OpenHarmony 智感握姿能力的完整过程,覆盖 KMP/CMP 工程盘点、ohosArm64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、权限、签名 HAP 和真机验收。本次适配复用 Kotlin 侧的状态模型、状态码解析、不可变归约和验收逻辑,再由 ArkTS 调用 HarmonyOS
@kit.MultimodalAwarenessKit的motion.on('holdingHandChanged')。这样验证的是同一份 KMP 代码在 OpenHarmony ARM64 设备上的运行结果,而不是重新写一套只在页面里生效的状态判断。
项目地址: AtomGit/oh-tpc/kmp-smart-grip
开发工具: DevEco Studio
一、背景
1.1 为什么做开源鸿蒙平台 KMP/CMP 适配
智感握姿是 HarmonyOS 多模态感知能力的一部分。应用订阅 holdingHandChanged 后,系统会根据设备传感器和系统算法返回当前握持状态。业务层不需要读取原始传感器,也不需要自行实现左右手识别算法。
如果只把页面重新写成 ArkTS,页面可能会显示几个“左手”“右手”按钮,却无法证明共享 Kotlin 模型、Native 动态库和实际设备事件已经连通。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | KMP 模块默认只有 JVM,必须增加 ohosArm64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK 和 Gradle 插件必须使用匹配版本。 |
| 系统能力边界 | motion 属于设备能力,编译成功不代表每台设备都支持握姿事件。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin data class,必须经过 C ABI、C++ N-API 和 JSON。 |
| 事件生命周期 | 订阅、重复订阅、停止监听和页面销毁都要与 motion.off 对应。 |
| 权限要求 | 宿主需要声明 DETECT_GESTURE 和 ACTIVITY_MOTION 使用场景。 |
| 交付链路复杂 | Native 动态库、CMake、HAP、签名、设备安装和 ARM64 依赖需要分别验证。 |
因此,本项目把适配边界放在三个地方:Kotlin/Native 目标配置、C ABI/N-API 桥接层和 ArkUI 事件层。状态模型仍由 KMP 维护,ArkTS 只负责系统事件生命周期、页面状态和权限错误。
1.2 库提供的能力
smart-grip 公共模块提供以下能力:
SmartGripStatus:未握持、左手、右手、双手和未识别五种状态;SmartGripStatus.fromCode:把系统整数码转换为类型安全的枚举;SmartGripSnapshot:携带状态、规范化状态码和事件序号的不可变快照;SmartGripEngine.reduce:接收新的系统事件并递增事件序号;SmartGripEngine.runChecks:在 JVM、Kotlin/Native 和 ArkTS 示例中复用同一组检查;SmartGripSnapshot.toJson:生成稳定的跨语言 JSON;SmartGrip:给业务提供简单的statusFromCode和snapshot门面。
状态码约定如下:
| KMP 枚举 | 状态码 | 含义 |
|---|---|---|
SmartGripStatus.NONE | 0 | 未识别到握持 |
SmartGripStatus.LEFT | 1 | 左手握持 |
SmartGripStatus.RIGHT | 2 | 右手握持 |
SmartGripStatus.BOTH | 3 | 双手握持 |
SmartGripStatus.UNKNOWN | 16 | 系统无法识别或收到新状态码 |
未知整数统一进入 UNKNOWN,旧版本应用不会因为系统未来增加状态码而崩溃。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 状态枚举、状态码映射、序号归约、JSON 和自检由 Kotlin 共享。 |
| 平台目标 | 为公共模块和示例加入 ohosArm64,生成 libsmart_grip.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象地址交给 ArkTS。 |
| UI 完整 | 页面支持本地状态预览、开始监听、停止监听和自检展示。 |
| 可测试 | JVM 测试、Native 链接、HAP 构建、设备安装和系统事件分别验收。 |
| 签名安全 | 源码只保留签名配置入口,证书和密钥材料由开发者本机配置。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面是独立的真机验收宿主,公共 API 仍然保持平台无关。其他 KMP/CMP 应用可以复用
SmartGripStatus和SmartGripEngine,再自行决定如何呈现左右手布局。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 KMP 模块、状态模型和 OpenHarmony 示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、独立 example 工程和 Native 构建任务
第 3 阶段:状态与序列化 ── 建立 SmartGripStatus、Snapshot、Reducer 和 JSON 契约
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API、内存释放和类型检查
第 5 阶段:系统能力封装 ── ArkTS motion.on/off、权限声明和页面生命周期
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装、事件回调和效果图
每个阶段都使用真实产物作为下一阶段输入:先用 JVM 验证共享模型,再把相同代码链接为 ARM64 动态库,随后由 CMake 和 N-API 加载到 Stage 工程,最后安装签名 HAP 观察设备事件。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点公共 API 和工程边界
项目采用与参考 CMP 工程一致的分层:
smart-grip/ KMP 状态模型、归约、自检和 JSON 边界
vico/ 与参考工程一致的库聚合层
sample/ android/desktop/shared/web/ios 主机入口
example/shared/ 示例门面和 JVM 验收测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 工程和 ArkUI 页面
scripts/ Native、HAP 和签名工程辅助脚本
docs/openharmony/ 验收记录和真机效果图
guide/ 集成指南
example 是独立 Gradle 工程,不把 DevEco 工程当作 Kotlin 子模块。这样可以分别执行 Gradle 和 Hvigor,也可以把 ohosApp 复制到另一个目录后在 DevEco Studio 中配置签名。
1.2 固定工具链和版本矩阵
本项目使用以下版本约定:
| 项目 | 配置 | 用途 |
|---|---|---|
| Kotlin Multiplatform | 2.2.21-1.0.0 | JVM、Kotlin/Native 和 KLIB |
| JDK | 21 | Gradle、Kotlin 编译和 Native 任务 |
| OpenHarmony 目标 | ohosArm64 | ARM64 真机动态库 |
| DevEco product | 6.0.0(20) | 工程兼容和目标版本 |
| ABI | arm64-v8a | HAP 原生库架构 |
| 状态事件 | holdingHandChanged | 系统握姿回调 |
执行 Gradle 脚本前先选择 JDK 21:
export JAVA_HOME="/path/to/jdk-21"
java -version
设备是否支持智感握姿还要单独验证。API 版本满足要求,只说明工程可以编译和安装,不能替代硬件能力检查。
1.3 创建 OpenHarmony 示例目录
示例页面没有把系统能力伪装成普通列表,而是围绕“当前状态、状态码、事件序号和监听生命周期”组织:
标题区 智感握姿 / KMP 公共模型 + OpenHarmony 多模态感知
状态区 左手握持、状态码 1、事件序号 8
预览区 未握持 / 左手 / 右手 / 双手
监听区 开始监听 / 停止监听
自检区 6/6 KMP 自检通过
预览按钮不访问硬件,用于在不支持该能力的设备上验证 KMP 和 N-API。只有点击“开始监听”时,ArkTS 才调用系统 motion.on。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
公共模块同时保留 JVM 测试和 OpenHarmony Native 目标:
plugins {
kotlin("multiplatform")
`maven-publish`
}
kotlin {
explicitApi()
jvm()
jvmToolchain(21)
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
JVM 目标让状态码规则可以快速测试;ohosArm64 则把同一份 commonMain 代码编译成 OpenHarmony KLIB。
2.2 Native focused build 的作用
example/nativeApp 构建一个受 linker map 约束的 shared library:
kotlin {
ohosArm64 {
binaries.sharedLib {
baseName = "smart_grip"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
链接器只导出四个 C ABI 符号:
SmartGripCatalog
SmartGripGet
SmartGripRunChecks
SmartGripFree
这样 ArkTS 只能通过明确的边界获取快照和自检结果,Kotlin/Native 内部实现不会变成不受控的 ABI。
2.3 配置仓库和独立消费工程
example/settings.gradle.kts 使用 OpenHarmony 社区 Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
共享示例通过项目依赖消费本地 smart-grip:
sourceSets {
commonMain.dependencies {
api(project(":smart-grip"))
}
}
如果使用已经发布的 Maven 产物,业务 KMP 模块可以写成:
commonMain.dependencies {
implementation("com.ohos.smartgrip:smart-grip:1.0.0")
}
2.4 通过构建产物消费共享库
prepareOhos 在 Native 链接成功后复制动态库和 C 头文件:
val prepareOhos by tasks.registering(Copy::class) {
dependsOn("linkDebugSharedOhosArm64")
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libsmart_grip.so")
into("libs/arm64-v8a")
}
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) {
include("libsmart_grip_api.h")
into("src/main/cpp/include")
}
into(rootProject.layout.projectDirectory.dir("ohosApp/entry"))
}
动态库、生成头文件和 HAP 属于构建产物,仓库通过 .gitignore 排除它们;每台开发机都可以从源码重新生成与自身 SDK 匹配的文件。
第 3 阶段:状态与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 不能直接共享 Kotlin 对象,因此边界使用 UTF-8 JSON:
HarmonyOS motion event
↓ integer code
SmartGripMotion.ets
↓ code + sequence
N-API getSnapshot()
↓ C ABI SmartGripGet()
Kotlin SmartGripEngine
↓ JSON snapshot
ArkUI page
页面只消费 JSON,不复制状态码映射。这样 JVM 测试和设备页面使用同一套 SmartGripStatus.fromCode 规则。
3.2 SmartGripStatus 和 SmartGripSnapshot
公共状态模型位于 smart-grip/src/commonMain:
public enum class SmartGripStatus(
public val code: Int,
public val displayName: String,
) {
NONE(0, "未握持"),
LEFT(1, "左手握持"),
RIGHT(2, "右手握持"),
BOTH(3, "双手握持"),
UNKNOWN(16, "未识别");
public companion object {
public fun fromCode(code: Int): SmartGripStatus = when (code) {
0 -> NONE
1 -> LEFT
2 -> RIGHT
3 -> BOTH
else -> UNKNOWN
}
}
}
public data class SmartGripSnapshot(
val status: SmartGripStatus,
val sequence: Long = 0,
)
sequence 用来确认状态更新确实经过共享模型。每次收到新系统事件,SmartGripEngine.reduce 生成一个新快照,不修改原对象。
3.3 Engine reducer 和 JSON
public object SmartGripEngine {
public fun snapshot(code: Int, sequence: Long = 0): SmartGripSnapshot =
SmartGripSnapshot(SmartGripStatus.fromCode(code), sequence)
public fun reduce(
previous: SmartGripSnapshot,
code: Int,
): SmartGripSnapshot {
require(previous.sequence < Long.MAX_VALUE)
return snapshot(code, previous.sequence + 1)
}
}
JSON 边界保持字段稳定:
{
"code": 1,
"status": "LEFT",
"displayName": "左手握持",
"sequence": 8
}
所有字符串经过 jsonQuote 处理,避免状态名称、错误信息或未来扩展字段破坏 JSON。
3.4 自检和错误边界
SmartGripEngine.runChecks() 覆盖六项检查:
- 五种状态都存在;
- 状态码唯一;
- 左手状态码映射正确;
- 未知状态码安全归一;
- reducer 会推进序号;
- 快照暴露规范化状态码。
Native 异常会转换为 {"error":"..."},C++ 在创建 ArkTS 字符串后立即调用 SmartGripFree。这样页面能显示可读错误,同时不会泄漏 Kotlin/Native 堆内存。
第 4 阶段:原生桥接(技术难点)
4.1 Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 SmartGripSnapshot 和 SmartGripStatus 属于 Kotlin 运行时对象。ArkTS 只能接收 JavaScript 值,C++ 不能把 Kotlin 对象地址直接当成 JavaScript 对象。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ SmartGripEngine
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 只导出整数 | 实现简单 | 页面会重新实现状态映射 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写完整模型 | 页面调用简单 | KMP 和 ArkTS 逻辑容易分叉 | ❌ |
4.3 Kotlin/Native 导出函数
@CName("SmartGripCatalog")
public fun catalogNative(): CPointer<ByteVar> = response {
SmartGripExamples.catalog().joinToString(prefix = "[", postfix = "]") { it.toJson() }
}
@CName("SmartGripGet")
public fun statusNative(code: Int, sequence: Int): CPointer<ByteVar> = response {
require(sequence >= 0)
SmartGripExamples.snapshot(code, sequence.toLong()).toJson()
}
@CName("SmartGripRunChecks")
public fun checksNative(): CPointer<ByteVar> = response { ... }
@CName("SmartGripFree")
public fun freeNative(pointer: CPointer<ByteVar>?) {
if (pointer != null) nativeHeap.free(pointer)
}
返回值使用 Native heap 分配的、以 0 结尾的 C 字符串。每一块返回缓冲区都由 SmartGripFree 释放。
4.4 C++ N-API 方法分发
C++ 注册三个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getSnapshot", nullptr, Snapshot, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getSnapshot 会检查参数数量、整数类型、非负范围,再调用 SmartGripGet。快照和自检都遵循“调用 Native、创建 ArkTS 字符串、释放 Native 缓冲区”的顺序。
CMake 将 Kotlin/Native 动态库作为 imported library:
add_library(smart_grip SHARED IMPORTED)
set_target_properties(smart_grip PROPERTIES
IMPORTED_LOCATION
"${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libsmart_grip.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE smart_grip libace_napi.z.so)
4.5 N-API 生命周期
ArkTS getSnapshot(code, sequence)
│
▼
ReadNumber + argument check
│
▼
SmartGripGet(code, sequence)
│
▼
napi_create_string_utf8(...)
│
▼
SmartGripFree(nativeBuffer)
│
▼
return JS string
C++ 负责完成跨语言字符串转换和 Native 释放,ArkTS 页面不需要知道 Kotlin/Native 的堆实现。
第 5 阶段:系统能力封装
5.1 ArkTS 调用 Multimodal Awareness Kit
系统监听封装在 SmartGripMotion.ets:
import { motion } from '@kit.MultimodalAwarenessKit';
start(listener: SmartGripStatusListener): void {
this.listener = listener;
if (this.callback !== null) return;
this.callback = (data: motion.HoldingHandStatus): void => {
const code = Number(data);
this.listener?.(Number.isInteger(code) ? code : 16);
};
motion.on('holdingHandChanged', this.callback);
}
stop(): void {
if (this.callback === null) return;
motion.off('holdingHandChanged');
this.callback = null;
this.listener = null;
}
start 具有幂等保护,重复点击不会创建第二个回调;stop 同时调用 motion.off 并清空引用,下一次订阅可以重新建立监听。
5.2 权限声明
宿主 entry/src/main/module.json5 声明:
"requestPermissions": [
{
"name": "ohos.permission.DETECT_GESTURE",
"reason": "$string:detect_gesture_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.ACTIVITY_MOTION",
"reason": "$string:activity_motion_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
权限只声明真实使用场景。系统能力仍然由目标设备决定,权限通过不代表设备一定返回握姿事件。
5.3 ArkUI 页面状态
Index.ets 保存 KMP 快照、监听开关、自检结果和错误文本:
@State private snapshot: SmartGripSnapshot = emptySnapshot();
@State private listening: boolean = false;
@State private checkStatus: string = '正在自检';
@State private errorText: string = '';
private motion: SmartGripMotion = new SmartGripMotion();
开始监听时,ArkTS 把系统整数码交给 N-API 的 getSnapshot;页面不写 if (code === 1) 这样的业务映射。停止页面时调用 SmartGripMotion.stop(),避免 Ability 退出后系统仍保留回调。
5.4 页面交互预设
页面提供四个预览按钮:
- 未握持:验证状态码
0; - 左手:验证状态码
1; - 右手:验证状态码
2; - 双手:验证状态码
3。
预览按钮只走 KMP/N-API,不访问硬件。点击“开始监听”后,按钮变为“停止监听”,系统回调会刷新状态和事件序号。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── shared/
│ ├── src/commonMain/.../SmartGripExamples.kt
│ └── src/commonTest/.../SmartGripExamplesTest.kt
├── nativeApp/
│ ├── src/ohosArm64Main/.../NativeBridge.kt
│ └── src/ohosArm64Main/linker/shared-library.map
└── ohosApp/
├── AppScope/
├── entry/src/main/cpp/
│ ├── CMakeLists.txt
│ └── napi_init.cpp
└── entry/src/main/ets/
├── pages/Index.ets
└── smartgrip/SmartGripMotion.ets
shared 验证公共模型,nativeApp 产生 Native 动态库,ohosApp 负责 ArkUI 页面和系统 API。三者的边界清晰,任何一层失败都能单独定位。
6.2 原生模块注册
napi_init.cpp 通过 napi_module_register 注册 entry 模块,类型声明位于:
example/ohosApp/entry/src/main/cpp/types/libentry/index.d.ts
ArkTS 使用:
import { getSnapshot, runChecks } from 'libentry.so';
6.3 Native 动态库准备
执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
脚本依次执行根模块测试、示例测试、linkDebugSharedOhosArm64 和 prepareOhos。成功后生成:
example/ohosApp/entry/libs/arm64-v8a/libsmart_grip.so
example/ohosApp/entry/src/main/cpp/include/libsmart_grip_api.h
还可以检查 ARM64 ELF 的强依赖:
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
6.4 构建、签名和安装
未配置签名时,可以在 DevEco Studio 构建未签名 HAP;真机安装需要签名 HAP。配置签名后执行:
./scripts/build-hap.sh /path/to/example/ohosApp
产物位于:
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
安装并启动:
hdc list targets -v
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.smartgrip.sample
签名证书、profile、p12 和密码只保存在本机,不要提交到 AtomGit。
四、完整代码对照
4.1 整体架构
HarmonyOS motion
│ holdingHandChanged
▼
SmartGripMotion.ets
│ normalized integer code
▼
Index.ets -> libentry.so
│ N-API
▼
SmartGripGet / SmartGripRunChecks
│ C ABI
▼
libsmart_grip.so
│ Kotlin/Native
▼
SmartGripEngine -> SmartGripSnapshot -> JSON
4.2 文件清单
| 文件 | 职责 |
|---|---|
smart-grip/SmartGrip.kt | 状态枚举、快照和业务门面 |
smart-grip/SmartGripEngine.kt | 状态归约、目录和自检 |
smart-grip/SmartGripJson.kt | JSON 编码和字符串转义 |
example/nativeApp/NativeBridge.kt | C ABI、JSON 返回和内存释放 |
example/ohosApp/.../SmartGripMotion.ets | motion 订阅与取消订阅 |
example/ohosApp/.../SmartGripClient.ets | N-API JSON 解析和校验 |
example/ohosApp/.../Index.ets | 真机预览页面 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | N-API 导出和参数检查 |
scripts/build-openharmony.sh | 测试、Native 链接和产物复制 |
scripts/build-hap.sh | 已配置签名工程的 HAP 构建 |
docs/openharmony/VALIDATION.md | 自动检查和真机验收记录 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Kotlin | SmartGripStatus.fromCode | 整数码转枚举 |
| Kotlin | SmartGripEngine.snapshot | 创建快照 |
| Kotlin | SmartGripEngine.reduce | 接收新事件并推进序号 |
| Native | SmartGripGet | 返回一个 JSON 快照 |
| N-API | getSnapshot | 向 ArkTS 暴露快照方法 |
| ArkTS | SmartGripMotion.start | 订阅系统事件 |
| ArkTS | SmartGripMotion.stop | 取消系统事件 |
4.4 ArkTS 与 Kotlin 的边界
ArkTS 只负责系统能力和界面生命周期:
this.motion.start((code: number): void => {
this.snapshot = readSnapshot(code, this.snapshot.sequence + 1);
});
Kotlin 负责状态含义和不可变数据:
val next = SmartGripEngine.reduce(previous, code)
两者之间只传输整数和 JSON,不传输 Kotlin 对象、ArkTS class 实例或未校验的动态结构。
五、关键决策说明
决策 1:把 ohosArm64 加入公共构建约定
智感握姿的系统事件发生在 ARM64 设备上,只有真正链接 ohosArm64 动态库,才能证明共享 Kotlin 代码可以进入 OpenHarmony 运行时。
决策 2:独立消费者必须通过构建产物消费
example 单独解析 smart-grip,ohosApp 单独接收 .so 和头文件,避免根工程编译通过却无法在 DevEco 中打包。
决策 3:JSON 作为跨语言数据契约
JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加可信字段而不暴露内部对象布局。
决策 4:桥接层只开放四个 C ABI 入口
目录、快照、自检和释放已经覆盖示例所需能力;减少 ABI 符号可以降低 Native 生命周期和兼容风险。
决策 5:预览状态和真实事件分开
预览按钮用于验证 KMP/N-API,系统监听用于验证硬件能力。两者分开后,不支持智感握姿的设备仍然可以完成共享模型验收。
决策 6:把库验证和设备验证分开
JVM 测试验证状态规则,Native 链接验证 ABI,Hvigor 验证 HAP,真机验证 motion.on/off 和设备回调。每一层都有明确的失败边界。
六、测试与验证
6.1 测试环境
本次真机验证使用:
- macOS;
- JDK 21;
- Kotlin Multiplatform
2.2.21-1.0.0; - DevEco Studio 及 OpenHarmony ARM64 Native SDK;
- 已签名
entry-default-signed.hap; - USB 连接的 HarmonyOS ARM64 真机;
hdc设备序列号FMR0223825079397。
6.2 静态检查与单元测试
./gradlew :smart-grip:jvmTest :sample:shared:jvmTest
(cd example && ./gradlew :shared:jvmTest)
测试覆盖状态码唯一性、左手映射、未知值归一、不可变归约、JSON 字段和六项公共自检。
6.3 原生桥接和 HAP 验证
./scripts/build-openharmony.sh
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
./scripts/build-hap.sh example/ohosApp
验证结果:
libsmart_grip.so: 0 unresolved strong imports
Hvigor BUILD SUCCESSFUL
entry-default-signed.hap generated
Hvigor 仍会提示 motion 并非所有设备都支持,这是系统能力提示,不是工程编译错误。
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后页面显示“智感握姿”和 6/6 KMP 自检通过。初始状态由 Native getSnapshot(0, 0) 生成。
用例 2:四种本地预览
点击未握持、左手、右手和双手按钮,页面分别显示状态码 0/1/2/3,事件序号每次递增。这个用例不依赖设备硬件。
用例 3:开始监听
点击“开始监听”,按钮变为“停止监听”。ArkTS 通过 motion.on('holdingHandChanged') 建立系统订阅,回调中的整数码再次经过 KMP JSON 链路。
用例 4:改变握持方式
用左手、右手和双手改变设备握持方式,确认页面状态名称、状态码和事件序号变化。截图中的 左手握持 / 状态码 1 / 事件序号 8 是本次真机效果。
用例 5:重复订阅和停止监听
重复点击停止和开始,确认不会创建多个回调;离开页面后 aboutToDisappear 会调用 motion.off。
用例 6:不支持设备的错误处理
如果设备没有开放该能力,页面显示订阅失败文本,但本地预览和 KMP 自检仍然可用。
6.5 验证结论
自动测试、Kotlin/Native ARM64 链接、ELF 依赖检查、Hvigor HAP 构建、签名安装和真机页面自检均已完成。真机已经显示实际握姿状态,说明从系统事件到 KMP 状态模型的链路可用。
七、运行效果
7.1 真机截图

截图中可以看到:
- 页面标题为“智感握姿”;
- 副标题说明 KMP 公共模型和 OpenHarmony 多模态感知;
- 当前状态为“左手握持”;
- 状态码为
1,符合系统约定; - 事件序号为
8; - 监听按钮显示“停止监听”;
- KMP 六项自检全部通过。
7.2 命令速查
# 根工程测试和 OpenHarmony Native 准备
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
# 单独运行共享测试
(cd example && ./gradlew :shared:jvmTest)
# 设备依赖检查
python3 scripts/check-native-deps.py \
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native \
example/ohosApp/entry/libs/arm64-v8a
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh example/ohosApp
# 安装和启动
hdc -t <设备序列号> install -r \
example/ohosApp/entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b com.ohos.smartgrip.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配:必须把共享模型真正编译成
ohosArm64动态库。 - N-API 不负责业务判断:C++ 只做类型检查、字符串转换和释放,状态语义放在 Kotlin。
- motion 事件必须成对管理:
motion.on后要在停止监听和页面销毁时调用motion.off。 - 权限和设备能力是两件事:声明权限只能满足访问条件,不能保证设备返回握姿结果。
- 签名配置需要绑定产品:
products[].signingConfig必须指向signingConfigs,否则 Hvigor 会继续生成未签名 HAP。
8.2 已知问题
- 智感握姿依赖系统版本、设备硬件和产品能力,部分设备只支持预览而不能产生实时事件;
- 系统会对
motionAPI 输出“并非所有设备支持”的编译警告,这是预期提示; - 文章中的签名配置只适用于本地开发机,不能直接复制到其他环境;
- 当前示例是单页面监听模型,多页面应用需要在业务层集中管理订阅。
8.3 未来优化方向
- 增加跨平台
expect/actual监听接口,让 Android 或桌面端可以提供模拟事件; - 将状态事件封装为
Flow<SmartGripSnapshot>,减少业务层回调管理; - 为 Compose Multiplatform 页面提供状态卡片和左右手布局示例;
- 增加设备能力探测和权限状态提示;
- 在持续集成中加入 Native 链接和 HAP 未签名构建任务。
九、总结
9.1 核心难点回顾
本次适配真正需要处理的不是一个 motion.on 调用,而是一条完整跨端链路:
OpenHarmony motion
→ ArkTS 生命周期和权限
→ C++ N-API
→ Kotlin/Native C ABI
→ KMP 状态模型
→ JSON
→ ArkUI 真机页面
9.2 封装层次
- KMP 层:定义稳定的状态、序号、归约和 JSON;
- Native 层:生成 ARM64 动态库,并通过 C ABI 输出有限入口;
- N-API 层:完成参数检查、字符串转换和内存释放;
- ArkTS 层:管理系统订阅、页面生命周期、权限错误和视觉展示;
- DevEco 层:完成 CMake、HAP、签名、安装和运行。
9.3 三条经验
- 先让共享模型在 JVM 和 Native 通过,再接入 ArkUI;
- 用 JSON 和少量 C ABI 代替跨语言对象传递;
- 把自动测试、HAP 构建和真机事件分别记录,避免把“编译成功”误认为“设备能力可用”。
9.4 适配成果
当前 kmp-smart-grip 已完成:
SmartGripStatus五状态公共模型;- 未知状态码安全归一;
- JVM 和 OpenHarmony ARM64 共用的自检逻辑;
- Kotlin/Native + C ABI + N-API 桥接;
motion.on/off('holdingHandChanged')真机监听;DETECT_GESTURE、ACTIVITY_MOTION权限声明;- 签名 HAP 构建、设备安装和左手握持效果图;
- 与参考 CMP 工程一致的模块和脚本组织;
- AtomGit 项目文档和 OpenHarmony 验收记录。
参考文档
更多推荐


所有评论(0)