U3D客户端框架(资源管理篇)之资源热更新管理器 ResourceManager

一、资源热更新管理器模块设计

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();
            }
        }

    }
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值