大家好,我是熊猫钓鱼,欢迎大家点赞和关注,谢谢!

在这里插入图片描述

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

适配后仓库地址(AtomGit):https://atomgit.com/oh-tpc/ohos_kstore

Kotlin Multiplatform 里有一类需求,看起来不起眼,但几乎每个 App 都会撞上:把一个对象存到磁盘上,下次启动再原样读回来。用户设置、未提交的草稿、离线缓存、上次停留的位置,最后都会落到这件事上。KStore 就是解决这件事的轻量方案——一个基于 kotlinx.coroutines、kotlinx.serialization 和 kotlinx.io 的 KMP 存储库,提供带互斥锁的读写、内存缓存、默认值和迁移支持。

但它和所有 KMP 库一样,第一道坎是同一个:官方发布物里没有 ohosArm64 的产物。你在 commonMain 里写下 implementation("io.github.xxfast:kstore-file:1.1.0"),Gradle 解析依赖时会直接告诉你找不到匹配的 variant。不是网络问题,也不是版本问题——这个库从来没有为 OpenHarmony 这个 target 编译过。

摘要

本文完整记录了将 KMP 存储库 KStore 适配到 OpenHarmony 的实战过程。文章从选型动机出发,说明 KStore 为何适合作为适配样本;随后梳理了库的源码结构,并分六步展开适配:接入 HarmonyOS Kotlin 定制版、声明 ohosArm64 target、处理沙箱路径、导出能力给 ArkTS、打包 HAR 接入鸿蒙工程、真机操作验证。文章重点剖析了本次适配真正的坑——沙箱路径问题,并给出了"目录从应用层拿、再传给 KMP 层"的解决方案。最后附上八条踩坑清单与小结,为同类 KMP 库的鸿蒙化适配提供参考。

目录

一、为什么挑 KStore 下手

KStore 特别适合作为一次"从 0 到 1"的适配样本,原因有三个。

第一,它足够小。kstore 加 kstore-file 两个模块的核心代码不到四百行,KStore<T> 的公开 API 只有 get / set / update / delete / reset / updates 六个入口,边界非常清晰,不会有读不完的源码树。

第二,它的难点集中且极具代表性。KStore 的代码里没有任何"鸿蒙特有问题",真正卡住适配的是一个隐含假设:它要求调用方显式给一个 Path,并且默认这个路径是可写的。Android 上你写 context.filesDir,iOS 上你写 NSDocumentDirectory,Desktop 上你写 user.home——各平台都有一个"众所周知"的落脚点。而 OpenHarmony 应用的沙箱目录是运行时由框架分配的,编译期不可知,KMP 侧更拿不到。这个缺口和我们之前适配 kotlinx-datetime 时遇到的"沙箱里没有 /usr/share/zoneinfo"是同一类问题:库假设平台会提供某个东西,而鸿蒙沙箱不提供。

第三,适配完成后真机上一眼就能验证。页面上显示"磁盘上的值",杀进程、重启、再打开——值还在,就说明落盘链路真的通了,不需要复杂的交互就能确认结果。

这篇文章记录的是完整过程:从 fork 源码、接入 HarmonyOS Kotlin 定制版、声明 ohosArm64 target,到处理沙箱路径、导出符号给 ArkTS、打成 HAR 放进鸿蒙工程,最后在真机上跑通并验证。

二、先摸清库的源码结构,再动手

动手改之前先看清楚这个库怎么组织,否则很容易在错误的地方加代码。KStore 1.1.0 的源码分成三块:

kstore(common 模块) 放的是全部对外 API 和与平台无关的逻辑。KStore<T> 内部用一个 Mutex 串行化读写,用一个 MutableStateFlow 做内存缓存,updates 这个 Flow 每次订阅都会先强制从磁盘重读一次。这些逻辑不依赖任何平台能力,因此在所有 target 上共用同一份实现。这也是适配工作量看起来不大的原因——绝大部分代码不需要碰。

kstore-file 放的是"把值落到文件"这件事。核心是 FileCodec<T>:

public class FileCodec<T : @Serializable Any>(
  private val file: Path,
  private val tempFile: Path,
  private val json: Json,
  private val serializer: KSerializer<T>,
) : Codec<T>

