【KMP for OpenHarmony】Cache4k 0.14.0 鸿蒙适配

Cache4k 0.14.0 鸿蒙适配

上游:https://github.com/ReactiveCircus/cache4k @ 0.14.0

适配产物:io.github.reactivecircus.cache4k:cache4k:0.14.0(仅 ohosArm64 变体)

Patch:cache4k-0.14.0-ohos.patch

验证设备:nova 14 Ultra,HarmonyOS API 26

一句话结论

Cache4k 0.14.0 可在 ohosArm64 编译并发布 klib。上游依赖的 stately-iso-collections

没有鸿蒙变体,改成「接口 + expect 工厂」并用 AtomicFu 锁重写并发集合后,

LRU 淘汰在真机验证通过:

text 复制代码
① 写入 3 项:[v1, v2, v3]
② 插入第 4 项后 key=1:null(已淘汰)
③ 保留:[v2, v3, v4]

〇、环境与 CPF 来源

0.1 环境搭建指引

本系列不重复官方环境搭建步骤,按 CPF(CPF-KMP-CMP 社区)官方文档准备即可:

内容 链接
快速开始(15 分钟:装工具 → 配签名 → 跑第一个鸿蒙应用) CPF 文档 · 快速开始
工具链插件安装、工程模板、运行调试 CPF 文档 · 工具链介绍
工具链插件离线安装包(KMP OHOS Support-1.0.0.zip) https://maven.eazytec-cloud.com/nexus/repository/file-storage/ohos-kmp-toolchain/1.0.0/KMP OHOS Support-1.0.0.zip
工具链插件源码 / Issue https://atomgit.com/CPF-KMP-CMP/ohos-kmp-toolchain
文档总目录 CPF 文档 · 目录

文档仓库分支名带版本号(当前 main-2.2,对应 KMP 2.2.21 / CMP 1.9.2)。

工具链换代后分支与链接会变,上面的链接若 404,从 CPF 组织首页 进 docs 仓库,切到与工具链版本匹配的分支。

0.2 环境版本(本次验证所用)

项 版本 / 路径 说明
JDK 17 Gradle 与 Kotlin/Native 链接阶段都需要
OHOS Kotlin 工具链 2.2.21-1.1.0-07 决定 Kotlin 与 klib 元数据上限
构建命令行工具 Command Line Tools 26.0.0.851 本次未安装 DevEco,用 CLT 的 hvigor/ohpm/hdc
DevEco Studio 6.0.0+(可选) 仅在需要 IDE 签名/预览时使用
HarmonyOS SDK API 26(工具链要求 API 17+) 与真机系统版本一致
目标平台 ohosArm64 仅真机 arm64
验证设备 nova 14 Ultra / API 26 真机安装运行

本机 JDK 17 与工具链路径:

bash 复制代码
export JAVA_HOME=/mnt/work/jdks/jdk-17.0.21+9
export DEVECO_SDK_HOME=/mnt/work/.kmptools-26.0.0.851/command-line-tools/sdk

Linux 上 Kotlin/Native 自带的 ld.lld 依赖 libxml2.so.2,而新版发行版只提供

libxml2.so.16;需要把工具链内的 libxml2.so.2.14.0 补到 ld.lld 的 RUNPATH 目录,

否则链接阶段报 error while loading shared libraries: libxml2.so.2。

0.3 上游地址

项 地址
上游仓库 https://github.com/ReactiveCircus/cache4k
本文所基于的 tag 0.14.0
适配后源码 https://atomgit.com/oh-tpc/Cache4k(ohos/Cache4K)
适配 diff cache4k-0.14.0-ohos.patch(主仓库 adapt/patches/)

取源命令(github.com:443 的 git 协议在本机不可达,走 codeload):

bash 复制代码
curl -sL -o cache4k.tar.gz \
  https://codeload.github.com/ReactiveCircus/cache4k/tar.gz/refs/tags/0.14.0

0.4 CPF 仓库与依赖来源

"CPF"即 CPF-KMP-CMP 社区 。Cache4k 的适配从它这里取三样东西:

鸿蒙定制 Kotlin 工具链 、已适配的 coroutines/atomicfu ,以及插件/文档。

