热更流程都不是固定的,每个人写的状态机也有所差别,但是一些必要步骤肯定不可能少,例如下载清单,对比版本,下载AB包,标记下载完成。我接下来的每一篇文章都是下载AB包的重要步骤,大概率是不能省略的。
检查沙盒路径是否存在
public static string MakePersistentLoadPath(string path)
{
#if UNITY_EDITOR
// 注意:为了方便调试查看,编辑器下把存储目录放到项目里
string projectPath = Path.GetDirectoryName(Application.dataPath).Replace("\\","/");
projectPath = GetRegularPath(projectPath);
return StringFormat.Format("{0}/Sandbox/{1}", projectPath, path);
#else
return StringFormat.Format("{0}/Sandbox/{1}", Application.persistentDataPath, path);
#endif
}
检查下载临时目录是否存在
请注意,热更时下载AB包先下载到临时目录,之后再拷贝到沙盒目录
public static string MakeDownloadTempPath(string path)
{
#if UNITY_EDITOR
string projectPath = Path.GetDirectoryName(Application.dataPath).Replace("\\", "/");
projectPath = GetRegularPath(projectPath);
return StringFormat.Format("{0}/Sandbox_Temp/{1}", projectPath, path);
#else
return StringFormat.Format("{0}/Sandbox_Temp/{1}", Application.persistentDataPath, path);
#endif
}
路径都确认存在后,开始下载
前文有介绍过,生成的AB包清单长这个样子,把这个文件生成二进制bytes文件扔到服务器上去下载。
第一行是SVN版本号
第二行是AB包数量
从第三行开始是资源包信息,以=号分割开有效数据,分别是
MD5.unity3d = 资源路径 = 资源路径的HashId = 包体KB大小 = SVN版本号 = 启动热更模式
每一行数据封装了一个PatchElement类,代码在下文中
我们项目封装了一下UnityWebRequest,叫WebDataRequest,你不想封装直接用UnityWebRequest即可,WebDataRequest代码在后面有。
private IEnumerator DownLoad()
{
// 解析APP里的补丁清单
string filePath = AssetPathHelper.MakeStreamingLoadPath(PatchDefine.InitManifestFileName);
string url = AssetPathHelper.ConvertToWWWPath(filePath);
using (WebDataRequest downloader = new WebDataRequest(url))
{
yield return downloader.DownLoad();
if (downloader.States == EWebRequestStates.Success)
{
PatchHelper.Log(ELogLevel.Log, "Parse app patch manifest.");
ParseAppPatchManifest(downloader.GetData());
}
else
{
throw new System.Exception($"Fatal error : Failed download file : {url}");
}
}
}
// 解析补丁清单文件相关接口
public void ParseAppPatchManifest(byte[] data)
{
if (AppPatchManifest != null)
throw new Exception("Should never get here.");
AppPatchManifest = new PatchManifest(true);
AppPatchManifest.Parse(data);
}
PatchManifest类是一个专门解析AB包清单的类,看代码也能知道,Parse方法的最终目的,就是把清单的每一行数据解析成PatchElement然后加入到字典中存起来等着下载时调用
/// <summary>
/// 补丁清单文件
/// </summary>
public class PatchManifest
{
private bool _isParse = false;
/// <summary>
/// 资源版本号
/// </summary>
public int DllVersion { private set; get; }
public int ResVersion { private set; get; }
private bool IsInit = false;
/// <summary>
/// 所有打包文件列表
/// </summary>
public readonly Dictionary<string, PatchElement> Elements = new Dictionary<string, PatchElement>();
public PatchManifest(bool isInit = false)
{
IsInit = isInit;
}
/// <summary>
/// 解析数据
/// </summary>
public void Parse(byte[] data)
{
using (var ms = new MemoryStream(data))
{
using(var br = new BinaryReader(ms))
{
Parse(br);
}
}
}
public void ParseFile(string filePath)
{
using (var fs = File.OpenRead(filePath))
{
using (var br = new BinaryReader(fs))
{
Parse(br);
}
}
}
/// <summary>
/// 解析数据
/// </summary>
public void Parse(BinaryReader br)
{
if (br == null)
throw new Exception("Fatal error : Param is null.");
if (_isParse)
throw new Exception("Fatal error : Package is already parse.");
_isParse = true;
// 读取版本号
DllVersion = br.ReadInt32();
ResVersion = br.ReadInt32();
GameVersion.PatchResDesc = ResVersion + " dllVer:" + DllVersion;
int fileCount = br.ReadInt32();
// 读取所有Bundle的数据
for(var i = 0; i < fileCount; i++)
{
var ele = PatchElement.Deserialize(br, IsInit);
if (Elements.ContainsKey(ele.Name))
throw new Exception($"Fatal error : has same pack file : {ele.Name}");
Elements.Add(ele.Name, ele);
}
}
}
PatchElement是AB包清单每一行数据的封装,PatchManifest的Parse里会循环创建一个Elements字典保存这些数据。
public class PatchElement
{
/// <summary>
/// 文件名称
/// </summary>
public string Name { private set; get; }
/// <summary>
/// 文件MD5
/// </summary>
public string MD5 { private set; get; }
/// <summary>
/// 文件版本
/// </summary>
public int Version { private set; get; }
/// <summary>
/// 文件大小
/// </summary>
public long SizeKB { private set; get; }
/// <summary>
/// 构建类型
/// buildin 在安装包中
/// ingame 游戏中下载
/// </summary>
public string Tag { private set; get; }
/// <summary>
/// 是否是安装包内的Patch
/// </summary>
public bool IsInit { private set; get; }
/// <summary>
/// 下载文件的保存路径
/// </summary>
public string SavePath;
/// <summary>
/// 每次更新都会先下载到Sandbox_Temp目录,防止下到一半重启导致逻辑不一致报错
/// temp目录下的文件在重新进入更新流程时先校验md5看是否要跳过下载
/// </summary>
public bool SkipDownload { get; set; }
public PatchElement(string name, string md5, int version, long sizeKB, string tag, bool isInit = false)
{
Name = name;
MD5 = md5;
Version = version;
SizeKB = sizeKB;
Tag = tag;
IsInit = isInit;
SkipDownload = false;
}
public void Serialize(BinaryWriter bw)
{
bw.Write(Name);
bw.Write(MD5);
bw.Write(SizeKB);
bw.Write(Version);
if (IsInit)
bw.Write(Tag);
}
public static PatchElement Deserialize(BinaryReader br, bool isInit = false)
{
var name = br.ReadString();
var md5 = br.ReadString();
var sizeKb = br.ReadInt64();
var version = br.ReadInt32();
var tag = EBundlePos.buildin.ToString();
if (isInit)
tag = br.ReadString();
return new PatchElement(name, md5, version, sizeKb, tag, isInit);
}
}
下面是下载数据封装,本质还是 UnityWebRequest
public class WebDataRequest : WebRequestBase, IDisposable
{
public WebDataRequest(string url) : base(url)
{
}
public override IEnumerator DownLoad()
{
// Check fatal
if (States != EWebRequestStates.None)
throw new Exception($"{nameof(WebDataRequest)} is downloading yet : {URL}");
States = EWebRequestStates.Loading;
// 下载文件
CacheRequest = new UnityWebRequest(URL, UnityWebRequest.kHttpVerbGET);
DownloadHandlerBuffer handler = new DownloadHandlerBuffer();
CacheRequest.downloadHandler = handler;
CacheRequest.disposeDownloadHandlerOnDispose = true;
CacheRequest.timeout = Timeout;
yield return CacheRequest.SendWebRequest();
// Check error
if (CacheRequest.isNetworkError || CacheRequest.isHttpError)
{
MotionLog.LogWarning($"Failed to download web data : {URL} Error : {CacheRequest.error}");
States = EWebRequestStates.Fail;
}
else
{
States = EWebRequestStates.Success;
}
}
public byte[] GetData()
{
if (States == EWebRequestStates.Success)
return CacheRequest.downloadHandler.data;
else
return null;
}
public string GetText()
{
if (States == EWebRequestStates.Success)
return CacheRequest.downloadHandler.text;
else
return null;
}
}