它用的是 kotlinx.io 的 SystemFileSystem,不是 okio——这一点值得留意,因为社区里 kotlinx-io 本体已经有人在 OpenHarmony 上适配过了,KStore 恰好站在这块地基上。

FileCodec 的写入策略是"先写临时文件、再原子改名":值写到 tempFile,成功后用 SystemFileSystem.atomicMove 把它移到目标文件;失败则删掉临时文件。临时文件由 uniqueTempFile(file) 生成,形如 <file>.<uuid>.temp,和目标文件在同一个目录下——这不是随手写的,因为 rename 只有在同一个文件系统内才是原子的。这个细节后面会变成一个坑。

kstore-storage 是浏览器侧的实现(localStorage),和 OpenHarmony 无关,本次不涉及。

还有一处关键设计,在 kstore/src/commonMain/kotlin/io/github/xxfast/kstore/utils/Dispatchers.kt:

internal expect val StoreDispatcher: CoroutineDispatcher

KStore 把"文件 IO 跑在哪个调度器上"抽象成了 expect。而它的 actual 是按平台逐个写的——androidMain、appleMain、desktopMain、jsMain、wasmJsMain、linuxMain、windowsMain 各有一份 Dispatchers.kt,没有 nativeMain 兜底。这意味着每新增一个 target,就必然缺一个 actual。这正是我们要补的第一块。

三、第一步:接入 HarmonyOS Kotlin 定制版

这是整个适配的前置条件,也最容易被忽略。ohosArm64() 这个 target 在 Kotlin 官方主线发行版里并不存在,它是 OpenHarmony 适配生态里的定制能力。如果你用官方 Kotlin 插件直接写 ohosArm64(),Gradle 会报 Unresolved reference,因为插件根本不认识这个 target 名字。

所以第一步是把工程使用的 Kotlin 切到 HarmonyOS Kotlin 定制版(当前对应 Kotlin 2.2.21-1.0.0 这一发行线),并在 settings.gradle.kts 里把插件仓库指向 KMP/CMP 鸿蒙化发行版对应的仓库:

// settings.gradle.kts
pluginManagement {
    repositories {
        // HarmonyOS Kotlin 定制版插件仓库(地址见 CPF-KMP-CMP 发布说明)
        maven("https://atomgit.com/CPF-KMP-CMP")
        gradlePluginPortal()
        mavenCentral()
        google()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
        google()
    }
}

这里有一个很容易踩的版本错配。KStore 上游的 gradle/libs.versions.toml 里写的是:

kotlin = "2.4.10"
kotlinx-coroutines = "1.11.0"
kotlinx-serialization = "1.11.0"
kotlinx-io = "0.9.1"

这些数字不能照抄。定制版 Kotlin 与它配套的 coroutines / serialization / io 版本是绑定的,必须整体降到定制版对应的版本线上,具体版本号以 CPF-KMP-CMP 的发布说明为准。如果你只把 kotlin 改掉、其余保留上游版本,配置阶段就会因为 klib 版本不兼容而失败。

环境上我用的是 DevEco Studio 26.0.0 Release + JDK 21 + Gradle 8.14.1,真机 ROM 为 HarmonyOS 6.1 以上。定制版 Kotlin 与 DevEco Studio 之间有配套关系,不建议随意混搭。

还有一个环境上的坑要先处理掉:KStore 上游同时声明了 Android target,用的是 com.android.kotlin.multiplatform.library 插件,要求 AGP 9.3.1 和 compileSdk = 36。如果构建机上没有装 Android SDK,./gradlew :kstore:compileKotlinOhosArm64 会在配置阶段就报错,而不是在编译 Kotlin/Native 时报错。先装好 Android SDK,或者临时把两个模块里的 android { } 块注掉再编译。

我的开发界面如下:
在这里插入图片描述
调试界面如下:
在这里插入图片描述

四、第二步:声明 target,然后让编译器告诉你缺什么

插件就位之后,在 kstore/build.gradle.kts 和 kstore-file/build.gradle.kts 里补上 target 声明。这里我刻意没有一次性写完所有适配代码,而是先只加 target,把"缺什么"交给编译器报出来——这是 KMP 适配里最高效的做法,比对着源码猜要准得多。

