
Compose Multiplatform KV 存储库 multiplatform-settings 的 OpenHarmony 鸿蒙化适配实战OH_Preferences cinterop 孤儿源集排查 模拟器实测库版本com.russhwolf:multiplatform-settings 1.3.0-ohos.1本地重建鸿蒙切片验证环境Kotlin 2.2.21-0.4.0鸿蒙定制版Compose Multiplatform 1.9.2-0.4.0DevEco Studio 26.0.0DevEco 模拟器HarmonyOS 7.0.0API 26multiplatform-settings是 KMP 生态做键值KV存储的事实标准——Android 用 SharedPreferencesiOS 用 NSUserDefaults鸿蒙侧对接系统OH_Preferences。和上一篇的reorderable不同这个库不是纯 Compose 库它的 ohos 实现要走 cinterop 绑定鸿蒙系统 C APIlibohpreferences.so是真正的「平台原生能力适配」。本文记录我把它完整跑上鸿蒙的全过程从本地重建对齐工具链的 klib、写一个 KV 读写 demo到排查一个比 IR 崩溃更隐蔽的「孤儿源集」问题——编译全程不报错但 ohos 代码压根没进 klib。先睹为快DevEco 模拟器实测。KV 读写 demo——Put/Get/Has/Remove/Clear/Reopen 全功能状态卡实时显示 store/size/get/has底部快照列出当前所有键值对一、先看清楚multiplatform-settings 的鸿蒙实现是怎么落地的照例先翻源码分布。这个库比reorderable重——它强依赖平台存储 API每个平台都有 actual 实现上游源集内容对平台的依赖commonMainSettings接口putString/getIntOrNull/hasKey/clear/keys/sizeSettingsListener无androidMainSharedPreferencesSettingsAndroid SharedPreferencesappleMainNSUserDefaultsSettingsiOS/macOS NSUserDefaultsohosMainOhosPreferencesSettingscinterop/ohpreferences.def鸿蒙OH_Preferences_*C APIjvmMainPreferencesSettingsJava Preferences API关键观察鸿蒙侧靠 cinterop 把libohpreferences.so的 C 函数包成 Kotlin 可调用的OhosPreferencesSettings。适配核心是三件事cinterop 的.def文件要能找到鸿蒙系统头文件这个 fork 用一个裁剪过的prefs_min.h最小头而非全量 SDK 头链接时要把-lohpreferences加进 linkerOpts否则 native 符号解析失败bundleName必须与鸿蒙工程的app.json5一致否则OH_Preferences_Open返回错误。二、工程结构cmp-settings-demo/ ├── composeApp/ # CMP 共享模块 │ ├── src/commonMain/kotlin/ohos/cmp/settings/ │ │ ├── App.kt # KV 读写 demo UIPut/Get/Has/Remove/Clear/Reopen │ │ └── Platform.kt # expect fun createSettings(): Settings │ ├── src/ohosMain/kotlin/ohos/cmp/settings/ │ │ └── Platform.ohos.kt # actual: OhosPreferencesSettings.Factory(...) │ └── build.gradle.kts # ohosArm64/ohosX64 双目标 -lohpreferences ├── harmonyApp/ # DevEco 鸿蒙应用工程 │ └── entry/src/main/ets/pages/ # ArkTS 页面加载 libkn.so ├── gradle/libs.versions.toml # 版本目录Kotlin 2.2.21-0.4.0 等 └── _ref/multiplatform-settings/ # 本地重建的 settings 源码对齐 0.4.0 工具链composeApp标准 CMP 模块commonMain写 UI 和expectohosMain写actualharmonyAppDevEco 工程通过 NAPI 加载libkn.so把 Compose 渲染结果上屏到 ArkUI_ref/multiplatform-settings从 oh-tpc 克隆的鸿蒙 fork 源码修改版本后publishToMavenLocal。三、适配过程六个关键步骤3.1 没有 0.4.0 工具链的现成切片本地重建 klib第一个坎eazytec-cloud Nexus 上com.russhwolf:multiplatform-settings的最新鸿蒙切片是1.4.0-0.1.0-rc1-05但它的 klib 是用 Kotlin2.2.21-OH.0.1.0-01工具链编的——和 demo 的2.2.21-0.4.0不一致直接链接会触发和 reorderable 一样的IrFakeOverrideSymbol崩溃。解法和 reorderable 一样把 oh-tpc 的multiplatform-settingsfork 克隆到_ref/把版本对齐到-0.4.0然后本地重建。这个 fork 有额外两处要改——它原本钉的是1.0.0工具链 JDK 21# _ref/multiplatform-settings/gradle/libs.versions.toml修改后 [versions] kotlin 2.2.21-0.4.0 # 原来是 2.2.21-1.0.0 composeMultiplatform 1.9.2-0.4.0 # 原来是 1.9.2-1.0.0// _ref/multiplatform-settings/multiplatform-settings/build.gradle.ktskotlin{jvmToolchain(17)// 原来是 21本机只有 JDK 17降级}# _ref/multiplatform-settings/gradle.properties修改后 org.gradle.java.homeC:\\Users\\nwu\\.jdks-portable\\jdk-17.0.1311发布到 mavenLocalcd_ref/multiplatform-settings .\gradlew.bat --no-daemon :multiplatform-settings:publishToMavenLocal产物坐标com.russhwolf:multiplatform-settings:1.3.0-ohos.1含ohosArm64/ohosX64两个 klib。经验鸿蒙生态的 KMP 库大多是「定制切片」版本号形如x.y.z-ohos.N。所有参与链接的 klib 必须用同一个 Kotlin 编译器版本本地重建是绕不开的常态操作。3.2 cinterop 配置用最小头文件绑鸿蒙系统 C API这个 fork 没有用全量 SDK 头而是在src/ohosMain/cinterop/放了一个裁剪过的最小头prefs_min.h只声明 demo 用到的OH_Preferences_*函数签名// prefs_min.h节选#pragmaonce#includestdbool.h#includestdint.htypedefstructOH_PreferencesOH_Preferences;typedefstructOH_PreferencesOptionOH_PreferencesOption;OH_PreferencesOption*OH_PreferencesOption_Create(void);intOH_PreferencesOption_SetFileName(OH_PreferencesOption*option,constchar*fileName);intOH_PreferencesOption_SetBundleName(OH_PreferencesOption*option,constchar*bundleName);OH_Preferences*OH_Preferences_Open(OH_PreferencesOption*option,int*errCode);intOH_Preferences_GetInt(OH_Preferences*preference,constchar*key,int*value);intOH_Preferences_SetInt(OH_Preferences*preference,constchar*key,intvalue);// ... GetBool/SetBool/GetString/SetString/Flush/Close 等.def文件极简单——直接指向这个本地头包名定为ohos.preferences# ohpreferences.def headers prefs_min.h package ohos.preferences compilerOpts -I.build.gradle.kts里为两个鸿蒙 target 注册 cinteroplistOf(ohosArm64(),ohosX64()).forEach{target-target.compilations.getByName(main){cinterops.create(ohpreferences){defFile(cinteropInclude.resolve(ohpreferences.def))includeDirs(cinteropInclude)compilerOpts(-I$cinteropInclude)}}}编译时 Kotlin/Native 会生成ohos.preferences包下的 Kotlin 绑定OhosPreferencesSettings就是基于这些绑定封装的。为什么用最小头而不是 SDK 全量头鸿蒙 SDK 的preferences.h头依赖大量其它内部头cinterop 解析时容易牵出一串无关符号。裁剪出一个只含 demo 所需函数的最小头能让 cinterop 干净快速地产出绑定是鸿蒙 KMP 库适配的常见技巧。3.3 排查比 IR 崩溃更隐蔽的「孤儿源集」接好依赖、改完版本后第一次编译报Unresolved reference: OhosPreferencesSettings——但 commonMain 的Settings接口能正常解析。这说明 klib 能下载到、commonMain API 可见唯独 ohos 特有的OhosPreferencesSettings类不见了。把依赖从commonMain.dependencies挪到ohosMain.dependencies也没用。最后用klib contents解包重建出的 klib发现里面只有com.russhwolf.settings的 commonMain 代码根本没有OhosPreferencesSettings。真相0.4.0 版定制 Kotlin 插件的applyDefaultHierarchyTemplate()默认源集层级不包含 ohos 组合1.0.0 版才内置。于是src/ohosMain/kotlin/成了一个「孤儿目录」——ohosArm64Main/ohosX64Main各自存在但没有公共的ohosMain中间源集把这段 Kotlin 代码喂给两个 target编译器全程不报错只是这段代码「消失」了。修复在_ref/multiplatform-settings/multiplatform-settings/build.gradle.kts的sourceSets块里显式补出中间源集sourceSets{commonMain.dependencies{}commonTest.dependencies{implementation(kotlin(test))}jvmTest.dependencies{implementation(kotlin(test))}// 0.4.0 工具链的 default hierarchy 不含 ohos 组合显式声明中间源集// 否则 src/ohosMain/kotlin 是孤儿目录编出的 klib 缺 OhosPreferencesSettingsvalohosMainsourceSets.maybeCreate(ohosMain)ohosMain.dependsOn(sourceSets.getByName(commonMain))sourceSets.getByName(ohosArm64Main).dependsOn(ohosMain)sourceSets.getByName(ohosX64Main).dependsOn(ohosMain)}重新publishToMavenLocal后再用klib contents验证OhosPreferencesSettings出现在 klib 里Unresolved reference消失。这是本批适配里最容易被忽略的一类坑不是编译错而是「静默缺代码」。当某个平台特有的类「应该存在却 unresolved」时先klib contents看产物里到底有没有再回到源集层级查是不是孤儿目录。3.4 demo 侧接入expect/actual linkerOptsdemo 的commonMain只认Settings接口平台差异收口在一个expect工厂// composeApp/src/commonMain/kotlin/ohos/cmp/settings/Platform.ktpackageohos.cmp.settingsimportcom.russhwolf.settings.Settings/** 平台差异收口点commonMain 只认 [Settings] 接口。 */expectfuncreateSettings():SettingsohosMain提供 actual用OhosPreferencesSettings.Factory创建实例。bundleName必须与harmonyApp/AppScope/app.json5的bundleName完全一致// composeApp/src/ohosMain/kotlin/ohos/cmp/settings/Platform.ohos.ktpackageohos.cmp.settingsimportcom.russhwolf.settings.OhosPreferencesSettingsimportcom.russhwolf.settings.SettingsactualfuncreateSettings():SettingsOhosPreferencesSettings.Factory(ohos.cmp.settings).create(demo)链接配置composeApp/build.gradle.kts——关键就是补上-lohpreferencesohosTarget.binaries.sharedLib{baseNameknexport(libs.compose.multiplatform.export)linkerOpts(-lz)// multiplatform-settings 的 ohos actual 依赖系统 Preferences 库linkerOpts(-lohpreferences)// CPF 统一渲染需要的系统库-lnative_drawing / -lace_napi.z / -lhilog_ndk.z 等同 reorderable}依赖声明commonMaincommonMain.dependencies{// ... compose 全家桶 ...implementation(com.russhwolf:multiplatform-settings:1.3.0-ohos.1)// ← mavenLocal 鸿蒙切片}别忘了settings.gradle.kts里启用mavenLocal()优先于远程仓库。3.5 写一个能验证持久化的 KV 读写 demoUI 不是随便摆几个按钮——它要能证明数据真的落盘了。核心逻辑ComposableinternalfunApp(){valsettingsremember{createSettings()}varkvCountbyremember{mutableIntStateOf(0)}varentriesbyremember{mutableStateOf(listOfString())}// ...funrefresh(){kvCountsettings.size entriessettings.keys.map{k-$k${readForDisplay(settings,k)}}}funput(){when(type){ValueType.STRING-settings.putString(key,value)ValueType.INT-settings.putInt(key,value.toIntOrNull()?:0)ValueType.BOOLEAN-settings.putBoolean(key,boolValue)}refresh()}funreopen(){valreopenedcreateSettings()// ← 重新打开同名 storekvCountreopened.size entriesreopened.keys.map{k-$k${readForDisplay(reopened,k)}}// Put 后 Reopen 的 size 不变 Flush 已落盘}}设计要点支持三种类型String/Int/Boolean用FilterChip切换覆盖OH_Preferences的三类基本读写状态卡实时显示store/size/get/has四个值一眼看到每次操作结果快照区列出当前所有键值对settings.keys 类型标注Reopen按钮是验证持久化的关键重新createSettings()打开同名 store若 size 不变说明OH_Preferences_Flush已把数据写盘否则只是内存态。3.6 编译、打包、签名、安装和 reorderable 同一套流程不再赘述坑点详见上一篇 FAQ.\gradlew.bat :composeApp:publishDebugBinariesToHarmonyApp# 产出双 ABI libkn.so 到 harmonyApp/entry/libscdharmonyApp ohpminstallhvigor assembleHap# 打包 entry-default-unsigned.hapjava-jarhap-sign-tool.jar sign-app...# 离线签名 → entry-signed.haphdcinstallentry-signed.hap hdc shell aa start-bohos.cmp.settings-aEntryAbilitybundleName用ohos.cmp.settings和Platform.ohos.kt里的Factory(ohos.cmp.settings)对齐。四、验证效果4.1 构建并安装publishDebugBinariesToHarmonyApp产出harmonyApp/entry/libs/{arm64-v8a,x86_64}/libkn.sohvigor 打包 hap-sign-tool 三级证书链离线签名得到entry-signed.haphdc install安装到模拟器aa start拉起。4.2 模拟器运行效果图 0 DevEco Studio 开发环境Pura X 模拟器HarmonyOS 7.0.0 / API 26运行 CMP Settings demo右侧 DevEco 面板可见图 1 demo 首屏标题 状态卡storedemo / size / status / get / has key/value 输入 类型切换 Chip Put/Get/Has/Remove/Clear/Reopen 按钮 底部快照区。Material 3 主题图 2 写入填入 keynickname、valueOpenHarmony选 String 类型点 Put状态卡显示put nickname ok快照区出现nickname OpenHarmony (string)size 变为 1图 3 持久化模拟器特写点 Reopen 重新打开同名 store状态卡显示reopen: size1 (persisted)快照区仍是nickname OpenHarmony (string)——证明 OH_Preferences_Flush 已落盘非内存态图 4 清空点 Clear 后状态卡storedemo size0、get → null (missing)、has → false快照区(empty)——证明 Clear 生效且未命中路径返回正确的失败态。底部验证点提示也随 UI 滚动露出功能验证putString/putInt/putBoolean→ 状态卡put ok快照区新增对应类型条目getStringOrNull/getIntOrNull/getBooleanOrNull→ 命中返回值类型未命中返回null (missing)hasKey→has → true/falseremove→ 快照区条目消失size减 1clear→ 快照区清空size归 0get/has返回失败态见图 4reopen→ 重新打开 store 后size与内容不变证明数据已持久化到鸿蒙 Preferences见图 3五、FAQ适配过程与使用问题Q1Unresolved reference: OhosPreferencesSettings但Settings接口能解析这是「孤儿源集」问题。0.4.0 工具链的applyDefaultHierarchyTemplate()不含 ohos 组合src/ohosMain/kotlin/没被编译进 klib。修复见 3.3在库的sourceSets里显式maybeCreate(ohosMain)并让ohosArm64Main/ohosX64Main都dependsOn它。先用klib contents确认产物里是否真的没有目标类再排查源集层级。Q2链接报undefined reference to OH_Preferences_*没在sharedLib的linkerOpts里加-lohpreferences。cinterop 只负责生成 Kotlin 绑定最终的 native 符号仍要在链接时解析到系统库。Q3OH_Preferences_Open返回错误 / 打开失败OhosPreferencesSettings.Factory(bundleName)的bundleName与harmonyApp/AppScope/app.json5的bundleName不一致。两者必须完全相等鸿蒙按 bundleName 隔离 Preferences 存储目录。Q4Put 之后杀进程重进数据没了OH_Preferences的写是异步缓冲的库内部会在合适时机Flush。若担心丢数据确认走的是OhosPreferencesSettings它封装了 flush 逻辑而非裸调 C API。demo 里的Reopen按钮就是验证这一点的。Q5cinterop 解析 SDK 全量头报一堆无关符号错别用全量 SDK 头裁剪一个只含所需函数的最小头本 fork 的prefs_min.h.def里headers prefs_min.hcompilerOpts -I.。这是鸿蒙 KMP 库适配的常见技巧。Q6换工具链版本后又要重编是。鸿蒙定制 Kotlin 的 klib 没有跨编译器版本的 ABI 兼容保证。每次 demo 换 Kotlin 版本如0.4.0→1.0.0所有本地重建的库都要跟着对齐重编并重新publishToMavenLocal。六、相关链接欢迎加入 CPF-KMP-CMP 鸿蒙社区CPF-KMP-CMP 鸿蒙社区https://atomgit.com/CPF-KMP-CMPCMP 鸿蒙开发环境搭建指南https://atomgit.com/CPF-KMP-CMP/cmp-docs华为云码道https://developer.huaweicloud.com/codeartsco.html本项目适配地址https://atomgit.com/oh-tpc/multiplatform