用途 地址
组织首页(文档 / 插件 / 已适配库源码) https://atomgit.com/CPF-KMP-CMP
Maven 仓库(Gradle 里要配的那个) https://maven.eazytec-cloud.com/nexus/repository/maven-public/
Maven 浏览页(查某个库有哪些 -1.0.0 定制版本) https://maven.eazytec-cloud.com/nexus/#browse/browse:maven-releases
三方库总览(上游版本 ↔ 鸿蒙化版本对照表) CPF 文档 · 三方库
三方库示例工程 kmp-thirdlibdemo · kmp-cmp-example

本文用到的 CPF 定制依赖:

toml 复制代码
kotlin     = "2.2.21-1.1.0-07"
coroutines = "1.10.2-1.0.0"
atomicfu   = "0.31.0-1.0.0"

查询某个依赖有哪些定制版本:

bash 复制代码
curl -s "https://maven.eazytec-cloud.com/nexus/repository/maven-public/org/jetbrains/kotlinx/atomicfu/maven-metadata.xml" \
  | grep -o "<version>[^<]*</version>" | sed 's/<[^>]*>//g' | grep -- "-1\.0\.0"

Gradle 侧只需加一条仓库(settings.gradle.kts 两处):

kotlin 复制代码
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")

一、为什么适配

Cache4k 是纯 Kotlin 的 LRU 内存缓存库,没有平台 API 调用,理论上只需要注册

ohosArm64 目标。但上游有两个实现细节会卡住:
#mermaid-svg-aTaQ23NE7ZyxbumM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aTaQ23NE7ZyxbumM .error-icon{fill:#552222;}#mermaid-svg-aTaQ23NE7ZyxbumM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aTaQ23NE7ZyxbumM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aTaQ23NE7ZyxbumM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aTaQ23NE7ZyxbumM .marker.cross{stroke:#333333;}#mermaid-svg-aTaQ23NE7ZyxbumM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aTaQ23NE7ZyxbumM p{margin:0;}#mermaid-svg-aTaQ23NE7ZyxbumM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster-label text{fill:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster-label span{color:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster-label span p{background-color:transparent;}#mermaid-svg-aTaQ23NE7ZyxbumM .label text,#mermaid-svg-aTaQ23NE7ZyxbumM span{fill:#333;color:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM .node rect,#mermaid-svg-aTaQ23NE7ZyxbumM .node circle,#mermaid-svg-aTaQ23NE7ZyxbumM .node ellipse,#mermaid-svg-aTaQ23NE7ZyxbumM .node polygon,#mermaid-svg-aTaQ23NE7ZyxbumM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aTaQ23NE7ZyxbumM .rough-node .label text,#mermaid-svg-aTaQ23NE7ZyxbumM .node .label text,#mermaid-svg-aTaQ23NE7ZyxbumM .image-shape .label,#mermaid-svg-aTaQ23NE7ZyxbumM .icon-shape .label{text-anchor:middle;}#mermaid-svg-aTaQ23NE7ZyxbumM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aTaQ23NE7ZyxbumM .rough-node .label,#mermaid-svg-aTaQ23NE7ZyxbumM .node .label,#mermaid-svg-aTaQ23NE7ZyxbumM .image-shape .label,#mermaid-svg-aTaQ23NE7ZyxbumM .icon-shape .label{text-align:center;}#mermaid-svg-aTaQ23NE7ZyxbumM .node.clickable{cursor:pointer;}#mermaid-svg-aTaQ23NE7ZyxbumM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aTaQ23NE7ZyxbumM .arrowheadPath{fill:#333333;}#mermaid-svg-aTaQ23NE7ZyxbumM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aTaQ23NE7ZyxbumM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aTaQ23NE7ZyxbumM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aTaQ23NE7ZyxbumM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aTaQ23NE7ZyxbumM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aTaQ23NE7ZyxbumM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster text{fill:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM .cluster span{color:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aTaQ23NE7ZyxbumM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aTaQ23NE7ZyxbumM rect.text{fill:none;stroke-width:0;}#mermaid-svg-aTaQ23NE7ZyxbumM .icon-shape,#mermaid-svg-aTaQ23NE7ZyxbumM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aTaQ23NE7ZyxbumM .icon-shape p,#mermaid-svg-aTaQ23NE7ZyxbumM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aTaQ23NE7ZyxbumM .icon-shape .label rect,#mermaid-svg-aTaQ23NE7ZyxbumM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aTaQ23NE7ZyxbumM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aTaQ23NE7ZyxbumM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aTaQ23NE7ZyxbumM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注册 ohosArm64
并发集合 IsoMutableSet

