Kotlin Multiplatform 三方库 multiplatform-settings 的 OpenHarmony 鸿蒙化适配指南

库版本:multiplatform-settings 1.3.0 鸿蒙 fork|验证环境:HarmonyOS Kotlin 2.2.21-1.0.0|Gradle 8.14.1|JDK 21|DevEco Studio 26.0.0.821|HarmonyOS 7.0.0(API 26)|模拟器 127.0.0.1:5555(x86_64)

qrcode-kotlin 验证的是纯计算:actual 不链任何系统 so,自己光栅、自己编 PNG。这篇换一类更常见的库——业务 API 在 commonMain,真正干活的是设备上那份 libohpreferences.so。

我以为把 OH_Preferences_SetInt 六个函数名抄进 Kotlin,就能交差。然后发生了三件事。

第一,SDK 头喂给 cinterop,clang 前端被 __availability__ 和 napi/native_api.h 撑爆。第二,编过之后 import OH_Preferences 仍然是 Unresolved reference,真实类型叫 cnames.structs.OH_Preferences,bool * 叫 BooleanVar。第三,Snapshot 只吐出两个键:GetString 撞上 count 这种 Int 列,返回码既不是 OK 也不是 KEY_NOT_FOUND,第一版 actual 直接 error(),剩下的键全没了。
在这里插入图片描述

Preferences 这种库,编过不算数。不 Flush,Reopen 就是空仓。bundleName 写错,Open 返回空指针。HasKey 的失败态不截图,审稿人会当没测。本文按这条真实路径写。

OpenHarmony 模拟器实拍:Init / Seed / Reopen / Clear

一、环境搭建

本章不展开,直接引用官方入口:KMP&CMP 鸿蒙社区、HarmonyOS 应用开发导读。

本文实际使用:

项值
语言 / 框架Kotlin Multiplatform,HarmonyOS Kotlin 2.2.21-1.0.0
插件仓库https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos
JDKTemurin 21(写进 gradle.properties 的 org.gradle.java.home)
Gradle Wrapper8.14.1
DevEco Studio26.0.0.821
HarmonyOS SDK7.0.0(API 26)
真机 ABIohosArm64 → arm64-v8a
模拟器 ABIohosX64 → x86_64
HAP 打包 JDKDevEco 自带 JBR,不要用 JDK 8
系统库libohpreferences.so(设备 sysroot,不打进 HAP)

ohosArm64() / ohosX64() 只存在于这套定制 Kotlin Gradle Plugin。用 Maven Central 上的官方 2.2.21 写这两行,配置期就会 Unresolved reference。这是判断工具链有没有接对的第一根探针。

二、应用背景

跨端业务里,键值存储出现的频率比二维码高一个数量级。登录 token、暗色模式开关、启动次数、实验分组,全都是「写进去、杀进程、再读出来还在」。Android 侧大家熟 SharedPreferences,Apple 侧是 NSUserDefaults。Russ H Wolf 的 multiplatform-settings 把这套差异收口成一个 Settings 接口:

settings.putString("nickname", "OpenHarmony")
settings.putInt("count", 7)
settings.hasKey("nickname")
settings.getStringOrNull("token")
settings.remove("nickname")
settings.clear()

commonMain 里的业务代码不需要知道底下是 XML 文件、plist,还是鸿蒙的 Preferences 数据库。鸿蒙缺的只是一个 actual。

三条常见路线我都否掉了。

在 ArkTS 里直接 preferences.getPreferences(this.context, 'demo'),那是使用文,不是适配。KMP 业务跑在 Kotlin/Native 线程,没有 JS runtime,也不能把 Ability 的 Context 传进 commonMain。

ohos actual 走 JNI 回调 ArkTS Preferences,等于每条配置跨一次 NAPI,生命周期更乱。KN sharedLib 里也没有 JVM。

把 JVM 的 PropertiesSettings 原样搬到 ohos,KN 没有 java.util.Properties。

