热更新
热更新是什么
| 类型 | 更新内容 | 典型技术 |
|---|
| 资源热更 | 贴图、模型、配置、UI 预制体等 | AssetBundle、Addressables |
| 代码热更 | C# 业务逻辑(不通过商店整包审核) | HybridCLR、ILRuntime、xlua 等 |
目的:修 Bug、发活动资源、降首包体积、分平台差异化内容。
限制:无法热更 Unity 引擎本身、原生插件(需走商店更新或单独下载 so/dll)。
常见方案对比
| 方案 | 资源 | 代码 | 特点 |
|---|
| AssetBundle | ✅ | ❌(需配合热更框架) | 底层、可控,自建版本与 CDN |
| Addressables | ✅ | ❌ | 基于 AB,官方封装分组/远程/引用计数 |
| HybridCLR | 可配合 AB | ✅ | 原生解释执行 IL,性能较好,国内常用 |
| ILRuntime | 可配合 AB | ✅ | 解释执行 DLL,成熟 |
| XLua / tolua | 可配合 AB | ✅(Lua) | 脚本语言热更,与 C# 互调 |
flowchart LR
A[首包安装] --> B{需要更新?}
B -->|资源| C[下载 AB / Addressables]
B -->|代码| D[下载 DLL / Lua]
C --> E[加载进内存]
D --> E
E --> F[运行新逻辑/新资源]
AssetBundleManifest
当项目有多个 AB 包(如 texture.ab 依赖 material.ab)时,打主包会生成 AssetBundleManifest(常随主包名,如 platform 或自定义主包名),用于:
| 作用 | 说明 |
|---|
| 包列表 | 所有 AB 名称、Hash(版本对比、增量更新) |
| 依赖关系 | 加载某 AB 前自动加载其依赖包 |
| 灵活路径 | AB 可放 StreamingAssets、持久化目录、CDN |
| 压缩 | 减小包体,按需解压 |
有依赖时必须先加载 Manifest 所在的主包,再通过 AssetBundleManifest API 查依赖。
AssetBundle 打包选项
(Unity AssetBundle Browser 或自定义构建脚本中的常见项)
| 选项 | 说明 |
|---|
| Build Target | 目标平台;各平台需分别打包 |
| Output Path | 输出目录 |
| Clear Folders | 打包前是否清空输出文件夹 |
| Copy To StreamingAssets | 是否复制到 StreamingAssets(首包内置资源) |
| Compression | 见下表 |
| ETI(Exclude Type Information) | 包内不含类型信息(更小,加载略受限) |
| FR(Force Rebuild) | 强制全量重建;与 Clear 不同:不删已不存在的旧包文件 |
| ITTC(Ignore Type Tree Changes) | 增量构建时忽略 Type Tree 变化 |
| Append Hash | 哈希附加到 AB 文件名(利于缓存与热更对比) |
| Strict Mode | 出错则构建失败 |
| Dry Run Build | 只模拟构建,不真正写文件 |
压缩方式
| 方式 | 特点 |
|---|
| No Compression | 不压缩,包大,加载快 |
| LZMA | 体积最小;整包解压才好用单个资源,首包解压慢 |
| LZ4 | 可按块解压,边用边解压,内存友好;热更常用 |
打包产物说明
| 文件 | 说明 |
|---|
| 主包 | 名称常与输出目录/平台主包一致;内含 AssetBundleManifest,记录依赖与 Hash |
| 各 AB 文件 | 实际资源数据(.ab 或自定义后缀) |
.manifest 文本 | 单个 AB 的元信息(依赖、Hash、类型等),调试用;运行时主要靠二进制 Manifest |
示例结构:
1
2
3
4
5
6
| Output/
├── StandaloneWindows64 ← 主包(含 Manifest)
├── ui_panel.ab
├── ui_panel.ab.manifest
├── textures.ab
└── textures.ab.manifest
|
AB 加载流程与代码
流程
sequenceDiagram
participant App
participant MainAB as 主包/Manifest
participant Dep as 依赖AB
participant Res as 目标AB
App->>MainAB: LoadFromFile(主包路径)
App->>MainAB: GetManifest()
App->>MainAB: GetAllDependencies(目标包名)
loop 每个依赖
App->>Dep: LoadFromFile
end
App->>Res: LoadFromFile / LoadAsset
App->>App: Instantiate / 使用资源
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| using System.IO;
using UnityEngine;
public class ABLoader : MonoBehaviour
{
// 主包路径(热更后一般在 persistentDataPath)
string abRoot = Path.Combine(Application.persistentDataPath, "AssetBundles");
string mainAbName = "StandaloneWindows64"; // 与打包主包名一致
AssetBundle mainBundle;
AssetBundleManifest manifest;
void LoadManifest()
{
string mainPath = Path.Combine(abRoot, mainAbName);
mainBundle = AssetBundle.LoadFromFile(mainPath);
manifest = mainBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}
public GameObject LoadPrefab(string bundleName, string assetName)
{
if (manifest == null) LoadManifest();
// 1. 先加载所有依赖
string[] deps = manifest.GetAllDependencies(bundleName);
foreach (var dep in deps)
{
string depPath = Path.Combine(abRoot, dep);
if (!IsLoaded(dep))
AssetBundle.LoadFromFile(depPath);
}
// 2. 加载目标包
string bundlePath = Path.Combine(abRoot, bundleName);
AssetBundle bundle = AssetBundle.LoadFromFile(bundlePath);
// 3. 加载资源
GameObject prefab = bundle.LoadAsset<GameObject>(assetName);
return Instantiate(prefab);
}
bool IsLoaded(string bundleName) { /* 维护已加载字典 */ return false; }
void OnDestroy()
{
mainBundle?.Unload(true);
// 其他 bundle 按引用计数 Unload(false/true)
}
}
|
异步加载(推荐)
1
2
3
4
5
6
7
| var req = AssetBundle.LoadFromFileAsync(path);
yield return req;
AssetBundle bundle = req.assetBundle;
var assetReq = bundle.LoadAssetAsync<GameObject>("MyPrefab");
yield return assetReq;
GameObject go = Instantiate(assetReq.asset as GameObject);
|
Unload 注意
| 调用 | 效果 |
|---|
Unload(false) | 卸载 AB 文件,已实例化对象仍可用(材质可能丢引用) |
Unload(true) | 连已加载出的资源一并卸载 |
有依赖时按加载逆序卸载,或使用统一 AB 管理器做引用计数。
版本与更新流程
典型热更流程
- 首包:内置少量 AB 或 StreamingAssets + 版本号
version.json - 启动:请求 CDN 上
version.json / 资源清单(文件名 + Hash + 大小) - 对比:本地 Hash vs 远程 Hash,得到需下载列表
- 下载:差量或全量 AB 到
persistentDataPath - 加载:从持久化路径
LoadFromFile,不走 StreamingAssets - 校验:MD5/CRC 防篡改(可选)
version.json 示例
1
2
3
4
5
6
7
| {
"appVersion": "1.0.0",
"resVersion": "2026051901",
"bundles": [
{ "name": "ui_panel.ab", "hash": "a1b2c3...", "size": 1024000 }
]
}
|
| 目录 | 用途 |
|---|
StreamingAssets | 首包只读,安装后不可写 |
persistentDataPath | 热更下载目录,可读写 |
Addressables 简述
在 AssetBundle 之上的资源管理系统:
| 概念 | 说明 |
|---|
| Group | 资源分组,决定打哪些 AB |
| Address | 用地址字符串加载,不必记 AB 名 |
| Remote / Local | 内置与远程 CDN 路径 |
| Catalog | 资源目录,类似 Manifest + 版本 |
1
2
3
4
5
6
7
8
9
10
11
| using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
async void Load()
{
AsyncOperationHandle<GameObject> handle =
Addressables.LoadAssetAsync<GameObject>("MyPrefab");
await handle.Task;
Instantiate(handle.Result);
}
|
适合新项目;老项目大量 AB 可逐步迁移。
常见坑与注意点
| 问题 | 原因 / 处理 |
|---|
| 材质变粉 | AB 里 Shader 丢失;Shader 放 Always Included 或进 AB |
| 重复加载依赖 | 未先 GetAllDependencies 或未缓存已加载 AB |
| 内存涨 | 未 Unload;重复 LoadFromFile 同一包 |
| 平台加载失败 | 打错 Build Target;路径大小写 |
| LZMA 卡顿 | 首包解压慢;热更资源改 LZ4 |
要点速查
| 主题 | 要点 |
|---|
| Manifest | 主包、依赖、Hash |
| 压缩 | LZMA 小但整包解压;LZ4 热更常用 |
| 加载顺序 | 主包 → Manifest → 依赖 AB → 目标 AB → LoadAsset |
| 路径 | 热更后用 persistentDataPath |
| 卸载 | Unload(false/true) 区别 |
| 代码热更 | HybridCLR / ILRuntime / Lua;与 AB 配合 |