【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 淘汰在真机验证通过:
① 写入 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,对应 KMP2.2.21/ CMP1.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 与工具链路径:
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):
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 目标。但上游有两个实现细节会卡住:
commonMain直接把co.touchlab.stately.collections.IsoMutableSet作为队列类型;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 源码集层级
不要让 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
| 任务 | 产物 | 作用 |
|---|---|---|
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 两处都要):
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 与常见缓存库的差异
| 能力 | Cache4k | Caffeine(JVM) | Okio |
|---|---|---|---|
| 多平台 | ✅ KMP | ❌ JVM | ✅ KMP |
| 容量淘汰 | LRU | W-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 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
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
排查顺序
compileKotlinOhosArm64是否通过.klib与.module是否都在 mavenLocal- 依赖是否误放
commonMain - 失败任务名:
Compile/Link/Sign/Dokka原因完全不同 - 真机日志:
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'
七、小结与复用
| 改动类别 | 文件数 | 说明 |
|---|---|---|
| 版本/仓库 | 4 | Kotlin 2.2.21、CPF 仓库、镜像 |
| 构建约定 | 2 | ohosOnly、ohosArm64、移除 BCV/JDK23 |
| 并发集合 | 4 | 接口 + expect 工厂 + 两个 actual |
| 发布配置 | 2 | 签名开关、Dokka 开关 |
三条经验:
- 纯 Kotlin 库也可能被传递依赖卡住(stately 没有 ohos 变体)
- 抽象成接口 + expect 工厂,比给每个平台复制一份实现更省代码
- 上游 CI 用的 JDK/Dokka/签名配置,在本地适配时要能一键跳过
同类库复用步骤:
- 先查传递依赖在 CPF 仓库是否有
-ohosarm64变体(POM 404 判断) - 没有就抽接口 +
expect fun工厂,只在缺失平台写新实现 - 把缺失依赖从
commonMain下移到nonJvmMain ohosOnly跳过 apple/js/wasm/jvm,构建时间从十几分钟降到十几秒- 本地发布给签名、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.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 卡片 |
更多推荐


所有评论(0)