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 淘汰在真机验证通过:

① 写入 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
工具链插件源码 / Issuehttps://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 环境版本(本次验证所用)

项版本 / 路径说明
JDK17Gradle 与 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 Studio6.0.0+(可选)仅在需要 IDE 签名/预览时使用
HarmonyOS SDKAPI 26(工具链要求 API 17+)与真机系统版本一致
目标平台ohosArm64仅真机 arm64
验证设备nova 14 Ultra / API 26真机安装运行

本机 JDK 17 与工具链路径:

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
本文所基于的 tag0.14.0
适配后源码https://atomgit.com/oh-tpc/Cache4k(ohos/Cache4K)
适配 diffcache4k-0.14.0-ohos.patch(主仓库 adapt/patches/)

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

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 定制依赖:

kotlin     = "2.2.21-1.1.0-07"
coroutines = "1.10.2-1.0.0"
atomicfu   = "0.31.0-1.0.0"

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

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 两处):

mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")

一、为什么适配

Cache4k 是纯 Kotlin 的 LRU 内存缓存库,没有平台 API 调用,理论上只需要注册
ohosArm64 目标。但上游有两个实现细节会卡住:

注册 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.02.1.0✓ 选用,升到 2.2.21-1.1.0-07
0.13.01.9.22可用但更旧

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

1.2 源码集层级

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:

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):

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:

toolchainVersion=23

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

Cannot find a Java installation ... matching: Compatible with Java 23

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

2.3 注册 ohosArm64

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

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 完全不解析它:

commonMain {
    dependencies {
        implementation(libs.coroutines.core)
        implementation(libs.atomicfu)
    }
}
findByName("nonJvmMain")?.dependencies {
    implementation(libs.stately.isoCollections)
}

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

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

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,保持原有语义:

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:

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:

if (project.providers.gradleProperty("cache4k.sign").orNull == "true") {
    signAllPublications()
}

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


三、编译与发布

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

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

产物:

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
任务产物作用
publishOhosArm64PublicationToMavenLocalcache4k-ohosarm64-0.14.0.klib编译器需要的平台库
publishToMavenLocalcache4k-0.14.0.module / .pomGradle 变体匹配所需的根模块

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

验证边界:

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

四、在鸿蒙工程中使用

4.1 引入依赖

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

mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")

依赖只能放 ohosMain:

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 基本用法

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()

带过期时间:

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 的读取

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

同一 key 的并发 loader 由 KeyedSynchronizer 串行化,避免缓存击穿;
loader 抛出的异常原样抛给调用方,不会写入缓存。

4.5 调试与类型约束

cache.asMap()   // 防御性拷贝,可安全遍历
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 典型场景与容量估算

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 控制条数,不是字节数:

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

缓存 200 条、每条 2KB 的响应约 400KB,加上对象头与哈希表通常不超过 1MB。
条数过小会频繁回源,过大浪费内存;上线前用真实数据量跑一遍。

4.8 与常见缓存库的差异

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

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


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

5.1 桥接

commonMain:

expect fun runCache4kDemo(): String

ohosMain:

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 卡片

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 构建顺序

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

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

./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 真机结果

在这里插入图片描述

① 写入 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 23C1
Unresolved reference 'co' / IsoMutableSetC2
no configured signatoryC3
Unresolved reference: dokkaC4
APP_INPUT_BLOCK / THREAD_BLOCK_3SC5
No matching variant ... ohos_arm64C6
Unresolved platforms: [android, android]C7
Could not find io.github.reactivecircus.cache4kC9
真机结果与代码不一致C10

C1. 守护进程固定 JDK 23

Cannot find a Java installation ... matching: Compatible with Java 23

仓库提交了 gradle/gradle-daemon-jvm.properties(toolchainVersion=23),
Gradle 启动守护进程前就按 23 找 JDK。删除该文件即可。

C2. stately 没有 ohos 变体

e: RealCache.kt:3:8 Unresolved reference 'co'.
e: RealCache.kt:66:29 Unresolved reference 'IsoMutableSet'.

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

co.touchlab:stately-iso-collections-ohosarm64:2.1.0 -> 404

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

C3. 本地发布卡在签名

> Task :cache4k:signOhosArm64Publication FAILED
Cannot perform signing task ... because it has no configured signatory

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

C4. ohosOnly 下 Dokka 聚合报错

e: build.gradle.kts:6:5: Unresolved reference: dokka

ohosOnly 未应用 Dokka 插件,但根脚本仍执行 dokka(project(":cache4k"))。
删除该 dependencies 块,并在约定插件里跳过 Dokka。

C5. 真机点击闪退

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 里没有产物。
两条发布命令都要执行,并用下面的命令确认:

find ~/.m2/repository/io/github/reactivecircus/cache4k -name '*.klib' -o -name '*.module'

C7. 依赖放错 source set

Unresolved platforms: [android, android]

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

C9. 工程里找不到产物

settings.gradle.kts 的 pluginManagement 与 dependencyResolutionManagement
两处都要加 mavenLocal();模板里常常是注释掉的。

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

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

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 查依赖
# 一键排查
./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'

七、小结与复用

改动类别文件数说明
版本/仓库4Kotlin 2.2.21、CPF 仓库、镜像
构建约定2ohosOnly、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 加开关

更新流程

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.ktsohosMain 增加 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 卡片
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