对位的公开能力是 C API:database/preferences/oh_preferences.h,动态库 libohpreferences.so。Open / SetInt / SetBool / SetString / Get* / Delete / Close 从 API 13 开始;HasKey 和 Flush 从 API 23 开始。目标设备 API 26,两档都能用。

选题时查过 CPF-KMP-CMP 组织:当时已有 Ksoup、kotlin-result、kotlin-multiplatform-diff 等仓,没有 multiplatform-settings。过审名单里的 KMP 库也不含它。这是增量题,不是重复领激励。方向锁定 KMP 三方库适配,不是 CMP,不是 Flutter,也不是「拿 ArkTS Preferences 写业务」的使用文。

三、接口分析

按 Demo 实际打到的表面列,不把上游 coroutines / serialization / datastore 扩展件算进「已适配」。

能力上游 API鸿蒙 actualDemo 怎么调验收
工厂Settings.Factory.create(name)OhosPreferencesSettings.Factory(bundleName)Init / Reopeninit size=0,再打开仍能读
写字符串putStringOH_Preferences_SetStringSeed 的 nickname列表 string = OpenHarmony
写整数putIntOH_Preferences_SetIntSeed 的 count=7列表 int = 7
写长整 / 浮点putLong / putFloat / putDoubleSetString 存十进制文本Seed 的 big / ratio / piround-trip 能读回来
写布尔putBooleanOH_Preferences_SetBoolSeed 的 on=true列表 boolean = true
读getX / getXOrNullGetInt / GetBool / GetStringGet状态栏打印 value
存在性hasKeyOH_Preferences_HasKeyHashas=nickname=true/false
删除removeDelete + 更新索引Removesize 减 1,列表不再有该键
清空clear按索引逐个 DeleteClearclear size=0
缺键getXOrNull非 OK 返回 nullClear 后再 Getvalue=null found=false
键集合keys / sizesidecar 索引 __ohos_settings_indexSnapshotsize 与列表条数一致

ArkTS 不直接碰这些 Kotlin API。它只调用一个 NAPI 函数:

import { SettingsCall } from 'libsettings_napi.so';

const raw = SettingsCall(JSON.stringify({
  op: 'seed',          // init / put / get / has / remove / clear / reopen / snapshot
  bundleName: this.bundleName,
  name: this.storeName,
  type: this.valueType,
  key: this.keyName,
  value: this.valueText,
}));

返回值是 JSON:{ ok, op, size, items, found, hasKey, value, error }。UI 进程不懂 Preferences 文件格式,Kotlin/Native 进程不懂 ArkUI。这是有意设计的。

四、六阶段路线图

六阶段:工具链 → 目标矩阵 → cinterop → actual → NAPI → 落盘

  1. 工具链。 JDK 21 + HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1。JDK 8/11 会在配置期直接死。
  2. 目标矩阵。 根项目收成 jvm() + ohosArm64() + ohosX64()。保留 JVM 是为了 jvmTest 当对照;砍掉 iOS / Android / JS / wasm,是因为这次要在 Windows 上闭环,不是把上游降级成单平台。
  3. cinterop。 手写 prefs_min.h,不要把 SDK 的 oh_preferences.h 整棵树丢进去。
  4. actual。 OhosPreferencesSettings 实现 Settings。每次调用走 Open → 读或写 → Flush → Close。
  5. C ABI + NAPI。 example/nativeApp 用 sharedLib { baseName = "ohossettings" } 产出 so,@CName("SettingsCall") 导出 const char* SettingsCall(const char*)。CAdapter 不挂业务符号,必须再编 libsettings_napi.so。
  6. 落盘。 hvigorw assembleHap 把两个 ABI 的三份 so 打进 HAP,模拟器上 Init / Seed / Reopen / Remove / Clear / 缺 key 全部有实拍。

applyDefaultHierarchyTemplate() 会在两个 ohos 目标之上生成 ohosMain。代码放这里,不要放 nativeMain:以后一旦加 linux/mingw,会把鸿蒙 Preferences 强加给不该用的平台。

上游原工程有一长串 artifact 和 target。原样打开,HarmonyOS Kotlin 插件会在用不到的目标上失败,cinterop commonization 也会把 ohos 和 linux 搅在一起。本 fork 的 settings.gradle.kts 只留 :multiplatform-settings 和 :example:nativeApp。

