直接回答
防止模型重复传输,应从定义工作身份开始,而不是只在一个界面禁用按钮。每个请求都应映射到模型键、不可变修订和传输策略。相同逻辑请求要以幂等方式入队;真正不同的模型修订必须有不同身份。发起并拥有传输的组件应与观察进度的界面分开。页面退出组合时,只应停止观察,不能暗中取消共享任务。用户明确取消时,应针对其选择的任务身份执行取消,并让所有观察者收到一致状态。底层数据文件还需要单写入者规则:即使调度器里只有一个任务,如果多个直接下载流同时写同一个临时路径,仍然会出错。 本仓库的 ModelDownloader.download 返回冷 Flow。每次收集都会重新执行 Flow 主体,因此对同一个 Flow 收集两次,并不等同于观察正在运行的那一次传输。两个收集器可能读取到相同临时文件长度,再尝试同时写入同一目标。ModelManager.startBackgroundDownload 对该方法有进程内状态保护,但这个保护无法跨进程死亡持久存在,也不会自动覆盖所有 downloadModel 或 ModelDownloader 调用路径。代码还公开了 downloadModel Flow。因此这些事实适合指导架构审查,不代表用户已经遇到重复下载,也不代表当前使用了 WorkManager。 当应用需要持久调度时,WorkManager 提供 unique-work API,规定再次以相同名称入队时的处理策略。唯一工作可以防止重复的调度记录,但不能代替制品键设计、文件锁、幂等状态转换或 HTTP 范围响应校验。唯一键必须谨慎选择:全局只用一个键会错误地串行化不相关模型变体;若键里漏掉制品修订,就可能把不同字节的传输合并。如果应用使用普通进程内协程而非持久任务,仍需要共享协调器或围绕文件写入操作的互斥规则,并明确进程重建时如何恢复。并非每次模型传输都必须使用某个固定调度器,不要这样暗示。 应明确传输所有权。Repository 或服务可以按模型身份维护状态机。调用方获得只读状态观察,enqueue、pause、resume、cancel 等命令由所有者执行。所有者串行化文件操作,并记录哪个尝试持有临时文件的写权限。如果观察者在传输开始后才连接,它应该看到当前状态,而不是发起第二次 HTTP 请求。如果观察者消失,传输应遵循已定义的产品策略,而非被界面生命周期偶然决定。用户取消时,所有观察者都应看见取消;过期 Worker 不能稍后把部分文件提升为已安装文件。 持久状态强度必须与产品承诺相匹配。内存映射只能保护当前进程。如果承诺在应用进程死亡后继续工作,就要保存传输记录或使用持久调度器,并在执行时把记录与真实临时文件重新核对。不能只信数据库:要比较制品修订、路径、字节长度和响应元数据。反过来,磁盘上有一个文件也不能证明仍有活动 Worker,或文件本身有效。如果多个应用模块共用模型目录,必须定义锁定范围是单个应用进程还是跨进程;Kotlin Mutex 不能协调相互独立的进程。 把并发作为状态机来测试。从两个 UI 入口用相同身份入队,加载中重复点击,重建 Activity,连接两个观察者,从一个观察者触发取消,再入队另一个制品。断言一个身份只由一个所有者写入部分文件,其他观察者看到一致状态,取消只产生一个终态,而且不同制品没有被合并。如果承诺持久后台执行,还要在入队与 Worker 开始之间模拟进程死亡。注入范围不匹配和版本变化,确认去重不会凌驾字节完整性策略。测试同时检查临时文件、最终文件和界面状态,因为单一进度条可能掩盖竞争写入。 任务身份和文件身份应配套。工作名称可以由制品 ID 与修订构成;文件路径也应包含修订,或通过原子方式声明唯一写入权。持久化 attempt token,避免旧 Worker 在新尝试接管之后继续写。提升最终文件前,要确认当前尝试仍是所有者且必要内容检查已经通过。如果另一请求指向同一制品,就加入现有任务或返回进行中状态。如果它指向新修订,应创建独立候选状态,并让当前可用制品留存,直到新候选通过验证。 ### 操作步骤 1. 根据模型 ID、不可变修订、存储策略及影响字节内容的格式,定义稳定传输键。说明不同模型是否允许并行下载。 2. 将全部写入统一到单一所有者。需要跨进程时采用唯一持久工作;仅在进程生命周期内工作时使用进程级协调器和互斥。观察者保持只读,控制命令显式传递身份。 3. 使入队具备幂等性。同一身份的重复请求按书面策略加入或替换现有任务;不同修订不得意外复用任务名或临时路径。 4. 每次尝试前重新核对调度状态与磁盘。确认 attempt token、文件所有权、制品修订和部分响应信息;新尝试开始前关闭并作废陈旧写入者。 5. 测试并发和生命周期竞态。覆盖多个入口、重复点击、Activity 重建、多个观察者、取消、第二个模型、进程重建、旧任务迟到完成以及最终验证。 ### 验收标准 - 对相同制品身份重复入队,不会产生两个写入同一临时文件的任务。 - 不同修订不会合并到旧任务、旧临时字节或旧校验状态。 - 观察者连接或离开不会暗中入队或取消共享传输;明确取消必须经所有者执行。 - 新尝试接管所有权后,陈旧任务无法把文件提升为可用制品。 ### 对照 | 机制 | 保护范围 | 单独使用时的限制 | |---|---|---| | WorkManager 唯一任务名 | 对相同选定键去重调度任务 | 无法阻止直接 Flow 或其他路径同时写共享文件 | | 进程内 Mutex/协调器 | 串行化单一进程的关键文件操作 | 不能处理进程死亡或另一应用进程 | | 修订级尝试与路径身份 | 隔离候选制品并控制文件所有权 | 不会自行验证远端响应或摘要 | ### 常见问题 **WorkManager 唯一工作能保证模型文件只有一个写入者吗?**不能单独保证。其他代码仍可能启动直接冷 Flow 或使用另一个调度器,所以传输所有者和目标文件写入路径也需要协调。 **所有模型下载是否应该共用一个 WorkManager 键?**不应该。全局键可能合并不同请求或阻塞无关变体。应根据模型与不可变修订构造身份,再明确制定并发策略。 **界面应该拥有并取消下载吗?**界面可以发送明确的取消命令,但不能因为观察进度就意外拥有传输生命周期。解除观察与取消共享任务是不同操作。 **进程内 Mutex 足够吗?**它只在一个仍运行的进程中串行操作,进程死亡后会消失,也无法协调独立进程。只有当这种限制符合产品持久性承诺时才够用。
两个界面请求同一模型,另一个变体仍可选择
用户在引导页和设置页连续点击下载,然后旋转手机。传输正在写临时文件,同时用户可能选择第二种模型变体。应用既需要每个制品只有一个写入者,也不能让所有模型共用一个全局锁。
决策
按不可变制品身份建立任务键,通过一个所有者管理写入,并把观察者和控制命令分开。只合并等价请求,不同修订各自作为候选。
预期结果
两个界面显示同一份权威进度,明确取消只影响该次任务,而其他模型不会共享临时文件或校验状态。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- 模型 ID 与固定修订的任务键
- 调度器、观察者和文件写入者身份
- 并发、取消与进程重建测试
收到完整字节流还不够;最终落盘、完整性、运行时加载和恢复路径也必须验证。
实施流程
- 选择稳定的传输键. 组合模型 ID、不可变修订,以及会改变字节的格式或目标策略。记录不同模型是否允许同时下载;避免全局单键,也不要在制品改变后沿用旧键。
- 由单一所有者写入文件. 让 Repository、服务或 Worker 统一管理传输。需要跨进程调度时使用唯一持久任务;否则用进程级协调器和互斥。多个直接收集器不能各自打开同一临时路径写入。
- 分离观察和控制. 界面只读观察传输状态。入队、暂停、恢复和取消都作为携带任务键的所有者命令。界面退出组合只解除观察,不应因页面销毁而意外终止共享任务。
- 将任务状态与文件重新核对. 续传前确认当前尝试令牌、制品修订、真实文件路径、偏移量及上次范围响应。作废旧写入者,不能只相信调度器元数据或磁盘上的文件。
- 测试竞态和陈旧完成. 从多个入口重复请求,连接多个观察者,旋转屏幕,触发取消,再加入另一修订并模拟旧 Worker 延迟完成。除界面状态外,也检查磁盘字节与最终文件提升所有权。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 等价请求共享一个传输所有者,不能并发写同一部分文件。
- 不同制品修订具有独立任务键、临时文件和验证状态。
- 观察者连接或离开没有隐藏的入队或取消副作用。
- 新尝试取得所有权后,旧任务不能提升文件。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 唯一持久任务 | 调度层去重 | 适用于相同身份;直接文件写入仍须协调。 |
| 进程互斥锁 | 进程内串行化 | 保护当前进程,但无法覆盖进程死亡或其他进程。 |
| 尝试令牌与修订路径 | 文件所有权和候选隔离 | 能阻止陈旧提升,但不证明网络响应来源或摘要可信。 |
需要避免的失败模式
- 仅禁用单个界面的按钮
- 让多个冷 Flow 同时写同一个临时文件
- 使用全局任务键把不同模型合并
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android persistent background work
- Android WorkManager 唯一工作与任务管理
- Android WorkManager 持久任务
- Kotlin 共享可变状态与并发
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
WorkManager 唯一任务是否能保证模型文件只有一个写入者?
不能单独保证。其他代码可能启动直接冷 Flow 或另一个调度器,因此传输所有者和目标文件写入路径也必须协调,并为每次尝试核验文件所有权。
所有模型是否应该使用同一个 WorkManager 键?
不应如此。全局键可能把不同制品请求合并,或阻塞无关变体。应用应基于模型 ID 和不可变修订派生任务身份,并明确并发策略。
界面应当拥有并取消传输吗?
界面可以发送明确取消命令,但观察进度不应使页面意外拥有传输生命周期。解除观察与取消共享任务是两个独立动作。
进程内互斥锁是否足够?
它只保护一个仍运行的进程,进程死亡后即消失,也无法协调独立进程。只有在产品无需更强持久性保证时才适用,并须写清边界。