一、资源热更新管理器模块设计
1.热更新是什么?
游戏或者软件内的 美术/脚本代码等资源 发生变化时,无需下载客户端重新进行安装,而是在应用程序启动的情况下,通过比对本地资源与CDN资源的MD5码,如果本地资源与CDN中的资源有差异,则优先使用CDN中的资源,以增量的方式进行下载更新这些变化了的 美术/脚本代码等资源。
2.热更新的商业价值
根据经验判断,每次强制用户换包将会造成 5%-15%+ 的用户流失,所以热更新对于商业价值来说是必要的。一个合格的产品采用热更新的方式更新资源是必要的。
3.资源热更新管理器 UML类静态视图
4.资源热更新管理器 网络拓扑图
5.热更新流程
1.启动游戏
2.初始化只读区资源版本和文件列表
3.获取CDN上的资源版本和文件列表
4.可写区是否有版本文件存在,如果没有,则拷贝资源版本和文件列表到可写区,不会把只读区的所有文件都往可写区都拷贝一份,不然太大了,可写区只存放增量资源。
5.可写区资源版本与CDN资源版本进行比对,如果版本号不一致进行资源比对
6.下载差异文件到可写区:1).可写区的MD5和CDN的MD5不一致并且只读区没有这个文件;
2).可写区的MD5和CDN的MD5不一致,只读区MD5和CDN MD5也不一致;
3).初始资源,可写区和只读区都没有;
4).初始资源,可写区没有,但是CDN上的MD5和只读区的MD5又不一致;
以上情况需要下载。
7.下载完成进入预加载流程。
二、代码设计
热更新管理器(ResourceManager)完整代码
//热更新管理器
public class ResourceManager : ManagerBase, IDisposable
{
#region 静态区域
/// <summary>
/// GetAssetBundleVersionList 根据字节数组获取资源包版本信息
/// 根据字节数组,获取资源包版本信息
/// </summary>
public static Dictionary<string, AssetBundleInfoEntity> GetAssetBundleVersionList(byte[] buffer, ref string version)
{
//使用zlib解压数据
buffer = ZlibHelper.DeCompressBytes(buffer);
//构造map
Dictionary<string, AssetBundleInfoEntity> dic = new Dictionary<string, AssetBundleInfoEntity>();
//使用buffer数组,初始化内存流对象
MMO_MemoryStream ms = new MMO_MemoryStream(buffer);
//assetbundle包的数量
int len = ms.ReadInt();
for (int i = 0; i < len; ++i)
{
//版本信息在完整数据的第二段
if (i == 0)
{
//删除字符串头部和尾部的所有空格,
//但是字符串中间的空格是不能被删去的。
version = ms.ReadUTF8String().Trim();
}
else
{
AssetBundleInfoEntity entity = new AssetBundleInfoEntity();
entity.AssetBundleName = ms.ReadUTF8String();
entity.MD5 = ms.ReadUTF8String();
entity.Size = ms.ReadULong();
entity.IsFirstData = ms.ReadByte() == 1;
entity.IsEncrypt = ms.ReadByte() == 1;
dic[entity.AssetBundleName] = entity;
}
}
return dic;
}
#endregion
//只读区管理器(其实看这个名字,我以为StreamingAsset才是可写区,LocalAsset是只读区)
public StreamingAssetsManager StreamingAssetsManager
{
get;
private set;
}
//可写区管理器
public LocalAssetsManager LocalAssetsManager
{
get;
private set;
}
//需要下载的资源包列表
private LinkedList<string> m_NeedDownloadList;
//检查版本更新下载时候的参数
private BaseParams m_DownloadingParams;
//只读区变量
private string m_StreamingAssetVersion;
//只读区资源包(ab包)信息
private Dictionary<string, AssetBundleInfoEntity> m_dicStreamingAssetsVersion = new Dictionary<string, AssetBundleInfoEntity>();
//是否存在只读区资源包信息(只读区有资源包吗?是这个意思吗?默认不存在?)
private bool m_IsExistStreamingAssetsBundleInfo = false;
private string m_LocalAssetsVersion;
//可写区资源包信息
private Dictionary<string, AssetBundleInfoEntity> m_dicLocalAssetsVersion = new Dictionary<string, AssetBundleInfoEntity>();
//CDN
//CSN资源版本号
private string m_CDNVersion;
//CDN资源版本号
public string CDNVersion
{
get { return m_CDNVersion; }
}
//cdn资源包信息
private Dictionary<string, AssetBundleInfoEntity> m_dicCDNVersion = new Dictionary<string, AssetBundleInfoEntity>();
public ResourceManager()
{
StreamingAssetsManager = new StreamingAssetsManager();
LocalAssetsManager = new LocalAssetsManager();
m_NeedDownloadList = new LinkedList<string>();
}
public override void Init()
{
}
#region 只读区
//初始化只读区资源包信息
public void InitStreamingAssetsBundleInfo()
{
ReadStreamingAssetsBundle(ConstDefine.VersionFileName, (byte[] buffer) =>
{
//只读区没有版本文件,初始化Cdn
if (null == buffer)
{
InitCDNAssetBundleInfo();
}
//只读区有版本文件,拿到本地的版本信息
else
{
m_IsExistStreamingAssetsBundleInfo = true;
m_dicStreamingAssetsVersion = GetAssetBundleVersionList(buffer, ref m_StreamingAssetVersion);
InitCDNAssetBundleInfo();
}
});
}
//读取只读区的资源包
internal void ReadStreamingAssetsBundle(string fileUrl, BaseAction<byte[]> onComplete)
{
StreamingAssetsManager.ReadAssetBundle(fileUrl, onComplete);
}
#endregion
#region 可写区
//检查可写区版本文件是否存在
private void CheckVersionFileExistsInLocal()
{
//输出流程log
GameEntry.Log(LogCategory.Resource, " CheckVersionFileExistsInLocal ");
//可写区有版本文件存在
if (LocalAssetsManager.GetVersionFileExists())
{
//加载可写区资源包信息
InitLocalAssetsBundleInfo();
}
//可写区无版本文件
else
{
//判断只读区 版本文件是否存在
//如果只读区 版本文件存在
if (m_IsExistStreamingAssetsBundleInfo)
{
//将只读取的版本文件 copy到可写区
InitVersionFileFromStreamingAssetsToLocal();
}
//检查是否需要热更
CheckVersionChange();
}
}
//将只读区的文件信息初始化到可写区(copy to)
private void InitVersionFileFromStreamingAssetsToLocal()
{
GameEntry.Log(LogCategory.Resource, "InitVersionFileFromStreamingAssetsToLocal");
m_dicLocalAssetsVersion.Clear();
//m_dicLocalAssetsVersion = new Dictionary<string, AssetBundleInfoEntity>();
//把只读区的文件信息拷贝到可写区
IEnumerator<KeyValuePair<string, AssetBundleInfoEntity>> iter =m_dicStreamingAssetsVersion.GetEnumerator();
while (iter.MoveNext())
{
string assetBundleName = iter.Current.Key;
AssetBundleInfoEntity entity = iter.Current.Value;
m_dicLocalAssetsVersion[assetBundleName] = new AssetBundleInfoEntity()
{
AssetBundleName = entity.AssetBundleName,
MD5 = entity.MD5,
Size = entity.Size,
IsFirstData = entity.IsFirstData,
IsEncrypt = entity.IsEncrypt,
};
}
//保存可写区的版本文件
LocalAssetsManager.SaveVersionFile(m_dicLocalAssetsVersion);
//保存可写区的资源版本号,= 只读取的资源版本号
m_LocalAssetsVersion = m_StreamingAssetVersion;
//可写区管理器 保存 可写区的资源版本号
LocalAssetsManager.SetResourceVersion(m_LocalAssetsVersion);
}
//初始化可写区的资源包信息
private void InitLocalAssetsBundleInfo()
{
GameEntry.Log(LogCategory.Resource, "InitLocalAssetsBundleInfo");
//拿到可写区的文件列表
m_dicLocalAssetsVersion = LocalAssetsManager.GetAssetBundleVersionList(ref m_LocalAssetsVersion);
//检查文件是否改变
CheckVersionChange();
}
public void SaveVersion(AssetBundleInfoEntity entity)
{
}
//保存资源版本号(用于检查版本,更新完毕后保存)
public void SetResourceVersion()
{
//本地的版本=cdn上的版本
m_LocalAssetsVersion = m_CDNVersion;
LocalAssetsManager.SetResourceVersion(m_LocalAssetsVersion);
}
#endregion
#region CDN
//初始化CDN资源包信息
private void InitCDNAssetBundleInfo()
{
StringBuilder sbr = StringHelper.PoolNew();
string url = sbr.AppendFormatNoGC("{0}{1}", GameEntry.Data.SysDataManager.CurrChannelConfig.RealSourceUrl, ConstDefine.VersionFileName).ToString();
StringHelper.PoolDel(ref sbr);
GameEntry.Log(LogCategory.Resource, url);
GameEntry.Http.SendData(url, OnInitCDNAssetBundleInfo, isGetData: true);
}
//初始化CDN资源包信息回调(访问版本资源之后的http回调)
private void OnInitCDNAssetBundleInfo(HttpCallBackArgs args)
{
GameEntry.Log(LogCategory.Normal," OnInitCDNAssetBundleInfointo");
if (!args.HasError)
{
m_dicCDNVersion = GetAssetBundleVersionList(args.Data, ref m_CDNVersion);
GameEntry.Log(LogCategory.Resource, "OnInitCDNAssetBundleInfo");
//检查本地的版本文件
CheckVersionFileExistsInLocal();
}
else
{
GameEntry.Log(LogCategory.Resource, args.Value);
}
}
#endregion
//获取资源包信息(返回CDN上的资源信息)
public AssetBundleInfoEntity GetCDNAssetBundleInfo(string assetBundlePath)
{
AssetBundleInfoEntity entity = null;
m_dicCDNVersion.TryGetValue(assetBundlePath, out entity);
return entity;
}
#region 检查更新&下载更新
//检查更新
private void CheckVersionChange()
{
GameEntry.Log(LogCategory.Resource, " CheckVersionChange");
//可写区存在版本文件(filelist文件)
if (LocalAssetsManager.GetVersionFileExists())
{
//判断只读区资源版本号 和 CDN资源版本号是否一致
if (!string.IsNullOrEmpty(m_LocalAssetsVersion) && m_LocalAssetsVersion.Equals(m_CDNVersion))
{
GameEntry.Log(LogCategory.Resource, " 可写区资源保本和CDN资源版本号 一致");
//一致 进入预加载流程
GameEntry.Procedure.ChangeState(ProcedureState.Preload);
}
else
{
GameEntry.Log(LogCategory.Resource, " 可写区资源保本和CDN资源版本号 不一致");
//不一致,本地文件列表和CDN上的文件列表做比对
GameEntry.Log(LogCategory.Normal, "out BeginCheckVersionChange1.0");
BeginCheckVersionChange();
}
}
//不存在filelist文件,下载初始资源
else
{
GameEntry.Log(LogCategory.Resource, "下载初始资源");
DownloadInitResources();
}
}
//下载初始资源
private void DownloadInitResources()
{
//TODO:派发 检查版本开始下载 事件
m_DownloadingParams = GameEntry.Pool.DequeueClassObject<BaseParams>();
m_DownloadingParams.Reset();
m_NeedDownloadList.Clear();
IEnumerator<KeyValuePair<string, AssetBundleInfoEntity>> iter = m_dicCDNVersion.GetEnumerator();
while (iter.MoveNext())
{
AssetBundleInfoEntity entity = iter.Current.Value;
//下载初始资源包
if (entity.IsFirstData)
{
m_NeedDownloadList.AddLast(entity.AssetBundleName);
}
}
if (m_NeedDownloadList.Count == 0)
{
BeginCheckVersionChange();
}
else
{
GameEntry.Download.BeginDownloadMulti(m_NeedDownloadList, OnDownloadMultiUpdate, OnDownloadMultiComplete);
}
}
//开始检查更新
private void BeginCheckVersionChange()
{
m_DownloadingParams = GameEntry.Pool.DequeueClassObject<BaseParams>();
m_DownloadingParams.Reset();
LinkedList<string> lstDel = new LinkedList<string>();
LinkedList<string> lstInconformity = new LinkedList<string>();
LinkedList<string> lstNeedDownload = new LinkedList<string>();
#region 找出需要删除的文件,然后删除
//一、寻找有差异的,需要删除的、需要下载的文件
IEnumerator<KeyValuePair<string, AssetBundleInfoEntity>> iter = m_dicLocalAssetsVersion.GetEnumerator();
//第一次循环:对可写区的文件循环
while (iter.MoveNext())
{
string assetBundleName = iter.Current.Key;
AssetBundleInfoEntity cdnAssetBundleInfoEntity = null;
if (m_dicCDNVersion.TryGetValue(assetBundleName, out cdnAssetBundleInfoEntity))
{
//本地有,CDN也有的,对比资源是否一致,不一致加入不一致列表
if (cdnAssetBundleInfoEntity.MD5.Equals(iter.Current.Value.MD5, StringComparison.CurrentCultureIgnoreCase))
{
lstInconformity.AddLast(assetBundleName);
}
}
else
{
//CDN没有的,本地也删除
lstDel.AddLast(assetBundleName);
}
LinkedListNode<string> iterInconformity = lstInconformity.First;
while (iterInconformity != null)
{
//差异文件
string inconformityFile = iterInconformity.Value;
//cdn中的ab包文件信息
AssetBundleInfoEntity cdnAssetBundleInfo = null;
m_dicCDNVersion.TryGetValue(inconformityFile, out cdnAssetBundleInfo);
AssetBundleInfoEntity streamingBundleInfo = null;
m_dicStreamingAssetsVersion.TryGetValue(inconformityFile, out streamingBundleInfo);
if (null == streamingBundleInfo)
{
//只读区没有
lstNeedDownload.AddLast(inconformityFile);
}
else
{
//只读区有,判断CDN中该文件的MD5和只读区的是否一致,如果一致,说明版本回退了。删掉可写区的,用只读区的
if (cdnAssetBundleInfo.MD5.Equals(streamingBundleInfo.MD5, StringComparison.CurrentCultureIgnoreCase))
{
//一致
lstDel.AddLast(inconformityFile);
}
else
{
//不一致,下载
lstNeedDownload.AddLast(inconformityFile);
}
}
iterInconformity = iterInconformity.Next;
}
}
//二、删除需要删除的文件
//第2.1次循环:删除需要删除的文件
for (LinkedListNode<string> iterNeedDelFile = lstDel.First; iterNeedDelFile != null;)
{
StringBuilder sbr = StringHelper.PoolNew();
string filePath = sbr.AppendFormatNoGC("{0}/{1}", GameEntry.Resource.LocalFilePath, iterNeedDelFile.Value).ToString();
StringHelper.PoolDel(ref sbr);
//删除文件
if (File.Exists(filePath))
{
File.Delete(filePath);
}
LinkedListNode<string> next = iterNeedDelFile.Next;
lstDel.Remove(iterNeedDelFile);
iterNeedDelFile = next;
}
#endregion
#region 检查需要下载的
//第3次循环:对CDN站点上的资源进行循环
iter = m_dicCDNVersion.GetEnumerator();
while (iter.MoveNext())
{
AssetBundleInfoEntity cdnAssetBundleInfo = iter.Current.Value;
//如果是初始资源
if (cdnAssetBundleInfo.IsFirstData)
{
//检查初始资源
//如果可写区没有CDN上的初始资源
if (!m_dicLocalAssetsVersion.ContainsKey(cdnAssetBundleInfo.AssetBundleName))
{
//如果可写区没有CDN上的这个资源文件,则去只读区检查一下
AssetBundleInfoEntity streamingAssetBundleInfo = null;
if (null != m_dicStreamingAssetsVersion)
{
m_dicStreamingAssetsVersion.TryGetValue(cdnAssetBundleInfo.AssetBundleName, out streamingAssetBundleInfo);
}
//只读区也不存在,需要下载
if (null == streamingAssetBundleInfo)
{
lstNeedDownload.AddLast(cdnAssetBundleInfo.AssetBundleName);
}
else
{
//如果只读区的MD5和CDN上的MD5数据不一致
if (!cdnAssetBundleInfo.MD5.Equals(streamingAssetBundleInfo.MD5, StringComparison.CurrentCultureIgnoreCase))
{
//MD5不一致
lstNeedDownload.AddLast(cdnAssetBundleInfo.AssetBundleName);
}
}
}
}
}
#endregion
//TODO:发送版本开始检查的事件
//GameEntry.
//进行下载
GameEntry.Download.BeginDownloadMulti(lstNeedDownload, OnDownloadMultiUpdate, OnDownloadMultiComplete);
}
/*
* 函数功能:下载中的回调
* t1:当前下载数量
* t2:总文件数量
* t3:当前下载的大小(单位:字节)
* t4:总下载大小(单位:字节)
*/
private void OnDownloadMultiUpdate(int t1, int t2, ulong t3, ulong t4)
{
m_DownloadingParams.IntParam1 = t1;
m_DownloadingParams.IntParam2 = t2;
m_DownloadingParams.ULongParam1 = t3;
m_DownloadingParams.ULongParam2 = t4;
//GameEntry.Log(LogCategory.Resource,"t1:{0} t2:{1} t3:{2} t4:{3}",t1,t2,t3,t4);
GameEntry.Event.CommonEvent.Dispatch(SysEventId.CheckVersionDownloadUpdate, m_DownloadingParams);
}
//下载完毕
private void OnDownloadMultiComplete()
{
//设置资源版本
SetResourceVersion();
//检查版本更新下载成功 事件
GameEntry.Event.CommonEvent.Dispatch(SysEventId.CheckVersionDownloadComplete);
GameEntry.Pool.EnqueueClassObject(m_DownloadingParams);
GameEntry.Procedure.ChangeState(ProcedureState.Preload);
}
#endregion
public void Dispose()
{
if (m_dicStreamingAssetsVersion != null)
{
m_dicStreamingAssetsVersion.Clear();
}
if (m_dicLocalAssetsVersion != null)
{
m_dicLocalAssetsVersion.Clear();
}
}
}