无 ohos 变体
换成 AtomicFu 锁实现
编译 + 发布 klib

  1. commonMain 直接把 co.touchlab.stately.collections.IsoMutableSet 作为队列类型;
  2. stately-iso-collections 只发布了 jvm/js/apple/linux/mingw,没有 ohosArm64。

1.1 版本选型

工具链上限是 2.2.21(与 Apollo 篇一致):

版本 上游 Kotlin 结论
main(未发布) 2.4.20 ✗ 超出工具链上限
0.14.0 2.1.0 ✓ 选用,升到 2.2.21-1.1.0-07
0.13.0 1.9.22 可用但更旧

升 Kotlin 属于小版本内升级,风险低于降库版本。

1.2 源码集层级

#mermaid-svg-aceUXr7UtLQbSXCO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aceUXr7UtLQbSXCO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aceUXr7UtLQbSXCO .error-icon{fill:#552222;}#mermaid-svg-aceUXr7UtLQbSXCO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aceUXr7UtLQbSXCO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aceUXr7UtLQbSXCO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aceUXr7UtLQbSXCO .marker.cross{stroke:#333333;}#mermaid-svg-aceUXr7UtLQbSXCO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aceUXr7UtLQbSXCO p{margin:0;}#mermaid-svg-aceUXr7UtLQbSXCO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aceUXr7UtLQbSXCO .cluster-label text{fill:#333;}#mermaid-svg-aceUXr7UtLQbSXCO .cluster-label span{color:#333;}#mermaid-svg-aceUXr7UtLQbSXCO .cluster-label span p{background-color:transparent;}#mermaid-svg-aceUXr7UtLQbSXCO .label text,#mermaid-svg-aceUXr7UtLQbSXCO span{fill:#333;color:#333;}#mermaid-svg-aceUXr7UtLQbSXCO .node rect,#mermaid-svg-aceUXr7UtLQbSXCO .node circle,#mermaid-svg-aceUXr7UtLQbSXCO .node ellipse,#mermaid-svg-aceUXr7UtLQbSXCO .node polygon,#mermaid-svg-aceUXr7UtLQbSXCO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aceUXr7UtLQbSXCO .rough-node .label text,#mermaid-svg-aceUXr7UtLQbSXCO .node .label text,#mermaid-svg-aceUXr7UtLQbSXCO .image-shape .label,#mermaid-svg-aceUXr7UtLQbSXCO .icon-shape .label{text-anchor:middle;}#mermaid-svg-aceUXr7UtLQbSXCO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aceUXr7UtLQbSXCO .rough-node .label,#mermaid-svg-aceUXr7UtLQbSXCO .node .label,#mermaid-svg-aceUXr7UtLQbSXCO .image-shape .label,#mermaid-svg-aceUXr7UtLQbSXCO .icon-shape .label{text-align:center;}#mermaid-svg-aceUXr7UtLQbSXCO .node.clickable{cursor:pointer;}#mermaid-svg-aceUXr7UtLQbSXCO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aceUXr7UtLQbSXCO .arrowheadPath{fill:#333333;}#mermaid-svg-aceUXr7UtLQbSXCO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aceUXr7UtLQbSXCO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aceUXr7UtLQbSXCO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aceUXr7UtLQbSXCO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aceUXr7UtLQbSXCO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aceUXr7UtLQbSXCO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aceUXr7UtLQbSXCO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aceUXr7UtLQbSXCO .cluster text{fill:#333;}#mermaid-svg-aceUXr7UtLQbSXCO .cluster span{color:#333;}#mermaid-svg-aceUXr7UtLQbSXCO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aceUXr7UtLQbSXCO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aceUXr7UtLQbSXCO rect.text{fill:none;stroke-width:0;}#mermaid-svg-aceUXr7UtLQbSXCO .icon-shape,#mermaid-svg-aceUXr7UtLQbSXCO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aceUXr7UtLQbSXCO .icon-shape p,#mermaid-svg-aceUXr7UtLQbSXCO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aceUXr7UtLQbSXCO .icon-shape .label rect,#mermaid-svg-aceUXr7UtLQbSXCO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aceUXr7UtLQbSXCO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aceUXr7UtLQbSXCO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aceUXr7UtLQbSXCO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} commonMain