五、三个关键决策

5.1 为什么不把 SDK 头直接喂给 cinterop

第一反应是 headers = oh_preferences.h。编不过,原因很具体。

头文件用了 __attribute__((__availability__(ohos, introduced=13))) 这一套,cinterop 自带的 clang 前端认不全。它还 #include <napi/native_api.h>,会把 NAPI 和 PreferencesValue 整棵树拉进来。我们这轮真正要用的只有 Option 的 Create/Set/Destroy,以及 Preferences 的 Open/Get/Set/Delete/HasKey/Flush/Close/FreeString。订阅和 GetAll 先不做。

所以 src/ohosMain/cinterop/ 下放了两份自己写的文件:prefs_min.h 手抄签名,类型改成普通 struct + int / bool / char*;ohpreferences.def 的 headers 只指向这一份。

编过之后,Kotlin 侧看到的不是头文件里的名字。函数在包 ohos.preferences,不透明结构体在 cnames.structs.OH_Preferences,bool * 映射成 kotlinx.cinterop.BooleanVar,不是 platform.posix.boolVar。

第一版 actual 按「头文件里的名字原样 import」写,编译器直接报 Unresolved reference。修法是 klib dump-metadata-signatures 看一眼生成物,再改 import。这是这篇适配里最值得记住的一步:cinterop 的 Kotlin 视图和 C 头文件不是一一对应,不要猜。

左:照着 SDK 头写;右:精简头 + dump 对类型

链接时,nativeApp 的 sharedLib 必须加 -L$sysroot/usr/lib/<abi>-linux-ohos -lohpreferences。不加的话,OH_Preferences_Open 会以 UND 留在 so 里,dlopen 阶段找不到符号。这些符号运行时由系统 libohpreferences.so 提供,不要把系统库打进 HAP。

5.2 类型怎么映射,以及 GetString 为什么不能乱抛

C API 原生只认 Int、Bool、String。上游 Settings 还有 Long、Float、Double。硬缺口只有两条路:拒绝编译,或者降级存储。

我选了后者。putLong / putFloat / putDouble 走 SetString 存十进制文本,读的时候再 parse。getLongOrNull 先按字符串解析,失败再尝试 getIntOrNull 再 widen。这样「以前用 int 存的计数器,后来改成 long 读」不会直接崩。

Demo 列表里 big 显示成 string = 1000000000,不是 UI 写错了,是存储层的诚实展示。Int 和 Boolean 走原生列,Snapshot 能把它们标成 int / boolean。限制必须写进文档:不要拿这些字符串列去做范围查询,那不是 Settings 的职责。

更阴的是读。OH_Preferences_GetString 碰到一个 Int 键时,返回码不是 KEY_NOT_FOUND,而是类型不匹配。第一版把「非 OK 且非 NOT_FOUND」直接 error(),结果 Snapshot 遍历 keys 时,一碰到 count 就抛异常,页面只显示得到 2 个键,看起来像丢数据。

修法:类型对不上就当 null,让 Snapshot 继续用 GetInt / GetBool 认类型。GetString 成功时必须 OH_Preferences_FreeString,这块堆是 C 侧分配的。

这件事 jvmTest 测不到。JVM 对照走的是 PropertiesSettings,没有类型列。只有装到模拟器、点 Seed 6 types 之后才会爆。

5.3 为什么每次都 Open / Flush / Close,以及 keys 从哪来

OH_Preferences 没有便宜的 keys()。GetAll 走 PreferencesValue 数组,cinterop 成本高,这轮不做。折中是写一个 sidecar 键 __ohos_settings_index,值为换行分隔的 key 列表。put* 时把 key 加进集合,remove / clear 时更新。这个键对调用方不可见。

限制同样写清楚:如果有人用别的进程直接改同一份 Preferences 文件、却不走本类,index 会和真实内容漂移。Demo 和常规 KMP 业务都走同一个 Settings 实例,不会踩。

