Files
obsidian-notes/InBox/zhihu_deterministic_build.md
Build Bot f7310caea0 同步
2026-05-18 01:20:38 +08:00

16 KiB
Raw Blame History

title, source, created, tags
title source created tags
Unity 热更新的隐形命门:一篇讲透"确定性构建管线" - 知乎 https://zhuanlan.zhihu.com/p/2028488903561163102 2026-05-09 15:35
zhihu
article
unity
assetbundle
deterministic-build

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 BB 依赖 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. 构建管线层 —— 你选错了工具或用错了姿势

  1. 🔥 搞混了几种 hash把”输入 hash”当成了”内容指纹”

现象明明包内容变了你的版本对比系统却说没变或者反过来内容没变hash 却变了。

原因Unity 内置 BuildPipeline.BuildAssetBundles 生成的 hash 是基于输入估算的,不是基于最终产物的内容。里面其实混着好几种 hash

hash 类型 它代表什么 IncrementalBuildHash 用来判断”要不要重新打这个包”的输入指纹 TypeTreeHash 用来判断”类型结构有没有变” 缓存 version hash 给 Caching 系统用的版本标识符 最终二进制内容 包文件实际的字节

这四个东西完全不是一回事。如果你把前面某个 hash 直接当成 CDN 版本号,或者当成”两个包一不一样”的判断依据,就会出错。

Unity 6.6 手册也明确提醒了:

The AssetBundle input hash isnt an ideal value for tracking file versions.

怎么修:

如果你的发布系统需要精确判断”包内容到底变没变”,优先考虑 SBP 或 Addressables — 它们的 hash 是基于构建输出算的,更准确 如果继续用内置 API就别拿它的 hash 直接当内容校验,自己对产物算一遍 SHA256 更靠谱 2. 以为开了 DeterministicAssetBundle 就万事大吉

现象:明明开了这个 flag不同机器打出来的包还是不一样。

原因BuildAssetBundleOptions.DeterministicAssetBundle 是老时代的知识。它只解决 PathID 分配的确定性问题。但在现代 Unity6.x破坏确定性的主要原因早已不是这个 flag而是

收集资源时没排序 构建脚本里塞了时间戳、随机数 不同机器的导入缓存不一致 构建环境OS、Unity 版本)不一样

怎么修:不要只盯这一个开关。真正要盯的是整个构建流程的输入稳定性。

  1. 打包前忘了切 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.36.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 的地方,必须手动排序。

  1. 往 ScriptableObject 里塞数据时,塞入顺序不稳定

现象:和上面一样,不同机器或不同时间构建出的包不一致。

原因:如果你在构建前用 IPreprocessBuildWithReport 之类的回调,动态往 SO 的 List