Cache / RealCache / ConcurrentSet 接口
nonJvmMain

stately IsoMutableSet 实现
ohosArm64Main

AtomicFu 锁实现 + ConcurrentMutableMap
js / wasmJs / apple / linux / mingw

不要 让 ohosArm64Main.dependsOn(nonJvmMain):nonJvmMain 的唯一作用就是承载

stately 依赖,一旦 ohos 继承它,Gradle 仍会为 ohos 解析 stately,编译直接失败。

所以两个平台平级,各自实现自己的并发集合。


二、改动清单

2.1 版本与仓库

gradle/libs.versions.toml:

toml 复制代码
kotlin = "2.2.21-1.1.0-07"   # was 2.1.0
coroutines = "1.10.2-1.0.0"  # was 1.10.1
atomicfu = "0.31.0-1.0.0"    # was 0.27.0

三者必须整体升级:只升 Kotlin 不升 atomicfu 会报 klib 元数据版本不一致;

只升 atomicfu 不升 coroutines 会在变体匹配阶段失败。

settings.gradle.kts 与 build-logic/settings.gradle.kts 增加 CPF 仓库与阿里云镜像

(本机直连 Maven Central 返回 403):

kotlin 复制代码
maven("https://maven.aliyun.com/repository/gradle-plugin")
maven("https://maven.aliyun.com/repository/public")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")

2.2 移除固定 JDK 23

上游提交了 gradle/gradle-daemon-jvm.properties:

properties 复制代码
toolchainVersion=23

本机没有 JDK 23,Gradle 在启动守护进程前就失败:

text 复制代码
Cannot find a Java installation ... matching: Compatible with Java 23

删除该文件,改用 JAVA_HOME 的 JDK 17。

2.3 注册 ohosArm64

上游用约定插件统一注册目标。加一个 ohosOnly 开关,避免在本机构建 apple/js/wasm:

kotlin 复制代码
val ohosOnly = project.providers.gradleProperty("cache4k.ohosOnly").orNull == "true"

if (!ohosOnly) {
    jvm { ... }
    js { ... }
    wasmJs { ... }
    iosX64(); iosArm64(); iosSimulatorArm64()
    macosX64(); macosArm64()
    tvosX64(); tvosArm64(); tvosSimulatorArm64()
    watchosX64(); watchosArm64(); watchosSimulatorArm64()
    linuxX64(); linuxArm64(); mingwX64()
}

ohosArm64()
applyDefaultHierarchyTemplate()

sourceSets {
    findByName("ohosArm64Main")?.dependsOn(commonMain.get())
}

同时移除 binary-compatibility-validator(不认识自定义目标)。

2.4 把 stately 收敛到 nonJvm

cache4k/build.gradle.kts:stately 从 commonMain 移到 nonJvmMain,

让 ohos 完全不解析它:

kotlin 复制代码
commonMain {
    dependencies {
        implementation(libs.coroutines.core)
        implementation(libs.atomicfu)
    }
}
findByName("nonJvmMain")?.dependencies {
    implementation(libs.stately.isoCollections)
}

2.5 并发集合抽象(核心改动)

commonMain 不再直接引用 stately 类型,改为接口 + expect 工厂:

kotlin 复制代码
internal interface ConcurrentSet<T> : MutableSet<T> {
    fun <R> access(block: (MutableSet<T>) -> R): R
    fun firstOrNull(): T?
}

internal expect fun <T> newConcurrentSet(): ConcurrentSet<T>

nonJvmMain 继续复用 stately,保持原有语义:

kotlin 复制代码
private class StatelyConcurrentSet<T> : IsoMutableSet<T>(), ConcurrentSet<T> {
    override fun add(element: T): Boolean = access {
        val exists = remove(element)
        it.add(element)
        !exists
    }
    override fun firstOrNull(): T? = access { it.firstOrNull() }
}
internal actual fun <T> newConcurrentSet(): ConcurrentSet<T> = StatelyConcurrentSet()

ohosArm64Main 用 AtomicFu 互斥锁 + LinkedHashSet:

kotlin 复制代码
private class LockedConcurrentSet<T> : ConcurrentSet<T> {
    private val lock = reentrantLock()
    private val delegate = LinkedHashSet<T>()