把 Open/Close 放在每次调用里,而不是进程级单例,是为了 Demo 的 Reopen 按钮有意义:点 Reopen 等于重新 Factory.create()。如果上一次没 Flush,新句柄读到的就是空。模拟器上 Seed 之后 size=6,点 Reopen 仍是 6,说明 Flush 真的落盘了。

bundleName 必须和 HAP 的 app.json5 一致。写错时 Open 返回空指针。Demo 启动时用 bundleManager.getBundleInfoForSelfSync 取运行中的包名,避免手写分叉。

高频计数器以后可以做成「一次 Open、多次 Set、一次 Flush」的 transaction {}。现在这条短事务对配置类场景够用,也让截图路径可复现。

六、桥接怎么接

调用链从上到下是五层。中间少一层就会在 dlopen 或 undefined is not a function 上爆。

ArkTS → libsettings_napi.so → libohossettings.so → OhosPreferencesSettings → libohpreferences.so

HarmonyOS Kotlin 的 CAdapter 会给 so 挂一套 ArkTS 表面。和 qrcode-kotlin 那次一样:@CName("SettingsCall") 在 llvm-nm -D 里看得到,ArkTS import KN so 却调不到。缺的是 NAPI 表面,不是 C 符号。所以 Demo 按官方 native 模块来:entry/src/main/cpp/napi_init.cpp 编成 libsettings_napi.so,内部链接 libohossettings.so,调完用 DisposeString 把 KN 分配的 C 字符串释放掉。

example/nativeApp 的 Bridge.kt 吃一小段 JSON,在 Kotlin 里走完整 Settings:

@CName("SettingsCall")
fun settingsCall(request: String): String = dispatch(request)

// seed 一次写入六种类型,截图不依赖输入法
s.putString("nickname", "OpenHarmony")
s.putInt("count", 7)
s.putLong("big", 1_000_000_000L)
s.putFloat("ratio", 1.5f)
s.putDouble("pi", 3.14)
s.putBoolean("on", true)

有四个地方我写错过。

  1. baseName 是 String,不是 Property<String>。 写成 baseName.set("ohossettings") 会在 configuration 阶段报 Unresolved reference 'set'。正确是 baseName = "ohossettings"。
  2. @CName 参数用 String。 KN 生成 const char* ABI。返回的 C 字符串必须 DisposeString,NAPI 包装层已经做了,ArkTS 不用管。
  3. entry/build-profile.json5 的 abiFilters 必须同时有 arm64-v8a 和 x86_64。 本机 DevEco 模拟器是 x86_64。只编 arm64,安装会报 9568347,看起来像签名问题,其实是 ABI 缺失。
  4. ArkTS @State size 不能用。 它和 CustomComponent.size 冲突,编译期直接红。改成 kvCount。这和 Preferences 无关,但会卡 HAP。

linkDebugSharedOhosArm64 / OhosX64 已经 finalizedBy 拷贝任务,会把 libohossettings.so 和 Kotlin/Native 依赖的 libc++_shared.so 一起放进 entry/libs/<abi>/。漏掉 libc++,运行时是 Error loading shared library libc++_shared.so。

CMake 强制 -Wl,-rpath,$ORIGIN、BUILD_WITH_INSTALL_RPATH TRUE、IMPORTED_NO_SONAME TRUE。IMPORTED 库会把本机绝对路径写进 so,真机上那条路径毫无意义。nm_modname 必须是 settings_napi,和 oh-package.json5 对上。名字写错,ArkTS 会编译过、运行时报模块找不到。

JSON 解析是手写的极简扫描,只认字符串字段。不要在 KN 侧再拉 kotlinx.serialization——ohos 目标的依赖图会被重新打乱。seed 做成独立 op,是因为 uitest inputText 往输入框里追加而不是替换,键名会被拼成一长串。对外截图只点按钮,不碰输入法。

七、踩坑表

