直接回答
模型下载的最终落位是接收字节之后的独立发布阶段。应用必须证明临时文件完整、已配置的完整性检查通过、目标路径所在卷兼容、最终文件操作确实成功,并且后续运行时能打开预期制品。Android 的 File.renameTo 会返回布尔值;API 文档列出同一挂载点等常见失败条件,并要求检查返回值。调用改名函数不代表模型已经出现在目标位置。尤其不能假设内部存储中的临时文件能够原子改名到可移动 SD 卡。选择改名之前应检查源文件和目标目录所在文件系统。如果二者不同,应使用有文档依据的复制与校验流程,确保空间足够容纳两份文件,以唯一候选路径在目标卷写入替换文件,关闭后验证目标字节及任何已配置的可信摘要,再按明确策略提升。候选路径和最终制品路径不得覆盖唯一可用的旧模型;在目标和应用状态提交前,源文件与旧模型保持不变。API 26 及后续版本的 Files.move 搭配 StandardCopyOption.ATOMIC_MOVE 请求文件系统原子移动;不支持时应把异常视为移动失败,而不是隐式复制。API 合约还说明,指定 ATOMIC_MOVE 时其他选项会被忽略;如果目标已存在,究竟替换还是失败由实现决定。不要依赖 REPLACE_EXISTING 来保护旧模型,应使用唯一候选/最终路径,验证后再按明确策略提交。每次运行都记录最终目的地、临时源、操作类型、返回值或异常、源/目标卷、可用空间观测、预期制品身份、实际长度、摘要状态和清理结果。Cove 的 ModelDownloader 会将下载内容写入配置存储目录下的临时制品;如果配置了 SHA-256,则可选地验证摘要,随后调用 renameTo 指向最终目标。当前 Gemma E2B 的摘要值为空,因此不能把这条配置描述成一定会执行摘要校验。当前检查到的源码调用 renameTo 时,没有用它的布尔结果控制完成状态;这是代码观察,不是设备结果,也不是已修复声明。测试时应在传输完成但最终落位之前强制失败,模拟目标卷写满、目标不可写、源目标属于不同卷,以及复制或提升过程中进程中断。确认旧模型仍完整,应用返回可恢复状态,而不是发出 Completed。重启后不能只因曾有临时路径就相信目标制品存在;再次加载前应校验文件身份、长度和已配置摘要。如果移动 API 报告失败,不能因为源文件存在就返回成功。需要复制时,要让源和目标保持为不同文件,待目标验证并提交持久状态后再清理源文件。清理陈旧临时文件时,应按制品身份和维护策略谨慎执行,不要删除仍由其他下载任务持有的文件。界面状态应分开表示字节接收、完整性校验、最终落位与模型可用。下载完成不等于存储提升成功,更不等于运行时加载成功。测试矩阵应包括:摘要已知的测试制品、未配置摘要、错误摘要、短传输、同卷替换、跨卷拒绝或复制、空间不足、每个阶段的进程死亡、目标文件冲突,以及恢复后的第二次加载。报告结论只适用于已测文件系统和 Android 构建。安全流程是固定来源身份;下载到选定卷上的临时路径;复查长度及完整性;创建唯一命名的目标候选;验证操作成功且目标字节一致;用选定运行时打开制品;完成检查后才更新持久状态;并且在替换提交前清理临时数据但保留旧模型。本文讲的是文件落位语义,不认证 Cove 当前实现,也不支持任何全设备兼容声明。
传输已完成,但移动到最终模型路径失败
网络数据流达到预期字节数,但目标路径位于另一个外部卷,且设备空间接近不足。应用仍有一个较旧但可用的模型,不能仅因临时文件存在就报告成功。
决策
把传输完成与存储落位分成两个状态;检查实际移动或复制结果,并在切换模型就绪状态前验证最终路径。
预期结果
移动失败会保留先前可用模型并提示可恢复的存储错误;单纯传输完成不会显示成已安装、已验证和可加载。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- 临时与最终文件的卷和路径
- rename/copy 返回值与最终字节
- 失败恢复及旧模型保留结果
收到完整字节流还不够;最终落盘、完整性、运行时加载和恢复路径也必须验证。
实施流程
- 固定来源和目标身份. 记录制品版本、临时路径、目标路径、目标卷、现有文件状态、预期长度、摘要配置及拥有该临时文件的下载任务。
- 判断改名及已存在目标的语义. 核对 Android API 合约和源/目标挂载点。File.renameTo 返回布尔值,跨挂载点可能失败。API 26 起的 Files.move 与 ATOMIC_MOVE 请求原子移动;该选项会忽略其他选项,目标已存在时是替换还是失败由实现决定。使用唯一目标候选,不依赖 REPLACE_EXISTING 保护旧模型。
- 跨卷复制但不改动源文件. 需要跨卷复制时,在目标卷写入唯一候选文件,关闭后验证目标字节及任何已配置可信摘要。目标制品与应用状态提交前,源文件和旧活动模型都保持不变;若无法原子提升,就明确失败或执行独立测试过的非原子策略。
- 检查最终落位结果. 检查所用移动 API 返回的布尔值或异常,验证最终路径和内容;若操作失败或只剩临时源文件,就不能发出模型完成状态。
- 验证中断后的恢复. 在每个阶段注入进程死亡、空间不足、权限丢失和目标冲突;重新启动后再次验证候选文件,只清理陈旧且由应用拥有的临时数据。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 移动或复制明确成功且最终路径身份与字节已验证后,才发送最终模型状态。
- 显式测试跨卷行为;原子移动失败不会被描述为原子复制回退,任何复制候选都在目标端验证且提交前保留源文件与旧模型。
- 空间不足、摘要错误、短传输、中断和目标冲突均保留旧模型与用户数据。
- 日志分别记录字节接收、摘要验证、最终落位、运行时加载和清理,不声称未验证支持。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 调用 rename 后忽略结果 | 乐观落位 | 不安全:API 可能失败,最终只剩临时文件。 |
| 检查同挂载点改名结果 | 条件式提升 | 检查布尔结果并验证最终路径;挂载点相同本身不能证明原子性。 |
| API 26+ 使用 ATOMIC_MOVE | 请求文件系统原子移动 | 文件系统不支持时 API 可以抛错;它不承诺复制回退或可移植的替换行为。 |
| 跨卷复制到新候选文件 | 复制、验证后再提升 | 这是独立的非原子流程:保留源和旧模型,验证目标字节,再按明确策略提交。 |
| 验证前替换当前模型 | 破坏性更新 | 会在新文件完整可用前移除旧制品。 |
需要避免的失败模式
- 忽略 renameTo 的返回值
- 假定内部存储与 SD 卡之间可原子改名
- 校验新文件前删除唯一可用旧制品
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android API — File.renameTo 及检查操作结果
- Android API 26+ — Files.move 文件操作合约
- Android API 26+ — StandardCopyOption.ATOMIC_MOVE
- Cove ModelDownloader 临时文件和最终落位流程(提交 0ec9e8f)
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
File.renameTo 能保证原子移动吗?
不能。Android 文档列出可能失败的情况,包括路径处于不同挂载点,并要求检查布尔返回值。不能假定跨卷改名具有原子语义,也不能因为源文件仍存在就报告成功。
目标文件已存在时 ATOMIC_MOVE 会怎样处理?
API 26 起 Files.move 搭配 StandardCopyOption.ATOMIC_MOVE 请求原子操作;不支持时可能抛出 AtomicMoveNotSupportedException。包括 REPLACE_EXISTING 在内的其他选项会被忽略,目标替换还是操作失败由实现决定。应使用唯一候选/最终制品路径,验证并提交前保留旧模型,否则拒绝更新;原子移动失败不会自动变成原子复制。
下载完成等于模型已安装吗?
不等于。还需通过完整性检查、最终落位成功,并由运行时打开文件,之后才可以报告模型就绪。网络传输结束仅代表字节接收阶段完成。
移动新文件前能否先删除旧模型?
如果旧模型是唯一可用副本,就不能。等替换文件在最终目录验证通过并且应用状态提交后,再考虑清理旧文件,以免失败时用户失去可用版本。
Cove 会对每个 Gemma 下载做 SHA-256 校验吗?
不会。当前 Gemma E2B 的 SHA-256 配置为空。当前源码会调用 renameTo,但不使用其布尔返回值决定完成状态;这是代码观察,不代表设备上的实际结果,也不表示实现已修复。