    override fun iterator(): MutableIterator<T> = lock.withLock { delegate.iterator() }

    override fun add(element: T): Boolean = lock.withLock {
        val exists = delegate.remove(element)
        delegate.add(element)
        !exists
    }

    override fun <R> access(block: (MutableSet<T>) -> R): R = lock.withLock { block(delegate) }
    override fun firstOrNull(): T? = lock.withLock { delegate.firstOrNull() }
    // size / contains / remove / clear 同理
}
internal actual fun <T> newConcurrentSet(): ConcurrentSet<T> = LockedConcurrentSet()

RealCache 里队列类型从 IsoMutableSet 改为 ConcurrentSet,构造从

ReorderingIsoMutableSet() 改为 newConcurrentSet(),其余逻辑不动。

ConcurrentMutableMap 同理:上游 nonJvmMain 用 IsoMutableMap,

ohos 侧用「AtomicFu 锁 + MutableMap」重写。

access {} 的语义是"持锁执行整段操作"。expireEntries() 依赖它做「迭代 + 删除」的原子性,

因此 ohos 实现必须把 iterator() 放在锁内返回。

2.6 发布开关

本地发布不需要签名,给 signAllPublications() 加开关,并在 ohosOnly 下跳过 Dokka:

kotlin 复制代码
if (project.providers.gradleProperty("cache4k.sign").orNull == "true") {
    signAllPublications()
}

跳过后发布耗时从 9 分钟降到 18 秒。


三、编译与发布

bash 复制代码
./gradlew -Pcache4k.ohosOnly=true :cache4k:compileKotlinOhosArm64

./gradlew -Pcache4k.ohosOnly=true \
  :cache4k:publishOhosArm64PublicationToMavenLocal \
  :cache4k:publishToMavenLocal

产物:

text 复制代码
BUILD SUCCESSFUL

~/.m2/repository/io/github/reactivecircus/cache4k/
├── cache4k/0.14.0/cache4k-0.14.0.module
└── cache4k-ohosarm64/0.14.0/cache4k-ohosarm64-0.14.0.klib
任务 产物 作用
publishOhosArm64PublicationToMavenLocal cache4k-ohosarm64-0.14.0.klib 编译器需要的平台库
publishToMavenLocal cache4k-0.14.0.module / .pom Gradle 变体匹配所需的根模块

只发前者会报 No matching variant;只发后者会找不到 klib。

验证边界:

  • ✅ ohosArm64 编译与发布
  • ✅ LRU 容量淘汰(3 条上限,插入第 4 条淘汰最久未访问)
  • ✅ 重复执行不崩溃、不阻塞 UI
  • ⬜ 过期策略(expireAfterWrite / expireAfterAccess)真机验证
  • ⬜ 多线程并发压测
  • ⬜ js/wasm/apple 等其他平台回归

四、在鸿蒙工程中使用

4.1 引入依赖

仓库(settings.gradle.kts 两处都要):

kotlin 复制代码
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")

依赖只能放 ohosMain:

kotlin 复制代码
val ohosMain = sourceSets.create("ohosMain").apply { dependsOn(commonMain.get()) }
ohosMain.dependencies {
    implementation("io.github.reactivecircus.cache4k:cache4k:0.14.0")
}

放 commonMain 会因 androidTarget 无法解析而报 Unresolved platforms: [android, android]。

4.2 基本用法

kotlin 复制代码
import io.github.reactivecircus.cache4k.Cache

val cache = Cache.Builder<Int, String>()
    .maximumCacheSize(100)
    .build()

cache.put(1, "v1")
cache.get(1)          // "v1"
cache.invalidate(1)
cache.invalidateAll()

带过期时间:

kotlin 复制代码
import kotlin.time.Duration.Companion.minutes

val cache = Cache.Builder<Long, User>()
    .maximumCacheSize(100)
    .expireAfterWrite(5.minutes)
    .expireAfterAccess(1.minutes)
    .build()

4.3 Builder 参数语义

参数 语义 边界行为
maximumCacheSize(n) 最多保留 n 条,按 LRU 淘汰 n=0 立即丢弃写入;不设置则无上限
expireAfterWrite(d) 写入后 d 过期(覆盖写重新计时) d=0 等价于不缓存
expireAfterAccess(d) 最后一次访问后 d 过期 读、写都算访问

