
几万个文件做热更新不是把包拆得越碎越好也不是服务器比对一下 MD5 就能完事。这里有一堆前置条件、依赖关系和边界情况处理不好增量更新反而比全量更新还痛苦。这篇文章我从工程落地的角度把 Unity 资源热更新里增量方案的关键环节全部拆开讲从分包设计到文件指纹从清单生成到加载流程再到我实际踩过的坑一次性说清楚。我先说下这篇文章适合谁。如果你正在做 Unity 项目的热更新项目资源已经多到按万计更新一次全量包要下几百 MB团队开始考虑做增量更新但不知道怎么下手或者你已经做了增量但发现对比半天下载量没降多少、加载反而更卡、动不动就“依赖缺失”那这篇文章就是写给你的。1. 资源热更新的本质先想清楚你要做的是哪种“增量”很多团队一上来就堆代码写一堆 http 下载、解压、替换的逻辑结果跑起来一测发现要么下载量根本降不下来要么更新完进游戏直接白屏。我见过太多这样的例子根子在于没想清楚增量更新的边界到底在哪。1.1 增量更新到底解决什么问题热更新分为两个方向一个是代码逻辑热更新一个是资源热更新。代码方面 Unity 主流方案是 ILRuntime、HybridCLR 这类解决的是 C# 程序集更新。我这边重点聊资源也就是 AssetBundle后面简称 AB以及散文件。资源热更新要解决的核心问题是已经发布的客户端在资源发生变化时不需要重新走应用商店下载整个安装包只通过网络把差异部分拉到本地。这里有个关键认知“只下载真正变动的那几个”是一个工程目标不是一句口号。要达成这个目标至少要满足三个前提客户端知道服务器上有哪些文件、版本是什么。客户端知道自己本地已经有哪些文件、版本是什么。双方有一把共同的“尺子”用来判断同一个逻辑文件是否相同。这三件事缺一件增量就是空谈。大多数方案把精力放在第三件事上——用什么哈希算法、怎么比对结果前两件没做好整个链路照样崩。1.2 全量更新、文件级增量、字节级差异的取舍热更新的差异粒度可以分三级我画个表格直接对比方案更新单位下载量实现复杂度适用场景整包更新整个安装包或整个 AB 包最大最低首次安装、大版本强制更新文件级差异单个资源文件小中等大多数资源更新场景字节级差异文件内部的变更片段最小高大文件小改动、弱网环境字节级差异就是类似 bsdiff、xdelta 这类做法把同一个大文件前后两个版本做二进制 diff只下载差异块然后在本地应用补丁。听着很美好但 Unity 资源场景下我一般不建议一上来就上这个。原因很简单AB 包内部的结构对增量算法不友好Unity 在打包时会对资源做类型压缩、对齐、序列化即使你只改了一个材质球的颜色参数生成的 AB 二进制变化可能散落在文件各个位置diff 出来往往不是一块干净的小补丁可能是几十个分散的小块。再加上加密、LZ4/LZMA 压缩的影响补丁应用的成功率和性能都很难保障。所以业内更通用、工程上更稳妥的是文件级增量。也就是标题里说的“几万个文件只下载真正变动的那几个”的正确做法按文件粒度去比对变了哪个下哪个。我下面讲的分包、指纹、清单、加载全部围绕文件级增量展开。1.3 为何“把包拆碎”不等于增量最优解有一类思路是把 AB 拆得特别碎每个小资源单独打包。理论上拆得越碎改动一个文件时其他文件不受影响。但这里有个代价包数量会爆炸。一个中型项目资源几万个文件如果你每个 Prefab、每个 Texture、每个 Material 都单独打一个 AB那 AB 数量可能跟原始文件数量一个量级甚至更多。后果是你每次构建需要处理几万个清单条目加载时依赖查询变成一场灾难AB 之间的引用关系复杂到人脑根本理不清测都测不完。而且Unity 的 AB 打包本身存在一个客观规律依赖某个资源的 AB 越多这个资源被改动时连锁需要更新的 AB 就越多。因为你加载 AB 时Unity 会递归加载它的所有依赖。如果依赖的依赖也被改动了那所有间接引用到它的 AB 都得重新下载。所以增量更新的第一步不是考虑怎么比对而是考虑怎么分包。文章的下一节专门讲这个。2. AB 分包与依赖管理增量更新的地基工程很多人把增量更新当成“下载更新文件”这个动作来理解但真正决定增量效果好坏的时刻发生在打包阶段也就是你决定怎么拆 AB 的那一刻。这是整个增量体系里最底层、也最容易被忽略的部分。2.1 分包粒度如何影响差异文件的范围分包的基本原则是把“一起加载的资源”放到同一个 AB把“生命周期不同、变更频率不同”的资源分开。举个例子。UI 界面里主界面、商城、背包、设置它们是各玩各的界面很少同时出现。如果你把所有 UI 打成一个超大 AB那么只要商城界面换了一张 Banner玩家从旧版本更新过来就需要下载整个 UI 大包里面可能包含大量完全没变化的主界面和设置界面图素。这就是典型的“包没拆到位”导致增量失效。反过来如果一个角色模型由骨骼、网格、贴图、材质、动画组成而这些资源几乎总是一起被加载你硬把它们拆成三五个 AB反而制造出复杂的依赖关系。每次改动角色衣服上的纹理其它两个 AB 的 Hash 可能没变但依赖链上的变化让整个加载逻辑变得脆弱。我的习惯是按功能模块拆每个玩法/界面模块一个到几个 AB模块之间尽量少引用公共资源。公共资源单独拆频繁被多个模块引用的图集、字体、Shader、通用 Prefab 放独立 AB尽量保证这部分“低频变更”。大资源单独拆单张超大纹理、大规模地形数据这种动不动几十 MB 的资源永远独立成包方便单独替换。为什么公共资源要单独拆因为依赖的公因数问题。Shader 这种资源几乎被所有模块依赖如果你把 Shader 和某个模块的 AB 混在一起那这个 Shader 只要变化所有引用它的模块 AB 在加载时都需要重新处理。更关键的是公共资源一旦变化在文件级增量体系里所有直接或间接引用它的包都可能会被判定为“需要重新下载”因为包之间的引用关系通常通过 manifest 固化了。2.2 依赖链是增量更新的隐形炸弹把 AB 拆好之后接下来是依赖关系。Unity 的 AB 依赖主要有两个来源一是打包时会自动生成的.manifest文件里的Dependencies列表二是运行时你通过AssetBundleManifest.GetAllDependencies或GetDirectDependencies查询到的依赖链。这里有一个容易踩坑的认知增量更新不仅要做文件级比对还要做依赖级比对。什么叫依赖级比对假设你更新了某个公共图集 AB但还有一个 UI 模块 AB 引用了这个图集里的图。虽然 UI 模块 AB 本身的资源没有任何改动文件指纹没变但因为你更新了公共图集UI 模块在运行时加载到这个旧 AB 时它引用的依赖已经被新的图集替换了而 UI 模块 AB 内部记录的依赖 Hash 可能跟新图集对不上。具体表现就是更新后 UI 界面某些图片花屏、串图、或者直接加载失败排查半天原来是公共依赖更新后旧包没跟着重打。所以正确的做法是构建时记录每个 AB 的依赖集合在服务端生成全量依赖图。做增量清单时不仅要看 AB 文件本身有没有变还要看它的依赖有没有变。依赖变了依赖它的上游即使文件字节没变客户端也必须把它拉下来重新加载。我在实际项目中是把这部分做进构建工具链的每次构建输出时生成一份depend_tree.json里面记录每个 AB 的所有直接依赖和间接依赖。客户端判断某个资源是否需要更新时先查自身 Hash再递归检查依赖 Hash只要依赖链上任意一环变了这个包就要重新下载或至少重新校验。2.3 处理变体与平台差异的注意事项Unity 的 AB 有个变体Variant机制用于实现类似“同一资源在不同平台使用不同纹理/模型”的效果。变体会让资源标识从简单的名字变成“名字变体名”很多增量系统没处理好这一点。我遇到过的典型问题iOS 和 Android 打包同一个逻辑资源依赖的变体不同导致服务端生成的清单一旦把不同平台的资源混在一起客户端拿到手的就是一份“污染清单”。要么误判更新把所有包都拉一遍要么漏判更新加载的时候资源长不对。我的解决方案是服务端清单按平台分目录iOS 和 Android 各自维护一份独立的version_list路径、Hash、依赖图全部隔离。这样增量比较在客户端发起时带去的是当前平台的平台标识只有本平台的文件参与比对互不干扰。3. 文件指纹用什么“尺子”判断文件是否变动分包和依赖解决了“哪些文件可以独立更新”的问题。接下来就是“如何判断某个文件是否变动”的问题。这部分看起来只是算个哈希但里面可选的方案和边界情况都不少。3.1 MD5、CRC32、版本号各自的适用场景文件指纹常见的做法有三种版本号、CRC、哈希MD5/SHA1/SHA256。方案判断准确性计算开销冲突风险适用场景版本号依赖人工/构建系统赋值极低人工维护易出错小团队、明文版本升级CRC32一般低有碰撞可能虽少见但存在传输校验、快速初筛MD5高中碰撞概率极低工程上可接受大多数项目的主流选择SHA256极高高几乎不可能碰撞对安全性要求极高的场景Unity 资源热更新这个场景我个人推荐直接用 MD5。它比 CRC32 可靠比 SHA256 快得多。考虑到要遍历几万个文件每个文件都要读一遍算一遍哈希性能开销必须纳入考量。几万个文件平均每个几 MB全部算一遍 MD5 大概也就几秒钟到十几秒做在构建阶段没问题但如果要每帧或者频繁做就得考虑缓存计算结果。版本号方案我不太建议作为唯一判断依据因为它依赖构建系统保证每次资源变动都要同步更新版本号。实际项目里策划改了一个图片、程序忘了触发版本号更新这种事太容易发生了。版本号可以作为一个辅助字段存在但真正的判断还是要靠文件哈希。3.2 清单文件的结构设计与生成时机增量更新不管怎么设计核心都会落到一份或多份清单文件上。客户端跟服务器沟通的第一步就是拉取并解析清单而不是直接遍询所有文件的下载地址。清单文件的设计我推荐一个稳了好多年的结构{ version: 2024.06.18.1030, platform: android, files: [ { path: ab/ui/main_ui.ab, size: 1245678, md5: a1b2c3..., dependencies: [ab/common/common_ui.ab, ab/shader/common_shader.ab] }, { path: ab/common/common_ui.ab, size: 3421567, md5: d4e5f6... } ] }这里有几个细节值得展开。path必须使用相对稳定、跟加载路径一致的格式。我见过有人用绝对路径Windows 上构建出来的路径带盘符发到服务器上安卓端就读不到了。我建议统一使用正斜杠、相对路径并且大小写敏感整个工程里不要出现Main_UI.ab和main_ui.ab同时存在的场景。version是整包资源的版本号用于做全量比对的前置判断。如果客户端本地 version 跟服务器 version 相同可以跳过大部分比对逻辑直接进游戏这是性能优化的大头。dependencies不是必须的如果你在 3.x 节说的依赖图已经在服务端算好可以不必放进清单减少网络传输体积。但如果你希望客户端本地也能做依赖判断把它带上会省事很多。生成时机要放在构建管线最后一步等所有 AB 文件落盘后用脚本遍历输出目录逐个计算 MD5再串上依赖信息统一生成清单。3.3 计算指纹时的文件读取性能注意点几万个文件逐个计算 MD5最大的瓶颈是磁盘 IO 而不是 CPU 算力。构建机上如果资源还在机械硬盘上遍历几万个文件可能要慢很多。我遇到过一个小坑构建阶段算 MD5 时把不必要的文件也读了一遍。比如 AB 的同名.manifest文件、调试用的 pdb、临时生成的 log 文件。这会让清单体积变大也会让增量判定出现“本来不该下载的文件也提示下载”的现象。更稳的写法是在构建时保留一张file_allowlist只有这张白名单里的路径才参与打包和指纹计算。白名单之外的文件压根不发布不比对。还有一个优化思路AB 构建输出时Unity 会自动生成.manifest文件里面也包含每个资源文件的 CRC 和 Hash 信息。你可以读这个 manifest 快速拿到内容指纹不需要自己把整个文件读一遍重新计算。但这种做法依赖构建环境如果用外部工具对 AB 做过压缩、加解密、补丁处理manifest 里的信息就失效了。我自己是构建后直接把 AB 重新读一遍算 MD5放在打包流程的凌晨任务里慢一点无所谓胜在结果真实可靠。4. 完整实现流程从启动检查到增量下载与装载前面聊的都是设计方案现在我们把整个链路串起来。增量更新的客户端流程并不复杂但每一步都有很多细节容易出问题我按实际运行顺序逐个讲。4.1 客户端启动后的更新时序拆解一次完整的增量更新在客户端上大致是下面这个流程。我按实际工程经验把它拆成八个步骤来写每一步你都能对上号启动热更管理器读取本地存储的本地版本号和清单文件。这个清单是上次更新完成后写入的。向服务器请求远程版本信息带上去的平台标识和本地版本号。这一步一般走 HTTP GET 一个配置接口返回远程版本号和远程清单的下载地址。如果远程版本号和本地版本号相同说明没有更新。跳到第 8 步。下载远程清单文件。最好是带版本号命名例如version_20240618_1030.json这样客户端可以根据版本号判断要不要下载新清单避免每次都重新拉取一个几百 KB 的清单。比较本地清单和远程清单生成本次需要下载的差异文件列表。下载差异文件列表中的每个文件边下边校验 MD5下载完成后再统一写盘。写盘完成后把所有新文件的路径、大小、MD5 持久化到本地清单覆盖旧清单。这一步是事务性的不能更新到一半就写清单。正式进入游戏加载 AB 资源。这套流程我强调一个原则清单负责描述“该有什么”本地文件负责“实际有什么”两者一致才算更新完成。4.2 差异比较逻辑怎么实现最稳妥差异比较的核心算法就是把两份清单文件做一次归并。这里不要用“遍历本地文件看它在不在远程清单里”这种思路因为本地还残留上次没删干净的旧文件会干扰判断。我用的比较逻辑是以远程清单为基准逐项读取其files列表。对每一项检查本地清单里是否存在同名记录。如果不存在或者存在但 MD5 不一致则加入下载列表。同时检查该文件的所有依赖如果依赖里有变化也加入下载列表。本地清单本身就是上次更新后保存的“真相”旧文件再怎么混在磁盘里都不会造成干扰因为我不按磁盘文件来判断只按清单比。判断完成后把下载列表按文件大小降序排列先下大的后下小的。理由很朴素这样即使下载被用户中断最大的文件已经大概率下进缓存重试时能复用的多。这个策略在弱网环境下体验提升很明显。4.3 断点续传与并发下载的取舍几万个文件即使只有几十个需要下载如果串行一个个拉几次都会把人逼疯。业界通行做法是并发下载但并发数不是越多越好。我在 Unity 客户端里维护一个下载线程池默认并发 4 到 6 路每路独立维持一个UnityWebRequest。为什么要做并发限制因为移动端设备性能差异巨大老机型并发太高下载线程和主线程抢 CPU进游戏加载时卡顿明显。但并发太低几百个文件的更新可能要用户等很久。断点续传方面UnityWebRequest 不太方便直接做 Range 续传我通常是自己维护一个FileDownloader用它自带的downloadHandler接受字节流边写入本地临时文件。每次下载前查一下临时文件大小如果服务端支持 Range 请求就发一个 Range 头接着下否则就从头下。临时文件命名要加后缀.tmp防止被 AB 加载逻辑当成正常资源读取。下载完了不要立刻覆盖正式文件。先写临时目录全部下载并校验通过后做一个原子性的 replace 操作。这一步是为了防止更新一半用户杀进程导致本地既有新文件又有旧文件清单状态不一致下次启动直接崩溃。4.4 AB 加载与缓存更新完成只是开始资源下载到位不代表万事大吉。AB 加载和缓存管理如果没配合好增量更新会带来一个新的问题旧 AB 被内存缓存了新 AB 下载下来也加载不了。Unity 里加载 AB 的主流方式是AssetBundle.LoadFromFile。这套接口默认会做缓存同一个路径重复加载返回的是同一个 AssetBundle 实例。如果你的热更新流程是先加载了旧 AB然后把新文件写到同一个路径再调 LoadFromFile你会发现加载出来还是旧的——因为 AB 缓存命中了旧句柄。常规解决办法加载前先调AssetBundle.Unload(true)把旧的卸载掉再加载新的。但这里有个讲究如果这个 AB 正在被其它资源引用强制卸载会导致引用它的资源全部丢失白屏就在眼前。更稳的做法是AB 文件名带版本号或 Hash。更新后的 AB 文件名从main_ui.ab变成main_ui_20240618_1030.ab。这样加载时天然是新文件旧缓存里的旧 AB 不会产生碰撞内存管理也更干净。代价是运行时所有资源路径都要经过一层映射从逻辑路径转到实际路径类似public static string ResolveBundlePath(string bundleName) { // 从版本映射表里查 bundleName 对应的实际文件路径 if (versionMap.TryGetValue(bundleName, out string actualPath)) return Path.Combine(assetRootPath, actualPath); return Path.Combine(assetRootPath, bundleName); }这套“逻辑资源名-物理文件名”双轨机制是很多成熟项目做资源更新时选择的方式代码多几十行但稳定性提升非常大。5. 构建与服务端配套让增量发布自动化起来客户端逻辑只是一条腿另一条腿是构建管线和服务端策略。这两块不配合增量更新只是纸上谈兵。5.1 构建阶段如何自动生成清单和依赖图我在做构建集成时写了一个独立的 C# 编辑器脚本挂到构建流水线的最后阶段。它的工作内容分四步第一步清空上一次构建的临时缓存目录防止旧文件残留。第二步调用BuildPipeline.BuildAssetBundles构建所有 AB得到输出目录下的.ab文件和对应的.manifest文件。第三步遍历输出目录读取每个 AB 的依赖信息。这里要用AssetBundleManifest.GetAllDependencies获取间接依赖而不只是GetDirectDependencies。为什么要全部因为运行时如果某个间接依赖缺失报错信息诡异到你想不到是依赖问题直接拉全依赖链可以提前发现。第四步逐个计算 MD5填充files字段写 JSON 到输出目录。同时把这份清单上传到服务器的版本目录里。整个流程可以在 Jenkins 或 GitLab CI 上挂定时任务每次代码提交后自动构建、自动发布发布后服务端版本目录多一个带时间戳的子目录。5.2 CDN 缓存与版本目录的更新策略文件级增量虽然下载量小但如果 CDN 缓存策略没配好CDN 回源会把服务器带宽打爆。比如你把同一个资源路径ab/main_ui.ab反复更新CDN 会缓存这个路径。客户端拿到新资源的请求后如果 CDN 还留着旧缓存即使服务器文件已经更新客户端下到的还是旧的。这是增量更新里一个特别隐蔽的问题。解决办法有几个方向资源文件名带版本号/Hash我在 4.4 节就说过这样新文件名和旧文件名 URL 不同CDN 天然不会串。 2 如果文件路径不能变那就在 CDN 上配置 Cache-Control 为no-cache或者设一个很短的 TTL让它频繁回源校验。或者每次发布新版本时在 CDN 控制台手动/自动刷新相关目录的缓存。我见过太多团队在这里翻车所以我强烈建议把文件指纹放进文件名而不是仅仅依赖响应头。这样就算 CDN 不回源新版本也能拿到正确的文件。5.3 更新出问题怎么回滚增量更新最大的阴影是新包发布后客户端大面积起不来急需退回旧版本。如果文件名带版本号回滚其实很方便。服务端把版本目录切回上一个版本客户端下次启动拉到旧版本号对比后发现本地需要回退一批文件。但这里有个问题本地已经下载了新版本的文件清单却是旧的直接进游戏可能会用错资源。我的回滚策略是客户端在更新任何文件之前先把旧文件备份到.backup目录。回滚时从备份目录恢复而不是重新下载。这个策略在文件量特别大、弱网环境下尤其有效。如果只是服务端操作失误导致说明文档更新错了那就直接在服务端重新发一个修正版客户端按增量逻辑继续更新即可。如果直接涉及资源缺失那就老老实实走整包灰度发布客户端强制弹窗提示更新。6. 真实项目里的坑与排查技巧实录做增量更新三年多踩过的坑基本都能归类。这一节我把那些典型问题、排查思路、避免方法都写出来省得你再遍历一遍。6.1 依赖更新导致的“新包变老”问题这是增量更新里最隐蔽的问题没有之一。现象是服务端更新了公共图集 AB客户端增量更新后主界面显示正常但商城界面加载出来的图片还是旧图集。排查半天发现商城模块 AB 没被标记为需要更新因为它的文件 MD5 跟远程清单一致。原因就是我第 2.2 节讲的依赖链没有进比对逻辑。客户端只比对“文件有没有变”没比对“依赖有没有变”。公共图集变了商城 AB 的依赖一变但商城 AB 自己没变于是增量逻辑漏掉了它。排查方法打开本地清单里商城 AB 的 dependencies 字段看看它依赖的是不是新版本公共图集。如果是旧的说明增量比较逻辑没有处理依赖。修复方法是把依赖链的变化作为更新条件之一判断顺序文件本身 md5 变化 → 更新文件依赖链上任意节点 md5 变化 → 更新。只要这个规则进去基本就不会再有“更新后资源没替换”的问题。6.2 文件内容相同但 Hash 不同导致的重复下载有一种情况是两个 AB 的内容其实一模一样只是打包时间、文件头信息、压缩参数不同导致 MD5 不一致。增量逻辑会把这俩都判定为需要下载。这种问题多出现在同一资源被多个不同 Bundle 配置重复打包。Unity 打包 AB 时同一个资源如果被多个 Bundle 引用要么复制进多个 Bundle要么通过依赖共享。如果之前的设计没做好公共资源拆分很容易出现“同一个角色模型在角色界面包和战斗界面包里各打包一份”的情况。解决思路是构建时做资源去重相同资源只允许进入一个 AB其余模块通过依赖引用它。这个属于构建方案优化不是增量逻辑能救回来的。另一种特殊情况是资源本身没变但 Unity 每次构建生成的 AB 二进制会带上不同的构建信息导致 MD5 变化。这种问题我见过解决办法是对 AB 做一次标准化处理把构建信息字段从 AB 文件里剥离或替换为固定内容但实现难度高一般团队没有必要做因为资源不变时你根本不会重新发布一个版本。6.3 路径、大小写和分隔符不一致造成的加载失败AB 加载路径不一致是这个领域的经典坑。Windows 构建生成的路径可能带反斜杠\手机上却是正斜杠/。如果你用AssetBundle.LoadFromFile(Path.Combine(root, ab\\main_ui.ab))这种拼法Windows 上可以跑打包到安卓上大概率找不到文件。更隐蔽的是大小写不一致。比如构建项目时资源名叫main_ui但清单里手动填了Main_UI安卓的某些文件系统是大小写敏感的文件明明在路径对不上加载就是失败。我在工程里立了一个规矩所有涉及 AB 的文件名、路径、清单字段统一用小写正斜杠相对路径。构建时通过脚本强制校验发现大小写或分隔符不一致直接报错。6.4 更新后内存残留旧资源导致的串图、花屏更新文件后AB 旧缓存没释放干净加载新 AB 时 Unity 返回了旧缓存对象导致串图、花屏。排查这个问题第一步看是不是文件名带版本号。如果没带十有八九是缓存冲突。第二步是看代码里有没有对旧 AB 调用Unload(false)。如果你既要释放旧的 AB 句柄又不想让已经实例化的资源对象失效就调Unload(false)它会释放 AB 的元数据但保留所有已加载的 Asset。等你不再使用这些 Asset 时再显式释放。这里的关键点要避免“一边用着旧资源一边把旧 AB 卸载”导致运行时引用异常。所以我在做资源更新时先切换加载映射表等旧资源的引用计数降为零再统一卸载旧 AB。这是一个异步过程不能图省事直接在更新时同步强杀。6.5 弱网环境下的下载失败与重试策略弱网环境是所有热更新方案的放大镜。增量更新虽然单个文件小但架不住文件数量多一个文件下载失败就可能导致整个流程卡住。我的重试策略分成三层单个文件失败允许重试三次重试间隔 2 秒、5 秒、10 秒指数退避。超过三次仍失败跳过该文件记录到失败列表。整个更新流程结束时如果失败列表中有文件提示用户“网络不佳是否重试”。用户切后台或杀进程后下次启动重新进入更新流程已经下载成功的临时文件还能复用所以重试成本很低。另外下载时的超时时间不要太短。CDN 首次回源时经常需要几秒钟如果你 3 秒就判定超时很多文件都会被误判为失败。我一般把连接超时设为 10 秒读取超时设为 30 秒。收尾这套方案用下来的体感从最开始做全量更新被玩家骂下载太慢到后来改成文件级增量把更新包从几百 MB 缩到几 MB最重要的不是那个 MD5 比对代码而是几个前置工程AB 分包设计、依赖链完整记录、清单生成工具链、缓存与路径规范。这些基础工作做到位增量更新才能真正“只下载真正变动的那几个文件”。跟我合作过的一些团队一开始总想找“一个函数解决增量更新”的现成方案后来发现热更新这件事90% 的功力在构建和服务端客户端只是最后一个动作而已。如果你准备在项目里落地这套机制建议从构建脚本开始改把分包和依赖图先理清楚再去写客户端的下载队列顺序反了会走很多弯路。