// kstore/build.gradle.kts
kotlin {
    explicitApi()
    applyDefaultHierarchyTemplate()

    // ……上游已有的 android / desktop / js / wasmJs / apple / linuxX64 / windows 保持不变

    // 本次新增:OpenHarmony
    ohosArm64()
}

注意 applyDefaultHierarchyTemplate() 这一行是上游本来就有的。ohosArm64 作为 native target 会被并入 nativeMain 分组——但前面说过,KStore 的 StoreDispatcher 是按平台叶子逐个实现的,没有 nativeMain 兜底,所以该补的还是 ohosArm64Main 这一层。

kstore-file 模块同样加上 ohosArm64()。至于 kstore-storage(浏览器侧),不声明即可。

声明完成后跑一次编译:

./gradlew :kstore:compileKotlinOhosArm64

Kotlin/Native 的编译任务命名规则是 compileKotlin<首字母大写的 target 名>。第一次编译会失败,报出:

Expected declaration 'StoreDispatcher' has no actual declaration in module <kstore> for target ohosArm64

这就是我们等的那条错误。补上实现——直接照 linuxMain 那一份写,OpenHarmony 的运行时是 POSIX 兼容的,文件 IO 的调度器语义和 Linux 一致:

// kstore/src/ohosArm64Main/kotlin/io/github/xxfast/kstore/utils/Dispatchers.kt
package io.github.xxfast.kstore.utils

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO

internal actual val StoreDispatcher: CoroutineDispatcher = Dispatchers.IO

叶子 source set(ohosArm64Main)在任何情况下都会被 Kotlin 自动创建,所以这个文件直接放进去就行,不需要额外改 sourceSets 块。

再编一次,通过。target 就算接上了。

五、第三步:沙箱路径——本次适配真正的坑

编译通过之后,真正的麻烦才开始。写一个最小验证:

val store = storeOf<Note>(file = Path("kstore_demo.json"))
store.set(Note(text = "hello", revision = 1))
println(store.get())   // 期望拿到刚写进去的值

结果是:要么直接抛 FileNotFoundException,要么"set 看起来成功了,get 却返回 null"。数据根本没有落到你以为的地方。

根因在第二节里已经埋下伏笔。Path("kstore_demo.json") 是一个相对路径,SystemFileSystem 会按进程的当前工作目录去解析它。而 OpenHarmony 应用进程的工作目录不是一个可依赖的可写数据目录;应用真正该用的位置是沙箱里的 files 目录,形如 /data/storage/el2/base/haps/entry/files——这个路径在编译期未知,由框架在运行时按 bundle 名和模块名分配,KMP 侧既猜不出来,也没有一个跨平台 API 能拿到它。

这不是 KStore 的 bug,而是"平台没有提供它期望的数据源"。解决方案只有一条:目录从应用层拿,再传给 KMP 层。 鸿蒙的应用上下文里 context.filesDir 就是这个目录,而 ArkTS 侧的 getContext(this) 能直接拿到它。

这里有个设计上的取舍值得说明。最直觉的做法是改 KStore 的公共 API,在 commonMain 里加一个 expect fun filesDir(): String。但这条路会立刻反噬:kstore 和 kstore-file 声明了 android、iOS、jvm、js、wasmJs、linux、windows 一长串 target,在 commonMain 里加一个 expect,就意味着每一个 target 都要补一个 actual,否则整条构建链全红。为一个平台的需求去改所有平台的代码,明显不划算。

所以我选择不动库的公共 API:KStore 只加 target 和 StoreDispatcher 的 actual,路径注入放在一个只编译 ohosArm64 的桥接模块里。库本身保持"调用方给路径"的原始模型,鸿蒙侧的路径来源由应用层决定。

六、第四步:把能力导出给 ArkTS

新建一个 kstore-ohos 模块,只声明 ohosArm64,依赖 kstore-file:

// kstore-ohos/build.gradle.kts
plugins {
    kotlin("multiplatform")
}

kotlin {
    ohosArm64 {
        binaries.sharedLib {
            baseName = "kmpkstore"
        }
    }

    sourceSets {
        val ohosArm64Main by getting {
            dependencies {
                implementation(project(":kstore-file"))
                implementation(libs.kotlinx.coroutines)
                implementation(libs.kotlinx.serialization.json)
            }
        }
    }
}

