开源鸿蒙平台 KMP 三方库「multiplatform-settings」适配全流程
本文记录
multiplatform-settings接入 OpenHarmony 的完整过程,覆盖键值存储库盘点、系统 C API(OH_Preferences)的 cinterop 绑定、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 页面、HAP 构建和真机验收。
multiplatform-settings是 Kotlin 社区事实标准的跨平台键值存储库:一套Settings接口覆盖Int/Long/Float/Double/Boolean/String六种类型的put/get/getOrNull,加上hasKey/remove/clear/keys/size与Settings.Factory,业务代码写在commonMain,Android / iOS / 桌面各自出平台 actual。本次适配与社区早期路线(qrose / haze 的"零依赖内核提取"、KStore 的"kotlinx 制品线真编译")又不一样:这个库commonMain零第三方依赖,适配难点不在依赖矩阵,而在 actual 必须直连系统库——OhosPreferencesSettings通过手写精简 C 头把OH_Preferences_*公开 C API 绑进 cinterop,链接期-lohpreferences、运行期从设备/system/lib64动态解析,一条"带系统能力"的 KMP 适配链路完整跑通,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/multiplatform-settings.git
上游仓库: russhwolf/multiplatform-settings
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP 适配
multiplatform-settings 为 Kotlin Multiplatform 应用提供"键值 ↔ 持久化"这一件事的标准答案:Settings 接口是全部门面,putInt / getStringOrNull / hasKey / remove / clear 覆盖键值生命周期,Settings.Factory 按平台注入构造器,Delegates.kt 还提供属性委托语法糖。Android 走 SharedPreferences,Apple 走 NSUserDefaults,JVM 走 java.util.Properties / Preferences。OpenHarmony 应用同样需要这套能力,但上游构建(Kotlin 2.1.20,一长串 Android / Apple / JS / wasm / mingw target)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共键值 API 在 OpenHarmony ARM64 设备上完成编译、链接和落盘。适配需要同时验证 Settings 接口语义、C API 绑定、类型映射、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.1.20 / 多 target 构建与鸿蒙工具链无法共存于一个 Gradle 构建,cinterop commonization 会把 ohos 和 linux 搅在一起。 |
| C API 绑定难 | SDK 的 oh_preferences.h 带 __availability__ 属性并 #include <napi/native_api.h>,直接喂 cinterop 会撑爆 clang 前端。 |
| 类型不齐 | OH_Preferences 只有 SetInt / SetBool / SetString 三个 setter,Long / Float / Double 没有原生落点。 |
| 键枚举缺失 | C 侧 GetAll 走 PreferencesValue union,绑定成本高,keys / size 需要替代方案。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:Settings 接口的 ohos 编译、根构建替换(cmp_qrose 模式)、OH_Preferences 的 cinterop 绑定与 C ABI/N-API 桥接层。键值语义完全由上游接口约定,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的键值存储会话,对应桥接的全部入口:
- 初始化(init):按
bundleName+ 文件名建Factory并打开 store,页面启动时自动完成; - 写入(put):六种类型全走
Settings.putX,值以字符串过桥、Native 侧再 parse; - 读取(get):
getXOrNull语义,缺 key 时found=false、value=null,类型不匹配容忍为null; - 查询(has):
OH_Preferences_HasKey,Clear 后再查返回false; - 删除(remove)与清空(clear):
remove删单键并更新索引,clear按索引逐个删除; - 快照(snapshot):返回全部键值对及推断类型,覆盖
keys/size; - 持久化闭环(reopen):重新
Factory.create()打开同一文件,Seed 后数据仍在——证明是 Flush 落盘而不是内存缓存; - 一键验收(seed):一次写入六种类型六条数据(Demo 快捷方式,非上游 API)。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / size | 回显本次操作与当前键数。 |
found / value | get 的读取结果;缺 key 时 value=null。 |
hasKey | has 的查询结果。 |
keys / items | snapshot 的键列表与带类型的键值明细。 |
接口侧完整能力(六类型读写、默认值、getOrNull null 语义、属性委托)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 jvmTest 交叉验证用例(上游 PropertiesSettings 一个字未改),证明 ohos .so 里执行的键值逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | commonMain 的 Settings.kt / Delegates.kt / Operators.kt 原样编译,不提取、不重写;新增的只有 ohosMain 一个 actual 与两份 cinterop 描述文件。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);coroutines / serialization / no-arg / datastore / make-observable 五个扩展 artifact 留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 本库 commonMain 零第三方依赖,无需 kotlinx 制品线,工具链直接用社区标准线 Kotlin 2.2.21-1.0.0。 |
| UI 完整 | 单页覆盖全部桥接入口:store 输入、键值输入、六类型选择、九个操作按钮、size 状态行与键值列表。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
multiplatform-settings本身不碰 UI,适配面天然最小;ArkTS Demo 只是把桥暴露的键值闭环画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认零依赖、确定交付物
第 2 阶段:目标与工具链打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版
第 3 阶段:C API 绑定与 actual ── prefs_min.h、cinterop、类型映射、sidecar 索引、短事务
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── bundleName 注入、六类型选择、九操作按钮、键值列表
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
multiplatform-settings/ 上游核心库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
multiplatform-settings/src/ohosMain/ 新增:cinterop(prefs_min.h / ohpreferences.def)+ OhosPreferencesSettings
multiplatform-settings/src/jvmMain/ 上游 PropertiesSettings / PreferencesSettings(交叉验证宿主)
multiplatform-settings/src/jvmTest/ 新增:JvmSmokeTest(round-trip 冒烟)
coroutines/ serialization/ no-arg/ 上游扩展 artifact,留盘不进 settings
datastore/ make-observable/ test/
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample/ tests/ convention-plugins/ 上游示例与共享测试,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :multiplatform-settings 与 :example:nativeApp,rootProject.name = "ohos_multiplatform-settings"。上游的 convention-plugins(standard-configuration 约约插件)、55 行版本目录、多 target 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 随根构建替换重新生成) |
| sysroot | DevEco SDK native/sysroot(ohos.native.sysroot 属性注入,链接 -lohpreferences) |
| DevEco product | 5.1.0(18) 兼容 / API 26 实测 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.settings |
export JAVA_HOME="/path/to/jdk-21"
java -version
注意:gradle.properties 里的 org.gradle.java.home 要等 Gradle 起来才生效,wrapper 自己启动 JVM 仍然需要先设 JAVA_HOME,否则 gradlew 直接报 JAVA_HOME is not set。
1.3 源码依赖度盘点
| 模块 | 形态 | 依赖 | 处置 |
|---|---|---|---|
multiplatform-settings | Settings 接口 + 各平台 actual | 纯 Kotlin,零第三方依赖,零 UI | 整体编译(jvm + 双 ohos + 新增 ohos actual) |
coroutines / serialization / no-arg / datastore / make-observable | 独立 artifact | coroutines / serialization / AndroidX DataStore | 留盘不进构建(P1 先跑核心) |
sample / tests / convention-plugins | 上游示例与共享测试 | 自带独立 Gradle 构建 | 留盘不进构建 |
盘点结论是本次路线的起点:commonMain 一个第三方依赖都没有,Settings.kt 里没有 expect 声明(平台差异走接口 + 工厂,不走 expect/actual),所以既不用像 qrose 那样抽内核,也不用像 KStore 那样验证 kotlinx 制品线——ohosArm64() / ohosX64() 加上就能编。真正的工作量全部在平台 actual:把 OH_Preferences 的公开 C API 绑进 Kotlin/Native。
1.4 页面边界
标题区 multiplatform-settings · OpenHarmony / 副标题(KMP Settings + OH_Preferences C API)
状态行 最近一次操作结果(如 "seed size=6 store=demo")
store 区 文件名输入框(默认 demo)+ Init / Reopen 两按钮
键值区 key 输入框(默认 nickname)+ value 输入框(默认 OpenHarmony)
类型区 string / int / long / float / double / boolean 六个选择 chip
操作区 Put / Get / Has / Remove + Snapshot / Seed 6 types / Clear 七按钮
统计行 size=N has=...(缺 key 时 found=false)
列表区 键值明细列表(键名 + 推断类型 + 值,滚动)
页面启动时先 bundleManager.getBundleInfoForSelfSync(0).name 取运行中包名,再自动发起 init,避免 bundleName 手写分叉。
第 2 阶段:目标与工具链打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0)
社区此前批准的标准钉法就是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze / qrcode-kotlin 用的同一条线)。KStore 后来升到 2.4.20-0.1.0-07,是因为它的 coroutines 制品线基于 stdlib 2.4.20 构建,旧线读不了新元数据——工具链版本跟着依赖走。本库 commonMain 零依赖、jvmTest 只依赖 kotlin("test"),没有任何制品把版本顶上去,那就直接落回社区标准线 2.2.21-1.0.0: Gradle 相应钉 8.14.1(2.2.21 线的社区标配),不追新不折腾。
settings.gradle.kts 把 eazytec 双仓写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
google()
mavenCentral()
}
}
rootProject.name = "ohos_multiplatform-settings"
include(":multiplatform-settings")
include(":example:nativeApp")
ohosArm64() 这个函数只存在于 HarmonyOS 定制 Kotlin Gradle 插件,官方 JetBrains Kotlin 没有它。插件解析失败时,先看仓库顺序,再看版本号,不要去 JetBrains 文档里找 ohos target。
2.2 根构建替换与库模块
根 build.gradle.kts 收敛为版本声明 + ohos 测试任务禁用:
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}
allprojects {
group = "com.russhwolf"
version = providers.gradleProperty("version").orElse("1.3.0-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false } // ohos 侧测试由 ArkTS Demo 当 L4 验收
}
库模块 multiplatform-settings/build.gradle.kts 声明三个目标与 cinterop:
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
val sysroot = (project.findProperty("ohos.native.sysroot") as String).replace("\\", "/")
val cinteropInclude = file("src/ohosMain/cinterop")
listOf(ohosArm64(), ohosX64()).forEach { target ->
target.compilations.getByName("main") {
cinterops.create("ohpreferences") {
defFile(cinteropInclude.resolve("ohpreferences.def"))
includeDirs(cinteropInclude)
compilerOpts("-I$cinteropInclude")
}
}
}
}
实测:
./gradlew :multiplatform-settings:compileKotlinOhosArm64 :multiplatform-settings:compileKotlinOhosX64 # BUILD SUCCESSFUL
Settings 接口一个字没动,新增的只有 ohosMain 下的 actual 与 cinterop 文件。
第 3 阶段:C API 绑定与 actual(技术难点)
3.1 为什么不能直接喂 SDK 头
第一反应是把 SDK 的 oh_preferences.h 丢给 cinterop,编不过,原因很具体:其一,头文件用了 __attribute__((__availability__(ohos, introduced=13))) 这一套,cinterop 的 clang 前端认不全;其二,它 #include <napi/native_api.h>、oh_preferences_value.h,会把 NAPI 和 PreferencesValue 整棵树拉进来,绑定表面积突然变成"半个 ArkTS 运行时";其三,真正要用的只有十几个函数。处置是手抄一份精简头:
// src/ohosMain/cinterop/prefs_min.h(节选)
struct OH_Preferences;
struct OH_PreferencesOption;
struct OH_PreferencesOption* OH_PreferencesOption_Create(void);
void OH_PreferencesOption_SetBundleName(struct OH_PreferencesOption*, const char*);
void OH_PreferencesOption_SetFileName(struct OH_PreferencesOption*, const char*);
void OH_PreferencesOption_SetDataGroupId(struct OH_PreferencesOption*, const char*);
void OH_PreferencesOption_Destroy(struct OH_PreferencesOption*);
struct OH_Preferences* OH_Preferences_Open(const struct OH_PreferencesOption*);
void OH_Preferences_Close(struct OH_Preferences*);
int OH_Preferences_GetInt(const struct OH_Preferences*, const char*, int*);
int OH_Preferences_SetInt(const struct OH_Preferences*, const char*, int);
int OH_Preferences_HasKey(const struct OH_Preferences*, const char*, bool*);
int OH_Preferences_Flush(const struct OH_Preferences*);
/* GetBool / GetString / SetBool / SetString / Delete / FreeString 同款手抄 */
# src/ohosMain/cinterop/ohpreferences.def
headers = prefs_min.h
package = ohos.preferences
compilerOpts = -I.
3.2 cinterop 的 Kotlin 视图不是头文件的镜像
绑定生成后,Kotlin 侧看到的不是 ohos.preferences.OH_Preferences 这个类:函数在包 ohos.preferences,不透明结构体在 cnames.structs.OH_Preferences,bool * 映射成 kotlinx.cinterop.BooleanVar 而不是 platform.posix.boolVar。第一版 actual 按头文件里的名字原样 import,编译器直接报 Unresolved reference: OH_Preferences 和 Unresolved reference: boolVar。修法是用 klib dump-metadata-signatures 看一眼生成物再改 import——cinterop 的 Kotlin 视图和 C 头不是一一对应,不要猜。
3.3 类型映射决策
OH_Preferences 的 setter 只有三个,类型映射是本适配唯一需要拍板的语义问题:
| Settings 调用 | 落盘方式 | 原因 |
|---|---|---|
putInt / getInt | SetInt / GetInt | C API 原生支持 |
putBoolean / getBoolean | SetBool / GetBool | C API 原生支持 |
putString / getString | SetString / GetString | GetString 返回的堆内存必须 OH_Preferences_FreeString |
putLong / putFloat / putDouble | SetString 存十进制文本 | C API 没有 int64 / float / double setter |
读侧 getLongOrNull 先按字符串 parse,失败再 getIntOrNull 再 widen——"以前用 int 存的计数器,后来改成 long 读"不会直接崩。还有一个只有运行时才会暴露的坑:GetString 撞上 count 这种 Int 键,返回码不是 KEY_NOT_FOUND 而是类型不匹配,第一版把"非 OK 且非 NOT_FOUND"直接 error(),结果 Snapshot 遍历到第一颗 Int 键就抛异常,列表只剩 2 个键。修法:类型对不上就当 null,让 Snapshot 继续用 GetInt / GetBool 认类型。
3.4 keys / size 的 sidecar 索引
C 侧 OH_Preferences_GetAll 走 PreferencesValue union 数组,cinterop 成本高,这轮不绑。折中是写一个 sidecar 键 __ohos_settings_index,值为换行分隔的 key 列表:put* 时入集,remove / clear 时同步更新,对调用方不可见。限制写进文档:绕过本类直接改文件的写入会让索引漂移——Demo 与常规 KMP 业务都走同一个 Settings 实例,不会踩。
3.5 每调用一次短事务
actual 的每次读写都走同一条短事务:
// OhosPreferencesSettings.kt(骨架)
private inline fun <T> withStore(block: (CPointer<OH_Preferences>) -> T): T {
val option = checkNotNull(OH_PreferencesOption_Create()) { "option create failed" }
OH_PreferencesOption_SetBundleName(option, bundleName)
OH_PreferencesOption_SetFileName(option, fileName)
OH_PreferencesOption_SetDataGroupId(option, "")
val store = checkNotNull(OH_Preferences_Open(option)) { "open failed bundle=$bundleName file=$fileName" }
try {
return block(store)
} finally {
checkOk(OH_Preferences_Flush(store), "flush")
OH_Preferences_Close(store)
OH_PreferencesOption_Destroy(option)
}
}
把 Open/Close 放在每次调用里而不是进程级单例,是为了 Demo 的 Reopen 按钮有意义:点 Reopen 等于重新 Factory.create(),如果上一次没 Flush,新句柄读到的就是空。不 Flush,Reopen 后 size=0;Seed 后点 Reopen 仍是 6,说明真的落盘了。bundleName 必须与 HAP 的 app.json5 一致,写错时 Open 返回空指针——Demo 启动时运行时取自身包名,避免手写分叉。
链接期,nativeApp 的 sharedLib 必须加 sysroot 与系统库(不加的话 OH_Preferences_Open 以 UND 留在 so 里,运行时 dlopen 找不到符号):
ohosArm64 {
binaries.sharedLib {
baseName = "ohossettings"
linkerOpts("-L$sysroot/usr/lib/aarch64-linux-ohos", "-lohpreferences")
}
}
libohpreferences.so 在设备 /system/lib64,不打进 HAP;llvm-readelf -d 里能看到这些符号是 UND FUNC,运行时由系统库提供。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libsettings_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohossettings.so)
│ SettingsCall
▼
OH_Preferences C API(系统库 libohpreferences.so)
HarmonyOS Kotlin 的 CAdapter 会给 sharedLib 生成 libohossettings_api.h 与 libohossettings_symbols(),但不会把业务 @CName 挂到 ArkTS 的 NAPI 模块表——ELF 里看得到 SettingsCall,ArkTS 运行时 import 不到(qrose / qrcode-kotlin 同款结论)。所以仍然要 CMake 再包一层 NAPI。
4.2 Kotlin/Native 导出函数
@OptIn(ExperimentalNativeApi::class)
@CName("SettingsCall")
fun settingsCall(request: String): String // init/put/get/has/remove/clear/snapshot/reopen/seed 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SettingsCall | SettingsCall(req) | Factory 构造 + 六类型 put/get + hasKey / remove / clear / keys / size / reopen / seed |
JSON 解析是手写的极简扫描(只认字符串字段),故意不拉 kotlinx.serialization——KN 侧多一个依赖会把 ohos 目标的依赖图重新打乱,这个库的卖点就是零依赖。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"SettingsCall", nullptr, NapiSettingsCall, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossettings_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。
4.4 CMake 和类型声明
add_library(ohossettings SHARED IMPORTED)
set_target_properties(ohossettings PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(settings_napi SHARED napi_init.cpp)
target_link_libraries(settings_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohossettings)
OHOS_ARCH 为空或 libohossettings.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++_shared.so 三件套同目录,dlopen 时互相可见。KN so 的 DT_NEEDED 同时含 libc++_shared.so(要打进 HAP)和 libohpreferences.so(设备 sysroot 提供)。
export const SettingsCall: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 注入 bundleName 和触发操作
aboutToAppear(): void {
this.bundleName = bundleManager.getBundleInfoForSelfSync(0).name; // 运行时取真实包名
this.call({ op: 'init', bundleName: this.bundleName, name: this.storeName });
}
private call(payload: CallReq): void {
if (this.busy) { return; }
this.busy = true;
try {
const raw = SettingsCall(JSON.stringify(payload)); // 进 Kotlin/Native
const parsed = JSON.parse(raw) as CallOk;
if (!parsed.ok) {
this.status = `失败: ${parsed.error ?? raw}`; // 错误不吞,直接上屏
return;
}
this.kvCount = parsed.size ?? this.kvCount;
if (parsed.items) { this.items = parsed.items; }
this.status = `${parsed.op ?? 'ok'} size=${this.kvCount} store=${this.storeName}`;
} finally {
this.busy = false;
}
}
5.2 页面交互
单页自上而下:store 输入框 + Init / Reopen、key / value 输入框、六个类型选择 chip(选中反色)、Put / Get / Has / Remove 与 Snapshot / Seed 6 types / Clear 两排按钮、size=N has=... 统计行、键值明细列表(键名加粗、类型 = 值 副行)。seed 一键写入 nickname / count / big / ratio / pi / on 六条六类型,截图验收不依赖输入法。
5.3 对比度和错误边界
主操作 Put 蓝、Get 蓝、Has 紫、Remove 棕、Snapshot 青、Seed 深蓝、Clear 红,破坏性语义与颜色对齐;ok:false 时错误原文进状态行。@State 变量不能叫 size——和 CustomComponent.size 冲突,ArkTS 严格模式编译期直接红,改名 kvCount。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohossettings)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName("SettingsCall") + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.settings、versionName 1.3.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/libohossettings_api.h
├── src/main/cpp/types/libsettings_napi/{Index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :multiplatform-settings:jvmTest # 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填)。
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.settings -m entry
四、完整代码对照
4.1 整体架构
com.russhwolf.settings/**(上游接口原样编译)
│ Settings / Settings.Factory / Delegates / Operators
OhosPreferencesSettings(ohosMain actual)
│ OH_Preferences_* via cinterop(prefs_min.h)
│ 类型映射 + sidecar 索引 + Open→Flush→Close 短事务
Bridge.kt + MiniJson
│ SettingsCall
libsettings_napi.so
│ SettingsCall
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec 双仓置前、ohos_multiplatform-settings、扩展 artifact 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版(上游 55 行目录收敛为 8 行) |
build.gradle.kts(根) | 版本 1.3.0-ohos.1、全局禁用 Ohos*Test* 任务 |
multiplatform-settings/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64 + ohpreferences cinterop |
src/ohosMain/cinterop/prefs_min.h / ohpreferences.def | 手写精简 C 头与绑定描述 |
src/ohosMain/.../OhosPreferencesSettings.kt | 核心 actual:类型映射、索引、短事务 |
src/jvmTest/.../JvmSmokeTest.kt | JVM 交叉验证(上游 PropertiesSettings 宿主) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、-lohpreferences、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、九个 op 分发、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(28 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 接口 | Settings.putInt / getIntOrNull | 六类型读写门面(commonMain,未改) |
| 接口 | Settings.hasKey / remove / clear | 键生命周期 |
| 接口 | Settings.keys / size | 键枚举(ohos 走 sidecar 索引) |
| 接口 | Settings.Factory.create(name) | 按文件名开 store |
| actual | OH_Preferences_Open / Flush / Close | 短事务三件套 |
| actual | OH_Preferences_HasKey | API 23+ 的存在性查询 |
| actual | __ohos_settings_index | keys / size 的 sidecar 索引 |
| Native | SettingsCall | C ABI JSON 导出 |
| N-API | SettingsCall(request) | 向 ArkTS 暴露能力 |
| ArkTS | call(payload) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const raw = SettingsCall(JSON.stringify({
op: 'put', type: 'string', key: 'nickname', value: 'OpenHarmony'
}));
// {"ok":true,"op":"put","key":"nickname","type":"string","size":1,...}
const got = SettingsCall(JSON.stringify({ op: 'get', type: 'string', key: 'nickname' }));
// {"ok":true,"found":true,"value":"OpenHarmony",...};缺 key 时 found=false、value=null
val settings = OhosPreferencesSettings.Factory(bundleName).create("demo")
settings.putString("nickname", "OpenHarmony")
settings.putInt("count", 7)
settings.hasKey("nickname") // true
settings.remove("nickname")
settings.clear()
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;init 必须先做,否则所有 op 返回 call init first。
五、关键决策说明
决策 1:走公开 C API OH_Preferences,不绕 ArkTS Preferences
ArkTS @ohos.data.preferences 是给 ArkTS 业务用的:上下文是 Context,API 是 Promise/async。KMP 业务运行在 Kotlin/Native 线程,没有 JS runtime,也不能 await——把 KN 的 putInt 回调到 ArkTS 再写 Preferences,每条配置都要跨一次 NAPI 还要传 Ability 生命周期。OH_Preferences 是给 Native 用的同步 API,正好对上 KN 的调用习惯。边界在 C ABI,不在 UI 层。
决策 2:工具链留社区标准线 2.2.21-1.0.0
KStore 升 2.4.20-0.1.0-07 是被 coroutines 制品的 stdlib 元数据顶上去的;本库零依赖,没有任何制品施压,直接落回社区标准线(qrose / haze / qrcode-kotlin 同款),Gradle 相应钉 8.14.1。工具链版本跟着依赖走——没有依赖,就跟社区已审线走,两条经验不要互相套用。
决策 3:Long / Float / Double 按十进制文本存储
C API 没有 int64 / float / double setter,硬造 union 或拆高低位都是给未来埋雷。文本 round-trip 无损,读侧 getLongOrNull 先 parse 字符串、失败再 fallback getIntOrNull widen,"int 升 long"的旧数据兼容。代价是不能和原生 Int 列混做范围查询——对 Settings 这种 API 够用,限制写进文档。
决策 4:sidecar 索引而不是绑定 GetAll
OH_Preferences_GetAll 走 PreferencesValue union,cinterop 成本高、释放路径复杂。一个换行分隔的索引键就能让 keys / size 工作,put / remove / clear 顺带维护。漂移风险(绕过本类直接改文件)是明确边界而不是隐患——Demo 与常规业务都走同一个 Settings 实例。
决策 5:jvmTest 交叉验证 + 每调用短事务
上游 commonTest 随多 target 裁剪没法保留,换成 jvmTest 冒烟(宿主是上游 PropertiesSettings,一个字未改)——它绿,说明 Settings 接口的行为约定(默认值、null 语义、remove/clear 后的 keys/size)没被适配动过。每调用 Open→op→Flush→Close 的短事务则让 Demo 的 Reopen 天然成为落盘验收:语义最稳,代价是不是热路径——高频场景留待 transaction { } 批量接口。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Eclipse Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / ohpm / hdc)、DevEco 模拟器(x86_64)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :multiplatform-settings:jvmTest
上游 PropertiesSettings(JVM actual,零改动)作宿主:
| 测试类 | 用例 | 钉住什么 |
|---|---|---|
JvmSmokeTest.putGetRoundTrip | 1 | 六类型 put/get 往返、默认值语义 |
JvmSmokeTest.missingAndRemove | 1 | getOrNull 的 null 语义、remove 后 hasKey=false |
用例数不多,但钉的是接口契约本身:ohos actual 实现的就是这份契约,PropertiesSettings 全绿即"公共 API 没有漂"。ohos 落盘语义(Flush、类型映射、索引)由设备端验收覆盖,两层互补。
6.3 HAP 验证
:multiplatform-settings:jvmTest: BUILD SUCCESSFUL
compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SettingsCall / libohossettings_symbols 均为全局 T 符号
llvm-readelf -d: NEEDED 全是短 soname(libohpreferences.so / libc++_shared.so 等,无绝对路径泄漏)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
BuildNativeWithCmake: finished(双 ABI 编出 libsettings_napi.so)
CompileArkTS / PackageHap: finished
install bundle successfully / start ability successfully
链接产物实测:KN so libohossettings.so arm64 约 3.7MB、x86_64 约 3.4MB(debugShared,含 Kotlin/Native 运行时),libc++_shared.so 分别约 1.8MB / 1.7MB;libohpreferences.so 不进 HAP,由设备 sysroot 提供。
6.4 功能用例
- 启动首屏状态行出现
init size=0 store=demo(aboutToAppear自动 init,bundleName 运行时获取); - 点
Seed 6 types,size=6,列表按序显示big/count/nickname/on/pi/ratio六条六类型; - 点 Get(string / nickname),状态行
Get nickname (string) = OpenHarmony; - 点 Has,统计行
has=nickname=true; - 点 Reopen,
size仍为 6——重新Factory.create()后数据仍在,Flush 落盘成立; - 点 Remove,
size=5,列表不再有 nickname; - 点 Clear,
size=0,列表清空; - Clear 后再 Get,
value=null、found=false——缺 key 失败态; - Snapshot 扫到 Int 键
count不抛异常(类型不匹配容忍为 null,用GetInt认类型)。
6.5 验证结论
Settings 接口语义、OH_Preferences C API 绑定、类型映射、sidecar 索引、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的键值会话:六类型读写、存在性查询、删除清空与 Reopen 持久化闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/multiplatform-settings.git
cd multiplatform-settings
export JAVA_HOME="/path/to/jdk-21"
./gradlew :multiplatform-settings:jvmTest # 交叉验证
./gradlew :multiplatform-settings:compileKotlinOhosArm64 :multiplatform-settings:compileKotlinOhosX64 # klib
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
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.settings -m entry
hdc shell hilog | grep SettingsNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:multiplatform-settings真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- SDK 的
oh_preferences.h不能直接喂 cinterop(__availability__属性 +napi/native_api.hinclude 树),必须手写prefs_min.h; - cinterop 的 Kotlin 视图不是 C 头的镜像:不透明结构体在
cnames.structs.OH_Preferences、bool*是kotlinx.cinterop.BooleanVar,用klib dump-metadata-signatures对齐,不要猜; GetString撞上 Int 键返回的不是KEY_NOT_FOUND:把"非 OK 非 NOT_FOUND"直接error()会让 Snapshot 一颗 Int 键就断流(列表只剩 2 项),类型不匹配要容忍为null;- 不
Flush就Close,Reopen 后 size=0——每次短事务结束必须 Flush,这是持久化的唯一保证; bundleName写死必错:与 HAPapp.json5不一致时Open直接返回空指针,运行时用getBundleInfoForSelfSync取自身包名;- CAdapter 不把业务
@CName挂到 ArkTS:ELF 里看得到SettingsCall也 import 不到,必须 CMake 再包一层 NAPI; - 漏拷
libc++_shared.so时 dlopen 失败、页面白屏——KN so 的DT_NEEDED必须同目录满足,copy 任务按 ABI 各拷一份; @State变量不能叫size(与CustomComponent.size冲突),ArkTS 严格模式编译期直接红,改名kvCount;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();gradlew启动自身还需要JAVA_HOME,org.gradle.java.home要等 Gradle 起来才生效。
8.2 已知问题
coroutines/serialization/no-arg/datastore/make-observable五个扩展 artifact 未移植(留盘不进构建),需要 Flow 与序列化委托的业务请继续在本仓加模块;Long/Float/Double是文本存储:能 round-trip,不能与原生 Int 列混做范围查询;keys/size依赖 sidecar 索引:绕过本类直接改文件的写入会让索引漂移;- 没有
ObservableSettings:OH_Preferences_Subscribe未绑,UI 刷新靠主动 Snapshot; - 每次调用 Open / Flush / Close,适合低频配置,不是热路径;
- 多进程并发写未测试(
OH_Preferences文档允许跨进程); - android / js / wasmJs / apple / mingw 目标剥离(源文件留在树里),本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.settings匹配的签名。
8.3 未来优化方向
- 绑定
OH_Preferences_GetAll,用真实全量读取替代 sidecar 索引; - 绑定
OH_Preferences_Subscribe,补ObservableSettings与 coroutines Flow artifact; - 在
OhosPreferencesSettings加transaction { }批量接口(一次 Open、多次 Set、一次 Flush); - 移植
multiplatform-settings-coroutines/serialization扩展 artifact; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套。
九、总结
9.1 核心难点回顾
上游 multiplatform-settings(commonMain 零依赖)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(社区标准线,Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式,扩展 artifact 留盘)
→ ohosArm64 / ohosX64 KLIB + cinterop(手写 prefs_min.h)
→ OhosPreferencesSettings(类型映射 + sidecar 索引 + 短事务 Flush)
→ C ABI(@CName SettingsCall)+ JSON 协议
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机单页验收(六类型读写 / Reopen 持久化闭环)
9.2 封装层次
- 接口层:上游原版
Settings门面、六类型读写、Factory、属性委托; - 平台层:
OhosPreferencesSettingsactual——OH_Preferences绑定、类型映射、sidecar 索引、短事务; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、操作分发、类型选择与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 系统能力型库的正确答案是直连公开 C API,而不是绕 ArkTS:KMP 业务在 Kotlin/Native 线程上,边界是同步 C ABI;
OH_Preferences与SharedPreferences/NSUserDefaults同构,actual 一个类收口全部平台差异; - cinterop 不要喂 SDK 头:availability 属性和传递 include 会撑爆 clang 前端,手写精简头 +
dump-metadata-signatures对齐 Kotlin 视图,绑定面积只留真正用到的十几个符号; - 存储类库的验收天然落在持久化闭环上:jvmTest 钉住"接口没漂",Reopen 钉住"Flush 真落盘"——每调用一次 Open→op→Flush→Close 的短事务让 Demo 的 Reopen 按钮本身就是验收用例。
9.4 适配成果
当前适配已完成:
multiplatform-settings 1.3.0核心库的双 ABI Kotlin/Native 编译(上游commonMain零改动,新增仅 ohos actual 与 cinterop 描述);OH_Preferences公开 C API 在 KMP 库上的完整落地(手写精简头、类型映射、sidecar 索引、短事务 Flush,链接期-lohpreferences、运行期系统库解析);- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号、llvm-readelf验证短 soname); - DevEco Stage ArkUI 单页验收页面(六类型读写、存在性查询、删除清空、Reopen 持久化、缺 key 失败态);
- JVM 交叉验证全绿 + 双 ABI 链接(KN so 3.7MB / 3.4MB)+ HAP 构建、签名、真机安装与运行;
- AtomGit 项目文档、OpenHarmony README(中英双份)、上游 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。
参考文档
更多推荐

所有评论(0)