ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Kotlin Multiplatform 三方库 multiplatform-settings 的 OpenHarmony 鸿蒙化适配指南 Kotlin Multiplatform 三方库 multiplatform-settings 的 OpenHarmony 鸿蒙化适配指南库版本multiplatform-settings 1.3.0 鸿蒙 fork验证环境HarmonyOS Kotlin 2.2.21-1.0.0Gradle 8.14.1JDK 21DevEco Studio 26.0.0.821HarmonyOS 7.0.0API 26模拟器127.0.0.1:5555x86_64qrcode-kotlin验证的是纯计算actual 不链任何系统 so自己光栅、自己编 PNG。这篇换一类更常见的库——业务 API 在commonMain真正干活的是设备上那份libohpreferences.so。我以为把OH_Preferences_SetInt六个函数名抄进 Kotlin就能交差。然后发生了三件事。第一SDK 头喂给 cinteropclang 前端被__availability__和napi/native_api.h撑爆。第二编过之后import OH_Preferences仍然是Unresolved reference真实类型叫cnames.structs.OH_Preferencesbool *叫BooleanVar。第三Snapshot 只吐出两个键GetString撞上count这种 Int 列返回码既不是 OK 也不是 KEY_NOT_FOUND第一版 actual 直接error()剩下的键全没了。Preferences 这种库编过不算数。不 FlushReopen 就是空仓。bundleName 写错Open 返回空指针。HasKey 的失败态不截图审稿人会当没测。本文按这条真实路径写。一、环境搭建本章不展开直接引用官方入口KMPCMP 鸿蒙社区、HarmonyOS 应用开发导读。本文实际使用项值语言 / 框架Kotlin MultiplatformHarmonyOS Kotlin2.2.21-1.0.0插件仓库https://maven.eazytec-cloud.com/nexus/content/groups/harmonyosJDKTemurin 21写进gradle.properties的org.gradle.java.homeGradle Wrapper8.14.1DevEco Studio26.0.0.821HarmonyOS SDK7.0.0API 26真机 ABIohosArm64→arm64-v8a模拟器 ABIohosX64→x86_64HAP 打包 JDKDevEco 自带 JBR不要用 JDK 8系统库libohpreferences.so设备 sysroot不打进 HAPohosArm64()/ohosX64()只存在于这套定制 Kotlin Gradle Plugin。用 Maven Central 上的官方2.2.21写这两行配置期就会Unresolved reference。这是判断工具链有没有接对的第一根探针。二、应用背景跨端业务里键值存储出现的频率比二维码高一个数量级。登录 token、暗色模式开关、启动次数、实验分组全都是「写进去、杀进程、再读出来还在」。Android 侧大家熟SharedPreferencesApple 侧是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原样搬到 ohosKN 没有java.util.Properties。对位的公开能力是 C APIdatabase/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 size0再打开仍能读写字符串putStringOH_Preferences_SetStringSeed 的nickname列表string OpenHarmony写整数putIntOH_Preferences_SetIntSeed 的count7列表int 7写长整 / 浮点putLong/putFloat/putDoubleSetString存十进制文本Seed 的big/ratio/piround-trip 能读回来写布尔putBooleanOH_Preferences_SetBoolSeed 的ontrue列表boolean true读getX/getXOrNullGetInt/GetBool/GetStringGet状态栏打印 value存在性hasKeyOH_Preferences_HasKeyHashasnicknametrue/false删除removeDelete 更新索引Removesize 减 1列表不再有该键清空clear按索引逐个 DeleteClearclear size0缺键getXOrNull非 OK 返回 nullClear 后再 Getvaluenull foundfalse键集合keys/sizesidecar 索引__ohos_settings_indexSnapshotsize 与列表条数一致ArkTS 不直接碰这些 Kotlin API。它只调用一个 NAPI 函数import{SettingsCall}fromlibsettings_napi.so;constrawSettingsCall(JSON.stringify({op:seed,// init / put / get / has / remove / clear / reopen / snapshotbundleName: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。这是有意设计的。四、六阶段路线图工具链。JDK 21 HarmonyOS Kotlin 2.2.21-1.0.0 Gradle 8.14.1。JDK 8/11 会在配置期直接死。目标矩阵。根项目收成jvm()ohosArm64()ohosX64()。保留 JVM 是为了jvmTest当对照砍掉 iOS / Android / JS / wasm是因为这次要在 Windows 上闭环不是把上游降级成单平台。cinterop。手写prefs_min.h不要把 SDK 的oh_preferences.h整棵树丢进去。actual。OhosPreferencesSettings实现Settings。每次调用走 Open → 读或写 → Flush → Close。C ABI NAPI。example/nativeApp用sharedLib { baseName ohossettings }产出 soCName(SettingsCall)导出const char* SettingsCall(const char*)。CAdapter 不挂业务符号必须再编libsettings_napi.so。落盘。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, introduced13)))这一套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手抄签名类型改成普通structint/bool/char*ohpreferences.def的headers只指向这一份。编过之后Kotlin 侧看到的不是头文件里的名字。函数在包ohos.preferences不透明结构体在cnames.structs.OH_Preferencesbool *映射成kotlinx.cinterop.BooleanVar不是platform.posix.boolVar。第一版 actual 按「头文件里的名字原样 import」写编译器直接报Unresolved reference。修法是klib dump-metadata-signatures看一眼生成物再改 import。这是这篇适配里最值得记住的一步cinterop 的 Kotlin 视图和 C 头文件不是一一对应不要猜。链接时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 之后 size6点 Reopen 仍是 6说明 Flush 真的落盘了。bundleName必须和 HAP 的app.json5一致。写错时Open返回空指针。Demo 启动时用bundleManager.getBundleInfoForSelfSync取运行中的包名避免手写分叉。高频计数器以后可以做成「一次 Open、多次 Set、一次 Flush」的transaction {}。现在这条短事务对配置类场景够用也让截图路径可复现。六、桥接怎么接调用链从上到下是五层。中间少一层就会在dlopen或undefined is not a function上爆。HarmonyOS Kotlin 的 CAdapter 会给 so 挂一套 ArkTS 表面。和qrcode-kotlin那次一样CName(SettingsCall)在llvm-nm -D里看得到ArkTSimportKN 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 里走完整SettingsCName(SettingsCall)funsettingsCall(request:String):Stringdispatch(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)有四个地方我写错过。baseName是String不是PropertyString。写成baseName.set(ohossettings)会在 configuration 阶段报Unresolved reference set。正确是baseName ohossettings。CName参数用String。KN 生成const char*ABI。返回的 C 字符串必须DisposeStringNAPI 包装层已经做了ArkTS 不用管。entry/build-profile.json5的abiFilters必须同时有arm64-v8a和x86_64。本机 DevEco 模拟器是 x86_64。只编 arm64安装会报9568347看起来像签名问题其实是 ABI 缺失。ArkTSState 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.himport OH_Preferences找不到cinterop 把不透明类型放到cnames.structscnames.structs.OH_PreferencesboolVar找不到生成物是BooleanVarkotlinx.cinterop.BooleanVarSnapshot 只显示 2 个键GetString碰到 Int 键就error()类型不匹配返回 nullReopen 后 size0没 Flush每次withStore结束都 FlushOpen返回空指针bundleName 写死写错运行时取自身 bundleNameimportKN so 调不到SettingsCallCAdapter 不导出业务符号ArkTS 改 importlibsettings_napi.so9568347安装失败HAP 缺 x86_64abiFilters加上x86_64并链接 ohosX64Error loading shared library libc_shared.so只拷了 KN socopy 任务同时带上 Konan 的libc_shared.soState size编译失败和组件属性冲突改名kvCountPackageHap报 Could not create JVMJAVA_HOME指到 JDK 8打包改用 DevEcojbrGradle daemon 假成功daemon 和 JDK 切乱--stop之后--no-daemon全局optIn ExperimentalForeignApijvm 目标也中招file:OptIn只放 ohos 文件还有一条环境向的系统node往往不在 PATH 里。ohpm install要把 DevEcotools/node加进去。hvigor 打包用 DevEco JBR不要和 Gradle 的 JDK 21 混用。八、Demo 验收每个接口一张实拍包名org.terminator.ohos.settingsAbilityEntryAbility。本机模拟器hdc install后hdc shell aa start -a EntryAbility -b org.terminator.ohos.settings下面每张图都是从模拟器抠出来的不是合成。状态栏时间 10:16 附近电量 100%。字幕写的是KMP Settings OH_Preferences C API。不要只贴一张启动图交差。Preferences 库要看失败态、持久化、多种类型。8.1Factory.create空仓 Init启动即init。运行时取自身 bundleNamestore 名demo。状态栏init size0 storedemo列表为空。这一步过了说明 NAPI → KN →OH_Preferences_Open整条链是通的。如果是失败:开头先查 so 有没有进 HAP再查 bundleName。8.2 六种类型一次写入点Seed 6 types不要碰输入框。状态栏seed size6 storedemo。列表可见count→int 7原生 Int 列on→boolean true原生 Bool 列nickname→string OpenHarmonybig→string 1000000000Long 降级为字符串pi/ratio同理走字符串列六种类型一次 round-trip比手点六次 Put 可复现。size6来自 sidecar 索引不是瞎数列表行。8.3getString读回 nickname输入框保持nickname/OpenHarmony类型选string点Get。状态栏Get nickname (string) OpenHarmony副行size6 hasnickname foundtrue。这是getStringOrNull的成功态不是把输入框的值回显上去。8.4hasKey存在为 true点Has。状态栏has size6 storedemo副行hasnicknametrue。底层是OH_Preferences_HasKeyAPI 23 才有。API 26 模拟器返回值是对的。8.5Factory.create再打开数据还在点Reopen。实现里丢掉当前Settings实例再factory.create(demo)。如果上一次 Seed 没 Flush这里会变成 size0。实拍是reopen size6六条记录还在。这张图是整篇适配的核心证据不是进程内 HashMap是落盘。8.6remove删掉 nickname点Remove。状态栏remove size5 storedemo。列表里nickname消失剩下big/count/on/pi/ratio。副行hasnicknametrue是上一次 Has 的缓存——lastHas只在has/get时更新Remove 以列表和 size 为准。再点一次 Has就会变成 false。8.7clear整仓清空点Clear。状态栏clear size0 storedemo列表空hasnicknamefalse。索引键一并删掉。这是成功态的终点也是下一张失败态的起点。8.8 缺 keygetXOrNull返回 nullClear 之后再点Get。状态栏Get nickname (string) null副行size0 hasnickname foundfalse。上游契约是缺省值或OrNull不是抛异常。Preferences 库如果不拍这张图等于没测失败路径。Put单类型、Snapshot刷新在 Seed 路径里已经覆盖。对外截图不用那组被输入法拼脏键名的调试图。九、分层验收不要把「库编过」和「Reopen 后数据还在」混成一次验收。四层口子分开过。层命令我这边的结果L1gradlew :multiplatform-settings:jvmTestputGetRoundTrip、missingAndRemove绿。公共 API 语义在PropertiesSettings上对照过L2gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64两个 ABI 的 so 都导出SettingsCall并拷到entry/libs/abi/L3 编译hvigorw assembleHap -p productdefault -p buildModedebug --no-daemonHAP 内arm64-v8a/x86_64各含libsettings_napi.so、libohossettings.so、libc_shared.soL4 运行hdc install 真点击Init / Seed / Get / Has / Reopen / Remove / Clear / 缺 key 全部有实拍打包必须用 DevEco JBR。本机默认JAVA_HOME若指向 JDK 8PackageHap会报Could not create the Java Virtual Machinenative 其实已经编过了只是最后把 so 塞进 HAP 的那一步没起来。命令行打出来的是 unsigned HAP。这个模拟器接受了hdc install。真机和正式签名仍走 DevEco 的signingConfig: default不要在build-profile.json5里把这段改空。Windows 上链出来的 ohos so 不能在本机dlopen。对照测试走jvmTest设备行为走 HAP。不要在第 L4 失败时回头改commonMain算法——先hilog搜SettingsNapi看 JSONerror字段。十、已知限制不是 CMP。没有 Compose 控件没有 Settings UI 主题。KMP 和 CMP 是两条配额线这个库走 KMP。只做了核心 artifact。multiplatform-settings-coroutines、serialization、no-arg、datastore、make-observable没有 ohos 目标。需要 Flow 或 DataStore 后端的请继续在本仓加模块不要假装已经有。没有ObservableSettings。上游 Android 实现能监听 SharedPreferences 变化。OH_Preferences_Subscribe这轮没绑。UI 刷新靠 Demo 主动 Snapshot。Long/Float/Double是字符串存储。能 round-trip不能拿去和原生 Int 列混着做范围查询。keys/size依赖 sidecar 索引。不走本类、直接改文件的写入索引会漂。GetAll留给下一轮。每次调用都 Open/Flush/Close。适合 Demo 和低频配置。高频计数器请自己持有句柄。没有多进程锁测试。C API 文档允许跨进程本 Demo 没有两个 Ability 同时写。CAdapter 限制仍在。即使 ELF 能看到CName(SettingsCall)ArkTS 也必须走 NAPI。不要在下一篇里再踩一次还当新发现。模拟器 ABI。本地 DevEco 模拟器是 x86_64。只交 arm64 HAP安装失败码经常被误判成证书问题。真机签名未跑。模拟器 unsigned 可装。对外演示请在 DevEco 配signingConfig: default再用 arm64 真机走一遍 Seed / Reopen / Clear。仓库位置。适配代码在 oh-tpc/multiplatform-settings社区组织仍是 CPF-KMP-CMP。不往 Flutter 组织塞 KMP 库。有人会问鸿蒙自己就有 Preferences为什么还要 KMP 这一层。答案不是性能是边界。ArkTS Preferences 的上下文是ContextAPI 是 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。请固定带这六项否则很难判断是类型映射问题、没 Flush还是 so 没装进对的 ABIABI真机ohosArm64还是模拟器ohosX64操作init/seed/put/get/has/remove/reopen/clear类型与键string/int/long/float/double/boolean以及 key 原文状态栏原文size数字、has、Get 的value/nullhilog | grep SettingsNapi以及OH_Preferences_Open的 errCode列表实拍不要只贴 JSONPR 建议拆开src/ohosMain的 actual / cinterop 是库本体example/nativeApp的CName是 C ABIexample/harmonyApp的 CMake 是 NAPI。不要把三种改动揉进同一个 commit。示例工程保持signingConfig: default。克隆适配仓请走 AtomGit 组织页导入后再git clone不要写第三方镜像站地址。十二、小结这次适配没有发明新的键值协议。commonMain里的Settings接口原样工作。鸿蒙侧真正要补的是三块一份 cinterop 肯吃的精简头以及 dump 出来才知道的 Kotlin 类型名一个守住六种类型 round-trip 的OhosPreferencesSettingsInt/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()还能读到。三者缺一截图上都是空列表。KMPCMP 社区地址https://atomgit.com/CPF-KMP-CMPGitHub 上游https://github.com/russhwolf/multiplatform-settingsMaven Centralhttps://mvnrepository.com/artifact/com.russhwolf/multiplatform-settings鸿蒙适配版https://atomgit.com/oh-tpc/multiplatform-settings欢迎加入 KMPCMP 鸿蒙社区https://atomgit.com/CPF-KMP-CMP
返回列表