这里不需要 export KStore 的符号——我们只导出自己写的 @CName 函数,KStore 的代码只要被链进动态库就够了。序列化插件也不用在这个模块里单独加,上游根 build.gradle.kts 已经对 allprojects 应用了 org.jetbrains.kotlin.plugin.serialization。

桥接层的核心是这样:

// kstore-ohos/src/ohosArm64Main/kotlin/ohos/kstore/bridge/KStoreBridge.kt
package ohos.kstore.bridge

import io.github.xxfast.kstore.KStore
import io.github.xxfast.kstore.file.storeOf
import kotlinx.coroutines.runBlocking
import kotlinx.io.files.Path
import kotlinx.serialization.Serializable
import kotlin.native.CName

@Serializable
data class Note(val text: String, val revision: Int)

private var store: KStore<Note>? = null

// 沙箱目录由 ArkTS 侧通过 context.filesDir 注入。
// OpenHarmony 沙箱内没有可猜的标准数据目录,不能像 Desktop 那样用 user.home
@CName("kmp_kstore_init")
fun kstoreInit(filesDir: String): String = runCatching {
    val file = Path("$filesDir/kstore_demo.json")
    store = storeOf<Note>(file = file)
    file.toString()
}.getOrElse { "ERR: ${it.message}" }

@CName("kmp_kstore_save")
fun kstoreSave(text: String): String {
    val s = store ?: return "ERR: not initialized"
    return runCatching {
        // update 会先从磁盘读回旧值,再写回新值——
        // revision 能否累加,就是"读回来的到底是不是磁盘上的东西"的证据
        runBlocking {
            s.update { prev -> Note(text = text, revision = (prev?.revision ?: 0) + 1) }
        }
        "rev=${runBlocking { s.get() }?.revision}"
    }.getOrElse { "ERR: ${it.message}" }
}

@CName("kmp_kstore_load")
fun kstoreLoad(): String {
    val s = store ?: return ""
    return runCatching {
        val note = runBlocking { s.get() } ?: return ""
        "rev=${note.revision}|${note.text}"
    }.getOrElse { "" }
}

@CName("kmp_kstore_clear")
fun kstoreClear(): String {
    val s = store ?: return "ERR: not initialized"
    return runCatching {
        // set(null) 会把目标文件删掉,而不是写一个空文件
        runBlocking { s.set(null) }
        "ok"
    }.getOrElse { "ERR: ${it.message}" }
}

save 里用 update 而不是 set,是刻意为了验证。update 会先 read 再 write,如果 revision 能跨进程累加(第一次 1、重启后再存变 2),就说明旧值确实是从磁盘读回来的,而不是被内存缓存或硬编码兜住了。

再补 C++ 侧的 NAPI 注册:

// src/main/cpp/napi_init.cpp
#include "napi/native_api.h"
#include <string>

extern "C" const char* kmp_kstore_init(const char* filesDir);
extern "C" const char* kmp_kstore_save(const char* text);
extern "C" const char* kmp_kstore_load();
extern "C" const char* kmp_kstore_clear();

static std::string GetStringArg(napi_env env, napi_value value) {
    size_t len = 0;
    napi_get_value_string_utf8(env, value, nullptr, 0, &len);
    std::string out(len + 1, '\0');
    napi_get_value_string_utf8(env, value, out.data(), out.size(), &len);
    out.resize(len);
    return out;
}

static napi_value KstoreInit(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = { nullptr };
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    std::string dir = GetStringArg(env, args[0]);
    napi_value result;
    napi_create_string_utf8(env, kmp_kstore_init(dir.c_str()), NAPI_AUTO_LENGTH, &result);
    return result;
}

static napi_value KstoreSave(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = { nullptr };
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    std::string text = GetStringArg(env, args[0]);
    napi_value result;
    napi_create_string_utf8(env, kmp_kstore_save(text.c_str()), NAPI_AUTO_LENGTH, &result);
    return result;
}

static napi_value KstoreLoad(napi_env env, napi_callback_info info) {
    napi_value result;
    napi_create_string_utf8(env, kmp_kstore_load(), NAPI_AUTO_LENGTH, &result);
    return result;
}

