16 KiB
title, source, created, tags
| title | source | created | tags | |||||
|---|---|---|---|---|---|---|---|---|
| Unity 热更新的隐形命门:一篇讲透"确定性构建管线" - 知乎 | https://zhuanlan.zhihu.com/p/2028488903561163102 | 2026-05-09 15:35 |
|
Unity 热更新的隐形命门:一篇讲透"确定性构建管线" - 知乎
来源: https://zhuanlan.zhihu.com/p/2028488903561163102
写在前面
文章是由AI生成的,最开始是为了当个人笔记,让AI补全一下相关内容
核心观点是确定性构建管线,这个在6000.x的官方文档里已经有明确说明
Deterministic builds
如发现错误,以官方文档为准
起因:看到 Unity Partner Engineer 发的一篇帖子 Understanding Old Dependency Reuse in Unity AssetBundles,通篇在反复强调一个短语——deterministic pipeline。这个概念在中文社区里几乎没人讲透,但它恰恰是 AssetBundle 热更新体系的根基。本文尝试把它讲清楚。 一、一个看似简单的问题,藏着一个深坑
先抛一个场景给你,你试着回答一下:
线上已经下发了 Bundle A 给玩家。现在你新打了一个 Bundle B,B 依赖 A。你想让玩家继续用本地那个旧的 A,不要重新下载,可以吗?
凭直觉回答,多数人会说”当然可以,A 没改嘛”。
但真相是:在”确定性构建管线”下可以,在”非确定性构建管线”下不行。
Unity 官方的说法原话是:
It might work, but do not rely on it. (可能能用,但别指望它。)
这个”能不能依赖”的分界线,就是Deterministic Build Pipeline。
二、到底什么是”确定性构建”?
一句话定义:
给定完全相同的输入(资源 + 代码 + 配置 + 工具链 + 环境),无论构建多少次、在哪台机器、谁来构建,输出的二进制文件字节级完全一致。
写成公式:
same input + same pipeline = same output (byte-identical)
初学者常常会有这样一种错觉:
“我重新打了一次包,文件哈希没变,那我的管线肯定是确定性的。”
这个理解方向对,但不够精准。我们需要区分两个概念:
概念 性质 描述 “输出未变” 结果现象 两次构建 AB 的 MD5/SHA 相同 “确定性管线” 管线属性 只要输入不变,就必然输出不变的能力
换句话说——“输出未变”只是一次幸运的观测;“确定性”是一种架构保证。后者才是工程意义上的可依赖性。
三、为什么它是热更新的命门?
让我们把场景画出来:
上线日: Bundle A (已推送给 1000 万玩家) 次月更新: Bundle B (依赖 A),本次不想动 A
你希望玩家的流量账单是这样:
下载 B + 复用本地 A = 省下一整个 A 的流量
要做到这点,你必须回答一个问题:B 里记录的那个”指向 A 的引用”,今天还指得到吗?
这就要讲 AB 的内部机制了。
AB 是怎么记录跨包引用的
每个 AssetBundle 内部是一个 SerializedFile,开头有一张 External References 表:
External References path(1): "Library/unity default resources" path(2): "Resources/unity_builtin_extra" path(3): "archive:/CAB-35fce856128a6714740898681ea54bbe/CAB-35fce856128a6714740898681ea54bbe"
然后对象内部的引用长这样:
m_Material PPtr m_FileID : 3 ← 指向上面 path(3) m_PathID : 6 ← 目标文件里的第 6 号对象
所以 B 里的一个引用本质是一个二元组:
(file, object) = (CAB 路径, PathID)
关键点:CAB 路径里那个 hash,是由”构建出的 SerializedFile“决定的,不是抽象的”Bundle A”。
现在你就能看懂这个”坑”了
如果管线是确定性的:
A 没改 → 重新打也一定得到字节完全一致的 A → CAB 路径不变 → PathID 不变 B 里的 (CAB-xxx, PathID=6) 仍然能 100% 命中旧 A 玩家本地的旧 A 可以安全复用,不用重新下载
如果管线是非确定性的:
A 没改,但重新打出来的 A’ 可能CAB 哈希变了、或者对象顺序变了、或者PathID 漂移了 B 里记的引用指的是”新 A’“的结构 你让玩家复用旧 A → 可能看起来正常,也可能随机 crash
这就是为什么 Unity 官方只敢说 “might work, but do not rely on it”。
构建管线的确定性,决定了你整个热更体系的可靠上限。
四、哪些行为会破坏确定性?踩雷清单
下面我把最容易踩的坑按四个层级整理出来。每个坑都用 “现象 → 原因 → 怎么修” 的格式来讲,方便直接对照排查。
A. 构建管线层 —— 你选错了工具或用错了姿势
- 🔥 搞混了几种 hash,把”输入 hash”当成了”内容指纹”
现象:明明包内容变了,你的版本对比系统却说没变;或者反过来,内容没变,hash 却变了。
原因:Unity 内置 BuildPipeline.BuildAssetBundles 生成的 hash 是基于输入估算的,不是基于最终产物的内容。里面其实混着好几种 hash:
hash 类型 它代表什么 IncrementalBuildHash 用来判断”要不要重新打这个包”的输入指纹 TypeTreeHash 用来判断”类型结构有没有变” 缓存 version hash 给 Caching 系统用的版本标识符 最终二进制内容 包文件实际的字节
这四个东西完全不是一回事。如果你把前面某个 hash 直接当成 CDN 版本号,或者当成”两个包一不一样”的判断依据,就会出错。
Unity 6.6 手册也明确提醒了:
The AssetBundle input hash isn’t an ideal value for tracking file versions.
怎么修:
如果你的发布系统需要精确判断”包内容到底变没变”,优先考虑 SBP 或 Addressables — 它们的 hash 是基于构建输出算的,更准确 如果继续用内置 API,就别拿它的 hash 直接当内容校验,自己对产物算一遍 SHA256 更靠谱 2. 以为开了 DeterministicAssetBundle 就万事大吉
现象:明明开了这个 flag,不同机器打出来的包还是不一样。
原因:BuildAssetBundleOptions.DeterministicAssetBundle 是老时代的知识。它只解决 PathID 分配的确定性问题。但在现代 Unity(6.x)里,破坏确定性的主要原因早已不是这个 flag,而是:
收集资源时没排序 构建脚本里塞了时间戳、随机数 不同机器的导入缓存不一致 构建环境(OS、Unity 版本)不一样
怎么修:不要只盯这一个开关。真正要盯的是整个构建流程的输入稳定性。
- 打包前忘了切 Build Target
现象:CI 上打 Android 包,但编辑器的 Active Target 还停留在 iOS。打出来的包和手动打的不一致。
原因:如果你调用 BuildPipeline.BuildAssetBundles 时指定的平台和当前 Active Target 不一样,Unity 会悄悄触发资源重导入和脚本重编译。更坑的是,正在跑的编辑器脚本(比如你的打包工具、构建回调)是按旧 Target 编译的,可能出现平台条件判断错乱。
怎么修:CI 脚本里,打包之前先显式切换 Active Target:
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); B. 资源数据层 —— 你的资源本身就不稳定 4. AB 名字里带了日期或构建编号
现象:每天打包,即使资源没变,包名都在变,hash 自然跟着变。
坏例子 vs 好例子:
❌ ui_mainmenu_20260417.bundle ← 每天都变 ❌ ui_mainmenu_build_2841.bundle ← CI 编号会变 ✅ ui_mainmenu ← 稳定标识 ✅ ui_mainmenu_v1.2.3 ← 语义化版本
Unity 6.3⁄6.6 文档的原话:
Avoid timestamps or build-date suffixes in AssetBundle names and use static identifiers or semantic versions. 5. ⭐ 收集资源时没排序(最常见的坑!)
现象:同一台机器连续打两次,包的 hash 居然不一样。更常见的场景是 A 电脑打出来的包和 B 电脑的不一样。
原因:AssetDatabase.FindAssets() 和 Directory.GetFiles() 返回的顺序不保证一致。不同机器、不同文件系统、甚至同一台机器在不同时间调用,顺序都可能飘。一旦资源添加顺序飘了,序列化数据的排列就飘了,hash 自然就变了。
坏写法:
// ❌ 直接遍历,顺序不可控 var guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/UI" }); foreach (var guid in guids) { group.AddAsset(guid); }
好写法:
// ✅ 加一行排序,问题消失 var guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/UI" }) .OrderBy(x => x, StringComparer.Ordinal) .ToArray();
记住这条规则:凡是涉及 FindAssets、GetFiles、EnumerateFiles 的地方,必须手动排序。
- 往 ScriptableObject 里塞数据时,塞入顺序不稳定
现象:和上面一样,不同机器或不同时间构建出的包不一致。
原因:如果你在构建前用 IPreprocessBuildWithReport 之类的回调,动态往 SO 的 List