过期是惰性 的:命中时发现过期就删;每次读写顺带扫描队首批量清理。

长时间不访问的 key 会一直占内存,直到下一次操作触发扫描。

4.4 带 loader 的读取

kotlin 复制代码
val user = cache.get(userId) {
    api.fetchUser(userId)   // 只有未命中时才执行
}

同一 key 的并发 loader 由 KeyedSynchronizer 串行化,避免缓存击穿;

loader 抛出的异常原样抛给调用方,不会写入缓存。

4.5 调试与类型约束

kotlin 复制代码
cache.asMap()   // 防御性拷贝,可安全遍历
kotlin 复制代码
public interface Cache<in Key : Any, Value : Any>
  • Key / Value 均非空
  • 要缓存"可能为空"的结果,用包装类型或哨兵对象
  • in Key 支持逆变:Cache<Any, V> 可当作 Cache<String, V> 使用

4.6 线程模型

调用方式 是否安全 说明
主线程 get/put ✅ 单次操作 O(1) 级别
后台线程批量 put ✅ 锁粒度是单次操作
在 access {} 内做耗时 IO ❌ 长时间持锁,阻塞其他线程

get(key) { loader } 是唯一 suspend 的 API;不要在 loader 里再调用同一 cache 的

invalidateAll(),可能自锁等待。

4.7 典型场景与容量估算

kotlin 复制代码
val apiCache = Cache.Builder<String, Response>()
    .maximumCacheSize(200)
    .expireAfterWrite(30.seconds)
    .build()

val userCache = Cache.Builder<Long, User>()
    .maximumCacheSize(50)
    .expireAfterAccess(5.minutes)
    .build()

maximumCacheSize 控制条数,不是字节数:

text 复制代码
近似占用 ≈ 条数 × (key + value + 每条约 100B 对象开销)

缓存 200 条、每条 2KB 的响应约 400KB,加上对象头与哈希表通常不超过 1MB。

条数过小会频繁回源,过大浪费内存;上线前用真实数据量跑一遍。

4.8 与常见缓存库的差异

能力 Cache4k Caffeine(JVM) Okio
多平台 ✅ KMP ❌ JVM ✅ KMP
容量淘汰 LRU W-TinyLFU ❌
过期策略 写后/访问后 写后/访问后/自定义 ❌
磁盘持久化 ❌ ❌ ✅(需自己实现)

Cache4k 适合「进程内、纯内存、按容量/时间淘汰」;需要落盘时组合 KVault/FileKit。


五、接入演示工程与真机验证

5.1 桥接

commonMain:

kotlin 复制代码
expect fun runCache4kDemo(): String

ohosMain:

kotlin 复制代码
actual fun runCache4kDemo(): String {
    val cache = Cache.Builder<Int, String>()
        .maximumCacheSize(3)
        .build()

    (1..3).forEach { cache.put(it, "v$it") }
    val before = (1..3).map { cache.get(it) }

    cache.put(4, "v4")            // 淘汰最久未访问的 key=1
    val evicted = cache.get(1)
    val kept = (2..4).map { cache.get(it) }

    return buildString {
        appendLine("① 写入 3 项:$before")
        appendLine("② 插入第 4 项后 key=1:${evicted ?: "null(已淘汰)"}")
        append("③ 保留:$kept")
    }
}

androidMain 放占位实现,保证 commonMain 可解析。

5.2 UI 卡片

kotlin 复制代码
Button(
    enabled = !running,
    onClick = {
        running = true
        scope.launch {
            val text = withContext(Dispatchers.Default) { runCache4kDemo() }
            result = text
            running = false
        }
    },
) { Text(if (running) "运行中..." else "运行 Cache4k 演示") }

不要在主线程 runBlocking,否则会 APP_INPUT_BLOCK 被系统强杀(详见第六节 C5)。

5.3 构建顺序

text 复制代码
Gradle(Kotlin/Native)→ libkn.so + libkn_api.h
        ↓
hvigor(ArkTS + NAPI)→ entry-default-signed.hap

顺序不能反,且要核对时间戳:

bash 复制代码
./gradlew :composeApp:publishDebugBinariesToHarmonyApp
stat -c '%y %n' harmonyApp/entry/libs/arm64-v8a/libkn.so