static napi_value KstoreClear(napi_env env, napi_callback_info info) {
    napi_value result;
    napi_create_string_utf8(env, kmp_kstore_clear(), NAPI_AUTO_LENGTH, &result);
    return result;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        { "init",  nullptr, KstoreInit,  nullptr, nullptr, nullptr, napi_default, nullptr },
        { "save",  nullptr, KstoreSave,  nullptr, nullptr, nullptr, napi_default, nullptr },
        { "load",  nullptr, KstoreLoad,  nullptr, nullptr, nullptr, napi_default, nullptr },
        { "clear", nullptr, KstoreClear, nullptr, nullptr, nullptr, napi_default, nullptr },
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "kmpkstore",
    .nm_priv = nullptr,
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void RegisterModule(void) {
    napi_module_register(&demoModule);
}

HAR 模块的入口声明:

// Index.d.ts
export const init: (filesDir: string) => string;
export const save: (text: string) => string;
export const load: () => string;
export const clear: () => string;
// Index.ets
import nativeLib from 'libkmpkstore.so';

export const init: (filesDir: string) => string = nativeLib.init;
export const save: (text: string) => string = nativeLib.save;
export const load: () => string = nativeLib.load;
export const clear: () => string = nativeLib.clear;

七、第五步:打包 HAR 并接入鸿蒙工程

编译出来的动态库要按鸿蒙的约定放好目录,才能被正确打进 HAR:

src/main/
├── cpp/
│   ├── napi_init.cpp
│   └── types/libkmpkstore/Index.d.ts
├── ets/Index.ets
└── libs/arm64-v8a/libkmpkstore.so

HAR 模块的 oh-package.json5:

{
  "name": "ohos_kstore",
  "version": "1.0.0",
  "description": "KStore OpenHarmony 适配",
  "main": "Index.ets",
  "types": "Index.d.ts"
}

接入前建议先确认符号确实导出了,这一步能省掉大量排查时间:

llvm-nm -D libkmpkstore.so | findstr kmp_kstore

能看到 T kmp_kstore_init、T kmp_kstore_save 这几行,说明 Kotlin/Native 侧的导出是成功的。

然后在鸿蒙工程的页面里把沙箱目录注入进去:

// Index.ets(页面)
import { init, save, load, clear } from 'ohos_kstore';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  @State filePath: string = '';
  @State text: string = '';
  @State persisted: string = '(空)';

  aboutToAppear() {
    // 沙箱目录只能从应用上下文拿:编译期未知,KMP 侧更拿不到
    const filesDir: string = (getContext(this) as common.UIAbilityContext).filesDir;
    this.filePath = init(filesDir);
    this.refresh();
  }

  refresh() {
    const v: string = load();
    this.persisted = v === '' ? '(空)' : v;
  }

  build() {
    Column({ space: 12 }) {
      Text('KStore on OpenHarmony')
        .fontSize(18).fontWeight(FontWeight.Bold)
      Text(`文件:${this.filePath}`)
        .fontSize(12).fontColor('#666666')
      TextInput({ placeholder: '输入要持久化的内容' })
        .onChange((v: string) => { this.text = v })
      Row({ space: 12 }) {
        Button('保存').onClick(() => { save(this.text); this.refresh(); })
        Button('清空').onClick(() => { clear(); this.refresh(); })
      }
      Text(`磁盘上的值:${this.persisted}`)
        .fontSize(20).fontColor('#0A59F7')
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .padding(24)
  }
}

八、第六步:操作验证

部署后,页面会先显示 KMP 侧返回的实际文件路径,以及 (空)。
执行运行如下:
在这里插入图片描述

输入 hello kstore 点保存,磁盘值变为 rev=1|hello kstore。此时杀掉应用进程再重新打开,页面直接显示 rev=1|hello kstore——这一条就证明了数据是从磁盘读回来的。
在这里插入图片描述

接着再输入 第二次 点保存,值变成 rev=2|第二次;再杀进程重启,仍然是 rev=2。revision 能跨进程累加,说明 update 真的读到了磁盘上的旧值,而不是被内存里的缓存兜住。

在这里插入图片描述

点"清空"后显示 (空),点击清空后数值为空,
在这里插入图片描述

重启依然为空——因为 KStore 的 set(null) 是把文件删掉,不是写一个空文件。

再用 hdc shell 进沙箱确认文件确实存在:

hdc shell ls /data/storage/el2/base/haps/entry/files

能看到 kstore_demo.json,说明路径注入链路是真正走通的。为了排除"碰巧对上"的可能,我又做了一组对照:把注入的目录换成一个不存在的路径,init 会直接返回 ERR: ...,页面显示的路径也跟着变——这证明这个目录是真的被用上了,而不是被硬编码兜住了。

九、踩坑清单

坑一:ohosArm64() 报未定义。 十有八九是还在用 Kotlin 官方主线插件。这个 target 只在 HarmonyOS Kotlin 定制版里存在,必须先把插件版本切过去。

坑二:Kotlin 版本错配。 上游 libs.versions.toml 里的 kotlin = "2.4.10"、kotlinx-coroutines = "1.11.0"、kotlinx-serialization = "1.11.0"、kotlinx-io = "0.9.1" 都不能照抄,必须整体对齐定制版的配套版本线,否则配置阶段就因 klib 不兼容失败。

坑三:构建机没装 Android SDK。 KStore 上游带 Android target(AGP 9.3.1 / compileSdk = 36),缺 SDK 时连 compileKotlinOhosArm64 都会在配置阶段失败。先装 SDK,或临时注掉 android { } 块。

坑四:StoreDispatcher 没有 actual。 KStore 的 StoreDispatcher 是按平台叶子逐个实现的,没有 nativeMain 兜底,所以新增 target 必然缺一份 Dispatchers.kt。

坑五:路径用相对路径,写不进去或读不回来。 Path("xxx.json") 会按进程工作目录解析,而鸿蒙沙箱的 CWD 不可依赖。表现为抛 FileNotFoundException,或者"写成功了但读不回来"。正确做法是从 ArkTS 侧注入 context.filesDir,不要在 native 侧硬编码路径绕过去。

坑六:自定义 tempFile 时跨目录,事务性丢失。 FileCodec 的原子性依赖"临时文件和目标文件同目录、用 rename 改名"。如果你手改 tempFile 指到别的目录,atomicMove 会退化成 copy + delete,崩溃时可能留下半个文件。KStore 默认生成的 uniqueTempFile(file) 已经在同目录,别动它。

坑七:llvm-nm -D 能看到符号,但 ArkTS 侧 import 不到。 这是最迷惑人的一类问题。Kotlin/Native 导出的 C 符号和 ArkTS 能 import 的模块接口不是一回事——中间还隔着 NAPI 注册这一层。按符号名 → nm_modname → Index.d.ts → oh-package.json5 的 main 这个顺序逐一核对即可。

坑八:改完 Kotlin 代码,产物没更新。 Kotlin/Native 的编译缓存比较激进,遇到产物与代码不一致时先 ./gradlew clean 再重新构建,比反复找代码问题高效。

十、小结

KStore 的适配过程其实很典型:真正的难点从来不在 Kotlin 代码本身,而在平台侧的隐含假设。 这个库默认"调用方会给出一个可写的文件路径",而 Android / iOS / Desktop 恰好各有一个众所周知的数据目录;OpenHarmony 的应用沙箱目录是运行时分配的,编译期不可知。只要识别出这个假设,把路径来源换成应用层注入,问题就解决了。整个适配改动量很小——一个 actual、一个只编译 ohosArm64 的桥接模块——但如果没有想清楚这一点,就很容易在 native 侧反复折腾却始终读不到数据。

顺着这个思路往下看,还有几个值得做的方向:Decompose 的难点会落在生命周期与状态保存恢复上(它的依赖 Essenty 已经有人适配过了),kable 的难点会落在蓝牙这套纯硬件能力的桥接上。它们和路径、时区问题属于同一类——都是平台能力与库预期之间的错位。

欢迎加入 KMP/CMP 鸿蒙化社区,一起共建 OpenHarmony 跨平台生态:
https://atomgit.com/CPF-KMP-CMP

适配后仓库地址(AtomGit):
https://atomgit.com/oh-tpc/ohos_kstore


AI 工具推荐:推荐使用码道(CodeArts)辅助开发 —— https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths

环境信息:DevEco Studio 26.0.0 Release / HarmonyOS Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 / JDK 21 / 真机 ROM 6.1+ / KStore 1.1.0

Logo

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

更多推荐