现象根因处理
Unresolved reference: ohosArm64用了官方 Kotlin 2.2.21换成 HarmonyOS Kotlin 2.2.21-1.0.0,仓库放到 pluginManagement 第一位
cinterop 喂 SDK 头直接死availability 属性 / napi 头手写 prefs_min.h
import OH_Preferences 找不到cinterop 把不透明类型放到 cnames.structscnames.structs.OH_Preferences
boolVar 找不到生成物是 BooleanVarkotlinx.cinterop.BooleanVar
Snapshot 只显示 2 个键GetString 碰到 Int 键就 error()类型不匹配返回 null
Reopen 后 size=0没 Flush每次 withStore 结束都 Flush
Open 返回空指针bundleName 写死写错运行时取自身 bundleName
import KN so 调不到 SettingsCallCAdapter 不导出业务符号ArkTS 改 import libsettings_napi.so
9568347 安装失败HAP 缺 x86_64abiFilters 加上 x86_64,并链接 ohosX64
Error loading shared library libc++_shared.so只拷了 KN socopy 任务同时带上 Konan 的 libc++_shared.so
@State size 编译失败和组件属性冲突改名 kvCount
PackageHap 报 Could not create JVMJAVA_HOME 指到 JDK 8打包改用 DevEco jbr
Gradle daemon 假成功daemon 和 JDK 切乱--stop 之后 --no-daemon
全局 optIn ExperimentalForeignApijvm 目标也中招@file:OptIn 只放 ohos 文件

还有一条环境向的:系统 node 往往不在 PATH 里。ohpm install 要把 DevEco tools/node 加进去。hvigor 打包用 DevEco JBR,不要和 Gradle 的 JDK 21 混用。

八、Demo 验收:每个接口一张实拍

在这里插入图片描述

包名 org.terminator.ohos.settings,Ability EntryAbility。本机模拟器 hdc install 后:

hdc shell aa start -a EntryAbility -b org.terminator.ohos.settings

下面每张图都是从模拟器抠出来的,不是合成。状态栏时间 10:16 附近,电量 100%。字幕写的是 KMP Settings + OH_Preferences C API。不要只贴一张启动图交差。Preferences 库要看失败态、持久化、多种类型。

8.1 Factory.create:空仓 Init

启动即 init。运行时取自身 bundleName,store 名 demo。状态栏 init size=0 store=demo,列表为空。这一步过了,说明 NAPI → KN → OH_Preferences_Open 整条链是通的。如果是 失败: 开头,先查 so 有没有进 HAP,再查 bundleName。

Init 空仓库 size=0

8.2 六种类型一次写入

点 Seed 6 types,不要碰输入框。状态栏 seed size=6 store=demo。列表可见:

  • count → int = 7(原生 Int 列)
  • on → boolean = true(原生 Bool 列)
  • nickname → string = OpenHarmony
  • big → string = 1000000000(Long 降级为字符串)
  • pi / ratio 同理走字符串列

六种类型一次 round-trip,比手点六次 Put 可复现。size=6 来自 sidecar 索引,不是瞎数列表行。

Seed 六种类型 size=6

8.3 getString:读回 nickname

输入框保持 nickname / OpenHarmony,类型选 string,点 Get。状态栏:

Get nickname (string) = OpenHarmony

副行 size=6 has=nickname found=true。这是 getStringOrNull 的成功态,不是把输入框的值回显上去。

Get nickname 返回 OpenHarmony

8.4 hasKey:存在为 true

点 Has。状态栏 has size=6 store=demo,副行 has=nickname=true。底层是 OH_Preferences_HasKey,API 23 才有。API 26 模拟器返回值是对的。

HasKey nickname=true

8.5 Factory.create 再打开:数据还在

点 Reopen。实现里丢掉当前 Settings 实例,再 factory.create("demo")。如果上一次 Seed 没 Flush,这里会变成 size=0。实拍是 reopen size=6,六条记录还在。这张图是整篇适配的核心证据:不是进程内 HashMap,是落盘。

Reopen 后 size 仍为 6

8.6 remove:删掉 nickname

点 Remove。状态栏 remove size=5 store=demo。列表里 nickname 消失,剩下 big / count / on / pi / ratio。副行 has=nickname=true 是上一次 Has 的缓存——lastHas 只在 has / get 时更新,Remove 以列表和 size 为准。再点一次 Has,就会变成 false。