cd harmonyApp
hvigorw --mode module -p module=entry@default assembleHap
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.example.harmonyapp

只重打 HAP 不重编 KMP,行为不会变化------这是最常见的"改了没效果"。

5.4 真机结果

text 复制代码
① 写入 3 项:[v1, v2, v3]
② 插入第 4 项后 key=1:null(已淘汰)
③ 保留:[v2, v3, v4]

说明 LRU 顺序维护正确(重复 add 移到末尾)、容量淘汰生效、连续点击不闪退。

5.5 集成检查清单

  • compileKotlinOhosArm64 通过
  • klib + 根模块都已发布到 mavenLocal
  • 依赖放在 ohosMain
  • expect/actual 双端齐全
  • 真机 LRU 淘汰结果正确
  • patch 可对 v0.14.0 干净应用

六、排障

报错关键词 编号
Cannot find a Java installation ... Compatible with Java 23 C1
Unresolved reference 'co' / IsoMutableSet C2
no configured signatory C3
Unresolved reference: dokka C4
APP_INPUT_BLOCK / THREAD_BLOCK_3S C5
No matching variant ... ohos_arm64 C6
Unresolved platforms: [android, android] C7
Could not find io.github.reactivecircus.cache4k C9
真机结果与代码不一致 C10

C1. 守护进程固定 JDK 23

text 复制代码
Cannot find a Java installation ... matching: Compatible with Java 23

仓库提交了 gradle/gradle-daemon-jvm.properties(toolchainVersion=23),

Gradle 启动守护进程前就按 23 找 JDK。删除该文件即可。

C2. stately 没有 ohos 变体

text 复制代码
e: RealCache.kt:3:8 Unresolved reference 'co'.
e: RealCache.kt:66:29 Unresolved reference 'IsoMutableSet'.

stately-iso-collections:2.1.0 没有 ohosArm64 变体:

text 复制代码
co.touchlab:stately-iso-collections-ohosarm64:2.1.0 -> 404

按 §2.4/§2.5 抽象 ConcurrentSet,并把 stately 下移到 nonJvmMain。

C3. 本地发布卡在签名

text 复制代码
> Task :cache4k:signOhosArm64Publication FAILED
Cannot perform signing task ... because it has no configured signatory

给 signAllPublications() 加开关,只在发中央仓库时启用。

C4. ohosOnly 下 Dokka 聚合报错

text 复制代码
e: build.gradle.kts:6:5: Unresolved reference: dokka

ohosOnly 未应用 Dokka 插件,但根脚本仍执行 dokka(project(":cache4k"))。

删除该 dependencies 块,并在约定插件里跳过 Dokka。

C5. 真机点击闪退

text 复制代码
AppDfr: NotifyAppFault:APP_INPUT_BLOCK, processExit:1
AppDfr: submit reportAppFreeze, name:THREAD_BLOCK_3S

onClick 里同步执行耗时操作,阻塞 UI 主线程。

改用 scope.launch { withContext(Dispatchers.Default) { ... } }。

C6. 依赖没有 ohos 变体

只发了平台 klib,没发根模块;或 mavenLocal 里没有产物。

两条发布命令都要执行,并用下面的命令确认:

bash 复制代码
find ~/.m2/repository/io/github/reactivecircus/cache4k -name '*.klib' -o -name '*.module'

C7. 依赖放错 source set

text 复制代码
Unresolved platforms: [android, android]

产物只有 ohosArm64 变体,必须放 ohosMain。

C9. 工程里找不到产物

settings.gradle.kts 的 pluginManagement 与 dependencyResolutionManagement

两处都要加 mavenLocal();模板里常常是注释掉的。

C10. 真机结果与代码不一致

只重打了 HAP,没重编 libkn.so。核对时间戳:

bash 复制代码
stat -c '%y %n' harmonyApp/entry/libs/arm64-v8a/libkn.so

排查顺序

  1. compileKotlinOhosArm64 是否通过
  2. .klib 与 .module 是否都在 mavenLocal
  3. 依赖是否误放 commonMain
  4. 失败任务名:Compile / Link / Sign / Dokka 原因完全不同
  5. 真机日志:APP_INPUT_BLOCK 查 UI 线程,No matching variant 查依赖
