用途
该技能解决的是“大体积词典资源是否随安装包分发”的权衡问题。示例场景中,一份约 13 MB 的日语分词词典(由 8 个 .bin 文件组成)不放入 Android APK,而是在需要时下载;桌面端由于安装体积的考量方式不同,仍将词典随类路径一起发布。整个功能通过一个 expect/actual 接缝跨平台表达。
主要能力
- 统一接口:expect object 声明三个函数
configure(directoryPath: String)、isReady(): Boolean、suspend fun download(): Result<Unit>。Android 的 actual 面向 filesDir 下的目录做真实安装;桌面与 iOS 的 actual 是一行实现(isReady() = true、download() = Result.success(Unit)),因为这两个平台无需下载。把“无事可做”建模为恒真的 actual,使上层仓库无需写平台分支。 - 就绪状态从磁盘重新推导:
isReady()检查 8 个预期文件都存在且非空(File.length() > 0同时覆盖缺失与空文件),而不使用持久化的“下载是否成功”标志,避免标志与磁盘状态失同步。 - 避免半安装目录被误判:8 个文件先流入与最终目录同级的
.staging目录,校验完整后删除旧目录,再用一次renameTo替换,真实目录不会被观察到部分填充状态。若在删除与重命名之间崩溃,目录会缺失,但download()可以从该状态干净重跑。 - 两级完整性校验,从最便宜的开始:归档 URL、预期字节数、SHA-256 三个常量声明在一起;声明的 Content-Length 与预期大小不符时在传输前即中止,之后才流式计算哈希,且不把整个归档缓冲进内存。
- 白名单式归档读取:即使归档由自己发布,每个条目名先去目录部分只留 basename,仅白名单中的 8 个名字才被打开写入,其余条目按大小跳过。
- 惰性构建不记忆失败:
by lazy { runCatching { build() }.getOrNull() }会把 null 永久缓存,因为块正常返回而非抛出。应改为手动的@Volatile缓存,仅在成功时赋值(配合synchronized与二次检查)。 - 门控功能而非选择:设置项允许用户立即选择语言,不因下载未完成而阻塞;罗马化调用在资源缺失时返回 null,渲染管线将其视为“不额外显示”。
- 单次下载在途:仓库用 Mutex 包裹下载,并在获取锁后重新检查“是否已就绪”;锁外加廉价检查只能避免双击时的无谓工作,真正的并发安全来自锁内重检。
使用方式
资料给出了用于验证实现的 grep 命令,需在 core 子模块根目录运行:
- 确认三个身份常量成组出现:
grep -n "DOWNLOAD_URL\|ARCHIVE_SHA_256\|ARCHIVE_SIZE_BYTES" \
service/lyricsService/src/androidMain/kotlin/org/simpmusic/lyrics/romanization/KuromojiDictionary.kt
通过条件:三者均为 private const val 且彼此相隔数行。
- 确认每个平台都实现了三个函数且无静默错误桩:
grep -rnE "actual (suspend )?fun (isReady|download|configure)\(" \
--include="*.kt" service/lyricsService/src
通过条件:android 文件调用真实安装器;ios 与 jvm 各自返回恒真 / 成功并附有说明原因的注释。
- 确认惰性缓存陷阱未回归:
grep -rn "by lazy" --include="*.kt" service/lyricsService/src/androidMain | grep -i "getOrNull\|runCatching"
通过条件:无输出;任何命中都是会永久缓存自身失败的消费者。
适用场景
适用于 Kotlin Multiplatform 项目中存在体积较大、仅部分用户会启用的词典或模型资源(如分词器、拼写检查、分析器),需要按平台差异化决定是否随安装包分发,并保证下载安装的完整性与并发安全。
限制
该技能聚焦单个归档、一次校验;涉及多分块与断点续传的相邻场景由另一技能覆盖。资料未提供具体收费、价格、平台支持范围或性能数据。