前言
热更新是手游平台对于游戏的一种特殊更新技术,本篇将讲解AB包的资源热更和Lua的代码热更
热更新
AssetBundle详解 - 知乎 (zhihu.com)
[Unity AssetBundle的打包 发布 下载与加载_assetbundle下载-CSDN博客](https://blog.csdn.net/newchenxf/article/details/124738469#:~:text=2.1 AB包的文件结构 1 将想要动态加载的资源添加到AssetBundle (可以有多个bundle) 2 要发布时,做打包工作 3,自己把bundle上传到服务器,服务器管理版本 4 客户端下载bundle,版本不匹配(md5校验啥的),则下载新bundle 5 加载指定路径的bundle,提取文件 6 卸载bundle, 节约内存)
AssetBundle的概念
AssetBundle又称AB包,是Unity提供的一种用于存储资源的资源压缩包。
Unity中的AssetBundle系统是对资源管理的一种扩展,通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,可以动态地加载和卸载AB包,继而有选择地加载内容。
AssetBundle和Resources的比较
AssetBundle |
Resources |
资源可分布在多个包中 |
所有资源打包成一个大包 |
存储位置自定义灵活 |
必须存放在Resources目录下 |
压缩方式灵活(LZMA,LZ4) |
资源全部会压缩成二进制 |
支持后期进行动态更新 |
打包后资源只读无法动态更改 |
AssetBundle的特性
AB包可以存储绝大部分Unity资源但无法直接存储C#脚本,所以代码的热更新需要使用Lua或者存储编译后的DLL文件。
AB包不能重复进行加载,当AB包已经加载进内存后必须卸载后才能重新加载。
多个资源分布在不同的AB包可能会出现一个预制体的贴图等部分资源不在同一个包下,直接加载会出现部分资源丢失的情况,即AB包之间是存在依赖关系的,在加载当前AB包时需要一并加载其所依赖的包。
打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)···
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ManifestFileVersion: 0 CRC: 3548016675 AssetBundleManifest: AssetBundleInfos: Info_0: Name: sceneres_unity Dependencies: {} Info_1: Name: a_png Dependencies: {} Info_2: Name: textureexample1_png Dependencies: {}
|
热更新步骤
热更的基本流程可以分成2部分:
第一步:导出热更新所需资源
第二步:游戏运行后的热更新流程
第一步、导出热更新所需资源
- 打包热更资源的对应的md5信息(涉及到增量打包)
- 上传热更对应的ab包到热更服务器
- 上传版本信息到版本服务器
第二步、游戏运行后的热更新流程
启动游戏
- 根据当前版本号,和平台号去版本服务器上检查是否有热更
- 从热更服务器上下载md5文件,比对需要热更的具体文件列表
- 从热更服务器上下载需要热更的资源,解压到热更资源目录
- 游戏运行加载资源,优先到热更目录中加载,再到母包资源目录加载
更新注意:
要有下载失败重试几次机制;
要进行超时检测;
要记录更新日志,例如哪几个资源时整个更新流程失败。
md5算法工作原理
MD5算法的核心思想是将任意长度的输入数据通过一系列复杂的变换,最终生成一个128位的哈希值。这个过程可以分为以下四个主要步骤:
- 填充:MD5算法首先对输入数据进行填充,使其长度达到一个特定的长度,这是为了使原始数据的长度可以被512整除。填充的方法是在原始数据后面添加一个“1”,然后添加足够数量的“0”,最后添加一个64位的整数表示原始数据的长度。
- 初始化缓冲区:MD5算法使用了一个64位的缓冲区,分为四个16位部分,用来存储中间结果和最终结果。这四个部分被初始化为特定的常数。
- 处理分组:填充后的数据被划分为长度为512位的分组,每个分组又划分为16个32位的子分组。然后,通过一系列的位操作和模加运算,每个分组都被处理并更新缓冲区的内容。这个过程涉及四个主要的轮函数和一系列的非线性函数。
- 输出:处理完所有分组后,缓冲区中的内容就是最终的哈希值。这个哈希值是一个128位的数,通常表示为32个十六进制数。
深入解析MD5哈希算法:原理、应用与安全性-腾讯云开发者社区-腾讯云 (tencent.com)
打包AB包
打包方案
名称 |
详细 |
优点 |
缺点 |
适用性 |
整包 |
将完整更新资源放在Application.StreamAssets目录下,首次进入游戏将资源释放到Application.persistentDataPath下。 |
首次更新小。 |
安装包下载时间长,首次安装久。 |
国内游戏大部分是使用整包策略 |
分包 |
少部分资源放在包里,其他资源存放在服务器上,进入游戏后将资源下载到Application.persistentDataPath目录下。 |
安装包小,安装时间短,下载快。 |
首次更新下载解压包时间久。 |
海外游戏大部分是使用分包策略 |
AssetBundle的压缩格式
格式 |
包体体积 |
速度 |
不压缩 |
没有经过压缩的包体积最大 |
访问速度最快。 |
LZ4格式 |
压缩后的AssetBundle包体的体积较大(该算法基于chunk)。 |
使用LZ4格式的好处在于解压缩的时间相对要短。 |
LZMA格式 |
使用LZMA格式压缩的AssetBundle的包体积最小(高压缩比) |
相应的会增加解压缩时的时间。 |
AssetBundle依赖加载
如果一个或者多个 UnityEngine.Objects 引用了其他 AssetBundle 中的 UnityEngine.Object,那么 AssetBundle 之间就产生的依赖关系。相反,如果 UnityEngine.ObjectA 所引用的UnityEngine.ObjectB 不是其他 AssetBundle 中的,那么依赖就不会产生。
假若这样(指的是前面两个例子的后者,既不产生依赖的情况),被依赖对象(UnityEngine.ObjectB)将被拷贝进你创建的 AssetBundle(指包含 UnityEngine.ObjectA 的 AssetBundle)。
更进一步,如果有多个对象(UnityEngine.ObjectA1、UnityEngine.ObjectA2、UnityEngine.ObjectA3……)引用了同一个被依赖对象(UnityEngine.ObjectB),那么被依赖对象将被拷贝多份,打包进各个对象各自的 AssetBundle。
如果一个 AssetBundle 存在依赖性,那么要注意的是,那些包含了被依赖对象的 AssetBundles,需要在你想要实例化的对象的加载之前加载。Unity 不会自动帮你加载这些依赖。
想想看下面的例子, Bundle1 中的一个材质(Material)引用了 Bundle2 中的一个纹理(Texture):
在这个例子中,在从 Bundle1 中加载材质前,你需要先将 Bundle2 加载到内存中。你按照什么顺序加载 Bundle1 和 Bundle2 并不重要,重要的是,想从 Bundle1 中加载材质前,你需要先加载 Bundle2。
AssetBundle管理器
利用AssetBundleBrowser可以轻松实现AB包的打包工作,更重要的是如何将AB包中的资源加载出来并使用它们,Unity已经提供了一组API能够实现AB包的加载和资源的加载。
主要API
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
AssetBundle ab = AssetBundle.LoadFromFile(path);
T obj = ab.LoadAsset<T>(ResourceName); Object obj = ab.LoadAsset(ResourceName); Object obj = ab.LoadAsset(ResourceName,Type); T obj = ab.LoadAssetAsync<T>(resName); Object obj = ab.LoadAssetAsync(resName); Object obj = ab.LoadAssetAsync(resName,Type);
ab.UnLoad(false); AssetBundle.UnloadAllAssetBundles(false);
|
ABManager.cs
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
| using System; using System.Net.Mime; using System.Collections; using System.Collections.Generic; using UnityEngine;
namespace Common { public class ABManager : MonoSingleton<ABManager> { private Dictionary<string, AssetBundle> abCache;
private AssetBundle mainAB = null;
private AssetBundleManifest mainManifest = null;
private string basePath { get { #if UNITY_EDITOR || UNITY_STANDALONE return Application.dataPath + "/StreamingAssets/"; #elif UNITY_IPHONE return Application.dataPath + "/Raw/"; #elif UNITY_ANDROID return Application.dataPath + "!/assets/"; #endif } } private string mainABName { get { #if UNITY_EDITOR || UNITY_STANDALONE return "StandaloneWindows"; #elif UNITY_IPHONE return "IOS"; #elif UNITY_ANDROID return "Android"; #endif } }
protected override void Init() { base.Init(); abCache = new Dictionary<string, AssetBundle>(); }
private AssetBundle LoadABPackage(string abName) { AssetBundle ab; if (mainAB == null) { mainAB = AssetBundle.LoadFromFile(basePath + mainABName); mainManifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); } string[] dependencies = mainManifest.GetAllDependencies(abName); for (int i = 0; i < dependencies.Length; i++) { if (!abCache.ContainsKey(dependencies[i])) { ab = AssetBundle.LoadFromFile(basePath + dependencies[i]); abCache.Add(dependencies[i], ab); } } if (abCache.ContainsKey(abName)) return abCache[abName]; else { ab = AssetBundle.LoadFromFile(basePath + abName); abCache.Add(abName, ab); return ab; }
} #region 同步加载的三个重载 public T LoadResource<T>(string abName,string resName)where T:Object { AssetBundle ab = LoadABPackage(abName);
return ab.LoadAsset<T>(resName); }
public Object LoadResource(string abName,string resName) { AssetBundle ab = LoadABPackage(abName);
return ab.LoadAsset(resName); } public Object LoadResource(string abName, string resName,System.Type type) { AssetBundle ab = LoadABPackage(abName);
return ab.LoadAsset(resName,type); }
#endregion
public void LoadResourceAsync(string abName,string resName, System.Action<Object> finishLoadObjectHandler) { AssetBundle ab = LoadABPackage(abName); StartCoroutine(LoadRes(ab,resName,finishLoadObjectHandler)); }
private IEnumerator LoadRes(AssetBundle ab,string resName, System.Action<Object> finishLoadObjectHandler) { if (ab == null) yield break; AssetBundleRequest abr = ab.LoadAssetAsync(resName); yield return abr; finishLoadObjectHandler(abr.asset); } public void LoadResourceAsync(string abName, string resName,System.Type type, System.Action<Object> finishLoadObjectHandler) { AssetBundle ab = LoadABPackage(abName); StartCoroutine(LoadRes(ab, resName,type, finishLoadObjectHandler)); } private IEnumerator LoadRes(AssetBundle ab, string resName,System.Type type, System.Action<Object> finishLoadObjectHandler) { if (ab == null) yield break; AssetBundleRequest abr = ab.LoadAssetAsync(resName,type); yield return abr; finishLoadObjectHandler(abr.asset); }
public void LoadResourceAsync<T>(string abName, string resName, System.Action<Object> finishLoadObjectHandler)where T:Object { AssetBundle ab = LoadABPackage(abName); StartCoroutine(LoadRes<T>(ab, resName, finishLoadObjectHandler)); }
private IEnumerator LoadRes<T>(AssetBundle ab, string resName, System.Action<Object> finishLoadObjectHandler)where T:Object { if (ab == null) yield break; AssetBundleRequest abr = ab.LoadAssetAsync<T>(resName); yield return abr; finishLoadObjectHandler(abr.asset as T); }
public void UnLoad(string abName) { if(abCache.ContainsKey(abName)) { abCache[abName].Unload(false); abCache.Remove(abName); } }
public void UnLoadAll() { AssetBundle.UnloadAllAssetBundles(false); abCache.Clear(); mainAB = null; mainManifest = null; } } }
|
其他
手游为什么需要热更新
基于以上两点,热更新就很重要了,快速,小巧,绕过苹果审核。
版本号的管理
客户端版本号我们是 4 位来标识,假设是 X.Y.Z.W,下面是 XYZW值对应的意义:
版本号 |
对应介绍 |
X【巨大版本号】 |
这一位其实就是 1,没事一般不会动它,除非有太巨大的变化,目前反正还是 1; |
Y【整包更新版本号】 |
我们游戏一般一个月会有一个比较大的版本迭代,这种版本会走商店,每次提交Y值+1; |
Z【服务器协议版本号】 |
一个月度版本周期内,万一 SDK 有问题或者 C#层有发现 bug,需要更新商店,这一位会+1,这里单独留一个 Z 处理这种商店版本号,是因为不想影响 Y 值,而商店提交新包要求版本号必须有增加,buildNum 也是商店要求必须要升的; |
W【编译版本号\热更版本号】 |
每次热更都+1 |
【第 2 位加 1 之后,3、4 位全部清 0】
比如目前商店版本号是 1.1.0.0,这个版本我们热更了 3 次后,版本 号就变成 1.1.0.3,这时候发现好像 C#层有一点 bug 必须要修复,那打 一个 1.1.1.3 提交商店,1.1.1.3 包里的资源和 1.1.0.3 的资源是一模一样的,这之后如果有第 4 次热更,那热更包的版本号就是 1.1.1.4。
链接
Lua 面试知识点 - 简书 (jianshu.com)
【Unity面试篇】Unity 面试题总结甄选 |热更新与Lua语言 | ❤️持续更新❤️_unity lua面试-CSDN博客
Unity游戏开发客户端面经——lua(初级)_unity lua-CSDN博客
Unity游戏开发客户端面经——热更新(初级)_unity 热更新-CSDN博客
【Lua面试】2021年Lua面试题分享(4.25更新)-CSDN博客