bash 复制代码
# 一键排查
./gradlew -Pcache4k.ohosOnly=true :cache4k:compileKotlinOhosArm64 --no-daemon
curl -s -o /dev/null -w '%{http_code}\n' \
  "https://maven.eazytec-cloud.com/nexus/repository/maven-public/co/touchlab/stately-iso-collections-ohosarm64/2.1.0/stately-iso-collections-ohosarm64-2.1.0.pom"
find ~/.m2/repository/io/github/reactivecircus/cache4k -name '*.klib' -o -name '*.module'
hdc shell ps -ef | grep com.example.harmonyapp
hdc shell hilog -x | grep -E 'APP_INPUT_BLOCK|THREAD_BLOCK_3S'

七、小结与复用

改动类别 文件数 说明
版本/仓库 4 Kotlin 2.2.21、CPF 仓库、镜像
构建约定 2 ohosOnly、ohosArm64、移除 BCV/JDK23
并发集合 4 接口 + expect 工厂 + 两个 actual
发布配置 2 签名开关、Dokka 开关

三条经验:

  1. 纯 Kotlin 库也可能被传递依赖卡住(stately 没有 ohos 变体)
  2. 抽象成接口 + expect 工厂,比给每个平台复制一份实现更省代码
  3. 上游 CI 用的 JDK/Dokka/签名配置,在本地适配时要能一键跳过

同类库复用步骤:

  1. 先查传递依赖在 CPF 仓库是否有 -ohosarm64 变体(POM 404 判断)
  2. 没有就抽接口 + expect fun 工厂,只在缺失平台写新实现
  3. 把缺失依赖从 commonMain 下移到 nonJvmMain
  4. ohosOnly 跳过 apple/js/wasm/jvm,构建时间从十几分钟降到十几秒
  5. 本地发布给签名、Dokka 加开关

更新流程

bash 复制代码
tar -xzf cache4k-<ver>.tar.gz -C adapt/cache4k-<ver> --strip-components=1
git -C adapt/cache4k-<ver> init && git -C adapt/cache4k-<ver> add -A
git -C adapt/cache4k-<ver> commit -m "baseline"
git -C adapt/cache4k-<ver> apply ../../adapt/patches/cache4k-0.14.0-ohos.patch

./gradlew -Pcache4k.ohosOnly=true :cache4k:publishToMavenLocal

升级时重点检查:stately 是否仍无 ohos 变体、ConcurrentSet 调用点是否变化、

Cache.Builder API 是否兼容。

本次改动文件(KMPCMPOHOS)

文件 改动
composeApp/build.gradle.kts ohosMain 增加 cache4k 依赖
composeApp/src/commonMain/.../ApolloDemo.kt 增加 expect fun runCache4kDemo()
composeApp/src/ohosMain/.../Cache4kDemo.ohos.kt 新增 LRU 演示实现
composeApp/src/androidMain/.../Cache4kDemo.android.kt 新增占位实现
composeApp/src/commonMain/.../App.kt 新增 Cache4k 卡片
相关推荐
SZYSK3 小时前
OpenHarmony 上读卡、推理和双屏刷新抢 CPU:核验机与消费机的调度差
物联网·算法·harmonyos·1024程序员节
lqj_本人3 小时前
开源鸿蒙平台 KMP_CMP 三方库「URI KMP」适配全流程
华为·开源·harmonyos
YB13753 小时前
kotlin与函数式编程
kotlin
hhhjhl3 小时前
React Native 三方库「react-native-sha256」的鸿蒙化适配指南
react native·华为·harmonyos
2601_949970694 小时前
React Native 三方库「react-native-version-check」的鸿蒙化适配指南
react native·华为·harmonyos
行者-全栈开发4 小时前
【鸿蒙心迹】鸿蒙 PC 应用上架实战:分布式 Markdown 笔记从开发到过审全流程
harmonyos·markdown·arkui·应用上架·鸿蒙 pc·分布式数据同步
hhhjhl4 小时前
React Native 三方库「react-native-proximity」的鸿蒙化适配指南
react native·华为·harmonyos
2601_949970694 小时前
React Native 三方库「react-native-media-notification」的鸿蒙化适配指南
react native·华为·harmonyos
GitCode官方5 小时前
从大语言模型到具身智能:百度百舸 LoongForge 入驻 AtomGit
人工智能·react native·百度·harmonyos·atomgit