Remove 后 size=5,列表无 nickname

8.7 clear:整仓清空

点 Clear。状态栏 clear size=0 store=demo,列表空,has=nickname=false。索引键一并删掉。这是成功态的终点,也是下一张失败态的起点。

Clear 后 size=0

8.8 缺 key:getXOrNull 返回 null

Clear 之后再点 Get。状态栏:

Get nickname (string) = null

副行 size=0 has=nickname found=false。上游契约是缺省值或 OrNull,不是抛异常。Preferences 库如果不拍这张图,等于没测失败路径。

缺 key 时 Get 返回 null

Put 单类型、Snapshot 刷新,在 Seed 路径里已经覆盖。对外截图不用那组被输入法拼脏键名的调试图。

九、分层验收

不要把「库编过」和「Reopen 后数据还在」混成一次验收。四层口子分开过。

L1 jvmTest / L2 链接 / L3 HAP / L4 运行

层命令我这边的结果
L1gradlew :multiplatform-settings:jvmTestputGetRoundTrip、missingAndRemove 绿。公共 API 语义在 PropertiesSettings 上对照过
L2gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64两个 ABI 的 so 都导出 SettingsCall,并拷到 entry/libs/<abi>/
L3 编译hvigorw assembleHap -p product=default -p buildMode=debug --no-daemonHAP 内 arm64-v8a / x86_64 各含 libsettings_napi.so、libohossettings.so、libc++_shared.so
L4 运行hdc install + 真点击Init / Seed / Get / Has / Reopen / Remove / Clear / 缺 key 全部有实拍

打包必须用 DevEco JBR。本机默认 JAVA_HOME 若指向 JDK 8,PackageHap 会报 Could not create the Java Virtual Machine,native 其实已经编过了,只是最后把 so 塞进 HAP 的那一步没起来。

命令行打出来的是 unsigned HAP。这个模拟器接受了 hdc install。真机和正式签名仍走 DevEco 的 "signingConfig": "default",不要在 build-profile.json5 里把这段改空。

Windows 上链出来的 ohos so 不能在本机 dlopen。对照测试走 jvmTest,设备行为走 HAP。不要在第 L4 失败时回头改 commonMain 算法——先 hilog 搜 SettingsNapi,看 JSON error 字段。

十、已知限制

  1. 不是 CMP。 没有 Compose 控件,没有 Settings UI 主题。KMP 和 CMP 是两条配额线,这个库走 KMP。
  2. 只做了核心 artifact。 multiplatform-settings-coroutines、serialization、no-arg、datastore、make-observable 没有 ohos 目标。需要 Flow 或 DataStore 后端的,请继续在本仓加模块,不要假装已经有。
  3. 没有 ObservableSettings。 上游 Android 实现能监听 SharedPreferences 变化。OH_Preferences_Subscribe 这轮没绑。UI 刷新靠 Demo 主动 Snapshot。
  4. Long / Float / Double 是字符串存储。 能 round-trip,不能拿去和原生 Int 列混着做范围查询。
  5. keys / size 依赖 sidecar 索引。 不走本类、直接改文件的写入,索引会漂。GetAll 留给下一轮。
  6. 每次调用都 Open/Flush/Close。 适合 Demo 和低频配置。高频计数器请自己持有句柄。
  7. 没有多进程锁测试。 C API 文档允许跨进程,本 Demo 没有两个 Ability 同时写。
  8. CAdapter 限制仍在。 即使 ELF 能看到 @CName("SettingsCall"),ArkTS 也必须走 NAPI。不要在下一篇里再踩一次还当新发现。
  9. 模拟器 ABI。 本地 DevEco 模拟器是 x86_64。只交 arm64 HAP,安装失败码经常被误判成证书问题。
  10. 真机签名未跑。 模拟器 unsigned 可装。对外演示请在 DevEco 配 signingConfig: default,再用 arm64 真机走一遍 Seed / Reopen / Clear。
  11. 仓库位置。 适配代码在 oh-tpc/multiplatform-settings,社区组织仍是 CPF-KMP-CMP。不往 Flutter 组织塞 KMP 库。

有人会问:鸿蒙自己就有 Preferences,为什么还要 KMP 这一层。答案不是性能,是边界。ArkTS Preferences 的上下文是 Context,API 是 Promise。KMP 业务拿到的是同步 C ABI,没有 JS runtime。把 KN 的 putInt 回调到 ArkTS 再写,等于每条配置都要跨一次 NAPI。本适配站在 OH_Preferences 这一层。Demo 之所以还出现 ArkTS,只是因为要有一个能点的界面,和征文要求的全接口截图。真正给业务用的入口是 OhosPreferencesSettings,不是 Index.ets。

十一、如何提 Issue / PR

上游功能问题优先去 russhwolf/multiplatform-settings。鸿蒙 actual、cinterop 头、NAPI 包装、Demo 安装问题开在适配仓库。不要把 ohosArm64 编译日志丢给上游——那不是他们的 target。

提 Issue 请带 ABI、操作、类型、状态栏、hilog、实拍

请固定带这六项,否则很难判断是类型映射问题、没 Flush,还是 so 没装进对的 ABI:

  1. ABI:真机 ohosArm64 还是模拟器 ohosX64
  2. 操作:init / seed / put / get / has / remove / reopen / clear
  3. 类型与键:string / int / long / float / double / boolean,以及 key 原文
  4. 状态栏原文:size= 数字、has=、Get 的 value / null
  5. hilog | grep SettingsNapi,以及 OH_Preferences_Open 的 errCode
  6. 列表实拍,不要只贴 JSON

PR 建议拆开:src/ohosMain 的 actual / cinterop 是库本体;example/nativeApp 的 @CName 是 C ABI;example/harmonyApp 的 CMake 是 NAPI。不要把三种改动揉进同一个 commit。示例工程保持 "signingConfig": "default"。

克隆适配仓请走 AtomGit 组织页导入后再 git clone,不要写第三方镜像站地址。

十二、小结

这次适配没有发明新的键值协议。commonMain 里的 Settings 接口原样工作。鸿蒙侧真正要补的是三块:

  • 一份 cinterop 肯吃的精简头,以及 dump 出来才知道的 Kotlin 类型名
  • 一个守住六种类型 round-trip 的 OhosPreferencesSettings,Int/Bool 走原生列,其余走字符串,缺 key 和类型不匹配都返回 null
  • 一层 CAdapter 不肯给的 NAPI 表面,外加每次调用都 Flush,让 Reopen 有意义

前两块是常规 KMP。第三块是 HarmonyOS Kotlin 目前的现实,上一篇二维码已经踩过,这篇用系统库又确认了一次。谁要是把「so 里有符号」当成「ArkTS 能调用」,或者把「编过」当成「落盘了」,会在 Zip/GZip 三个导出和空的 Reopen 上再浪费整下午。

和 qrcode-kotlin 那篇是同一条工具链上的两个样本。一个纯计算、不链系统 so;一个必须 dlopen 设备上的 libohpreferences.so。两篇一起看,才能判断 ohos target 是不是真的接上了,而不是复制了一份 so。

模拟器已经把 Init / Seed / Get / Has / Reopen / Remove / Clear / 缺 key 跑通。仓库在 oh-tpc/multiplatform-settings。下一步用 DevEco 默认签名在真机上再走一遍 arm64。

如果只记住一件事:在 HarmonyOS Kotlin 这条链上,Kotlin/Native so 负责对系统 C API 读写,CMake NAPI so 负责被 ArkTS 看见,Flush 负责让下一句 Factory.create() 还能读到。三者缺一,截图上都是空列表。

KMP&CMP 社区地址:https://atomgit.com/CPF-KMP-CMP
GitHub 上游:https://github.com/russhwolf/multiplatform-settings
Maven Central:https://mvnrepository.com/artifact/com.russhwolf/multiplatform-settings
鸿蒙适配版:https://atomgit.com/oh-tpc/multiplatform-settings

欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