什么是热更新?

        游戏上线后,玩家下载第一个版本(1G左右或者更大),在之后运营的过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载,很显然体验非常不好。 热更新可以在不重新下载客户端的情况下,更新游戏的内容。 如王者荣耀,经常有下载补丁的时候。

如何判断哪些文件需要更新?

        为了知道我们需要更新的内容,我们就要知道哪些文件发生了改变,或者新增文件?所以我们需要在本地保存一份 需要热更新文件信息(名称,大小、Md5值)的 配置文件。在添加或改变资源时打新的热更包时我们和这个配置文件进行比较,相同资源名称的Md5值不一致,或者在配置文件中找不到该资源配置,就说明这个资源是发生改变或新增的,需要被加进热更包中。

如何实现热更新?

        上面我们知道了哪些文件需要被热更新,那么我们需要把这些文件放到服务器上,并记录这次补丁包的信息(版本信息、第几次热更,以及这些资源的详细信息配置)。用户打开App后会去拉取这个配置文件,并找到最后一次热更的资源信息与本地的资源进行MD5校验,不通过的就加入到热更列表,下载后保存到本地上,下次进入游戏的时候MD5就校验成功不会在出现重新下载服务器资源的情况,至此我们大致的思路就完成了。

资源热更流程图

unity热更新组件用什么 unity热更新一般更新什么_开发语言

打包时记录版本信息及所有资源信息

  • 包名
  • 版本号
  • 资源MD5文件信息(name,文件md5,size)
  • 保存到本地(Xml文件或二进制文件)

一键生成热更资源

如何生成AB包,以及实现: 一键生成热更资源

热更包及配置文件

数据结构:

using System;
using System.Collections.Generic;
using System.Xml.Serialization;

namespace Hot
{
    [Serializable]
    public class GameVersion
    {
        [XmlElement]
        public ServerVersionInfo[] ServerInfo;
    }
    /// <summary>
    /// 当前游戏版本对应的所有补丁
    /// </summary>
    [Serializable]
    public class ServerVersionInfo
    {
        [XmlAttribute]
        public string Version;
        [XmlElement]
        public List<Patches> Patches = new List<Patches>();
    }
    /// <summary>
    /// 一个总补丁包信息
    /// </summary>
    [Serializable]
    public class Patches
    {
        [XmlAttribute]
        public int Version;        // 第几次热更
        [XmlAttribute] 
        public string Desc;
        [XmlElement]
        public List<Patch> patches = new List<Patch>();
    }

    /// <summary>
    /// 单个补丁包信息
    /// </summary>
    [Serializable]
    public class Patch
    {
        [XmlAttribute]
        public string Name;
        [XmlAttribute]
        public string Url;
        [XmlAttribute]
        public long Size;
        [XmlAttribute]
        public string MD5;
    }
}

服务器部署

Apache服务器搭建:

我这边使用Apache: Apache Download 下载后将期解压到需要放置的目录下

找到 Apache24/conf/httpd.conf  将 Define SRVROOT改成Apache的解压目录,端口号默认时80,如果被占用可以自行修改

Define SRVROOT "F:\WebServer/Apache24"

unity热更新组件用什么 unity热更新一般更新什么_java_02

 运行 httpd.exe文件,测试可以在浏览器下访问 localhost 可以方位代表成功

服务器文件部署:

在  ...\Apache24\htdocs 文件夹下新建存放需要热更的AssetBundle的文件

unity热更新组件用什么 unity热更新一般更新什么_开发语言_03

 文件夹0.1: 版本文件夹,

文件夹 1: 第一次需要热更的资源

添加在服务器里添加GameVersion.xml文件:对应上面的 ServerInfo数据结构,每次有新的热更包时就往xml里添加 Patche.xml里的内容,需要回退的话只需要删除对应Patches的补丁配置

<?xml version="1.0"?>
<GameVersion>
    <ServerInfo Version="1.0.1">
        <Patches Version="1" Desc="测试热更">
            <patches Name="AssetBundle" Url="http://127.0.0.1/AssetBundle/1.0.1/1/AssetBundle" Size="1130" MD5="7baa969436d20f0e1b8a41e78d3cb23d" />
            <patches Name="audio" Url="http://127.0.0.1/AssetBundle/1.0.1/1/audio" Size="20555034" MD5="f4fc534e2615ca3a0c199e29a4f22df6" />
            <patches Name="image" Url="http://127.0.0.1/AssetBundle/1.0.1/1/image" Size="563082" MD5="1da1ee923a73386d0871f0287954ffa4" />
            <patches Name="material" Url="http://127.0.0.1/AssetBundle/1.0.1/1/material" Size="8010" MD5="08fc079c4a19f701c3e914e2d591f7a8" />
            <patches Name="prefab" Url="http://127.0.0.1/AssetBundle/1.0.1/1/prefab" Size="5034" MD5="90bd7fb6bee13fdabc642bb445dbaa98" />
        </Patches>
    </ServerInfo>
</GameVersion>

到这里热更新的准备都已完成,接下来就是实现热更流程

文件下载基类

using System;
using System.Collections;
using System.IO;

namespace Hot
{
    public abstract class DownloadItemBase
    {
        protected string url;
        public string Url => url;
        
        protected string fileName;
        public string FileName => fileName;
        
        protected string fileNameWithoutExt;
        public string FileNameWithoutExt => fileNameWithoutExt;

        protected string ext;
        public string Ext;

        protected string fullName;
        public string FullName => fullName;
        
        protected string fullNameWithoutExt;
        public string FullNameWithoutExt => fullNameWithoutExt;

        protected long size;
        public long Size => size;

        protected bool isLoading = false;
        public bool IsLoading = false;

        public DownloadItemBase(string savePath,string url,long size)
        {
            isLoading = false;
            this.url = url;
            fileNameWithoutExt = Path.GetFileNameWithoutExtension(url);
            ext = Path.GetExtension(url);
            fileName = Path.GetFileName(url);
            fullName = $"{savePath}/{fileName}";
            fullNameWithoutExt = $"{savePath}/{fileNameWithoutExt}";
            this.size = size;
        }
        
        public abstract void Destroy();
        
        public abstract IEnumerator StartDownload(Action<bool> callBack);
        
        public abstract float GetCurProgress();
    }
}

AB包文件下载类

using System;
using System.Collections;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;

namespace Hot
{
    public class ABDownloadItem:DownloadItemBase
    {
        private UnityWebRequest webRequest;
        public ABDownloadItem(string savePath, string url, long size) : base(savePath, url, size)
        {
        }
        
        public override void Destroy()
        {
            webRequest.Dispose();
        }
        
        public override IEnumerator StartDownload(Action<bool> callBack = null)
        {
            webRequest = UnityWebRequest.Get(Url);
            webRequest.timeout = 30;
            isLoading = true;
            yield return webRequest.SendWebRequest();
            if (webRequest.result == UnityWebRequest.Result.Success)
            {
                FileUtils.SaveFile(FullName, webRequest.downloadHandler.data);
                if(null != callBack) callBack(true);
            }
            else
            {
                Debug.LogError($"download {Url} fail err: {webRequest.error}");
                if(null != callBack) callBack(false);
            }
            isLoading = false;
        }
        
        public override float GetCurProgress()
        {
            return webRequest != null ? webRequest.downloadProgress : 0;
        }

    }
}

核心热更新管理类

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Build;
using Core.Base;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;

namespace Hot
{
    public class HotManager:Singleton<HotManager>
    {
        private string ServerGameVersionPath = $"{Application.persistentDataPath}/GameVersion.xml";
        private string version;
        private string packageName;
        private GameVersion gameVersion;
        private string hotDesc;
        public string HotDesc => hotDesc;
        
        // 服务器上需要热更的补丁包
        private List<Patch> serverHotPatches = new List<Patch>();
        private Dictionary<string,Patch> serverHotPatchDic = new Dictionary<string, Patch>();
        // 需要下载的补丁包
        private List<Patch> downLoadPatchs = new List<Patch>();
        // 下载完成的补丁包
        private List<Patch> alreadyPatchLists = new List<Patch>();
        private ABDownloadItem curDownloadItem;
        private int reloadCount = 0;
        private float hotAllSize = 0;
        public float HotAllSize => hotAllSize;

        private bool isLoading = false;
        public bool IsLoading => isLoading;
        // 加载完成回调
        private Action hotCompeleteHandler;
        // 加载失败回调
        private Action<List<Patch>> hotFailHandler;

        private MonoBehaviour corMono;
        public void Init(MonoBehaviour mono)
        {
            corMono = mono;
        }

        /// <summary>
        /// 检查版本是否需要更新
        /// </summary>
        /// <param name="callBack"></param>
        public void CheckVersionNeedHot(Action<bool> callBack)
        {
            VersionInfo versionInfo = XmlSerializerOpt.Deserialize<VersionInfo>(PathUtlis.LOCAL_VERSION_PATH);
            version = versionInfo.Version;
            packageName = versionInfo.PackageName;

            corMono.StartCoroutine(LoadServerGameVersion(() =>
            {
                // 判断是否需要热更
                GetServerPatches();
                CheckDownloadPatches();
                hotAllSize = serverHotPatches.Sum(x => x.Size);
                callBack(downLoadPatchs.Count > 0);
            }));
        }

        private IEnumerator LoadServerGameVersion(Action callBack)
        {
            UnityWebRequest webRequest = UnityWebRequest.Get("http://127.0.0.1/GameVersion.xml");
            webRequest.timeout = 30;
            yield return webRequest.SendWebRequest();

            if (webRequest.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"加载服务器游戏配置失败: {webRequest.error}");
            }
            else
            {
                Debug.Log(ServerGameVersionPath);
                if(File.Exists(ServerGameVersionPath)) File.Delete(ServerGameVersionPath);
                FileUtils.SaveFile(ServerGameVersionPath,webRequest.downloadHandler.data);
                gameVersion = XmlSerializerOpt.Deserialize<GameVersion>(ServerGameVersionPath);
            }

            callBack();
        }

        private void GetServerPatches()
        {
            if (gameVersion != null && gameVersion.ServerInfo != null)
            {
                for (int i = 0; i < gameVersion.ServerInfo.Length; i++)
                {
                    if (gameVersion.ServerInfo[i].Version == version)
                    {
                        List<Patches> patches = gameVersion.ServerInfo[i].Patches;
                        if (patches != null && patches.Count > 0)
                        {
                            serverHotPatches = patches[patches.Count - 1].patches;
                            hotDesc = patches[patches.Count - 1].Desc;
                        }
                        break;
                    }
                }
            }
        }

        // 检查需要去下载的补丁
        private void CheckDownloadPatches()
        {
            downLoadPatchs.Clear();
            for (int i = 0; i < serverHotPatches.Count; i++)
            {
                serverHotPatchDic.Add(serverHotPatches[i].Name, serverHotPatches[i]);
                AddDownloadPatch(serverHotPatches[i]);
            }
        }

        private void AddDownloadPatch(Patch patch)
        {
            string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";
            if (!File.Exists(savePath))
            {
                downLoadPatchs.Add(patch);
            }
            else
            {
                if (patch.MD5 != MD5Utils.GenerateMD5(savePath))
                {
                    downLoadPatchs.Add(patch);
                }
            }
        }

        public void StartHot(Action hotCompeleteHandler,Action<List<Patch>> hotFailHandler)
        {
            this.hotCompeleteHandler = hotCompeleteHandler;
            this.hotFailHandler = hotFailHandler;
            corMono.StartCoroutine(StartLoad());
        }

        private IEnumerator StartLoad(List<Patch> patches = null)
        {
            if (patches == null)
            {
                patches = downLoadPatchs;
            }

            if (!Directory.Exists(PathUtlis.LocalAssetBundlePath))
                Directory.CreateDirectory(PathUtlis.LocalAssetBundlePath);
            
            List<ABDownloadItem> downloadItems = new List<ABDownloadItem>();
            for (int i = 0; i < patches.Count; i++)
            {
                downloadItems.Add(new ABDownloadItem(PathUtlis.LocalAssetBundlePath,patches[i].Url,patches[i].Size));
            }

            isLoading = true;
            for (int i = 0; i < downloadItems.Count; i++)
            {
                ABDownloadItem item = downloadItems[i];
                curDownloadItem = item;
                yield return corMono.StartCoroutine(item.StartDownload((success) =>
                {
                    if (success)
                    {
                        Patch patch = FindPatch(item.FileName);
                        if (patch != null)
                        {
                            if(!alreadyPatchLists.Contains(patch)) alreadyPatchLists.Add(patch);
                        }
                    }
                    else
                    {
                        Debug.LogError($"{item.FileName} 下载失败,尝试重新下载");
                    }
                    item.Destroy();
                }));
            }

            // 重新比较文件md5,避免文件下载失败
            yield return VerifyMD5(downLoadPatchs);
        }

        //校验下载后的文件
        private IEnumerator VerifyMD5(List<Patch> patches)
        {
            List<Patch> downPatchList = new List<Patch>();
            for (int i = 0; i < patches.Count; i++)
            {
                Patch patch = patches[i];
                string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";
                if (!File.Exists(savePath))
                {
                    downPatchList.Add(patch);
                }
                else
                {
                    if (patch.MD5 != MD5Utils.GenerateMD5(savePath))
                    {
                        downPatchList.Add(patch);
                    }
                }
            }
            
            if (downPatchList.Count > 0)
            {
                reloadCount++;
                if (reloadCount < 5)
                {
                    yield return corMono.StartCoroutine(StartLoad(downPatchList));
                }
                else
                {
                    isLoading = false;
                    if (null != hotFailHandler) hotFailHandler(downPatchList);
                }
            }
            else
            {
                isLoading = false;
                if (null != hotCompeleteHandler) hotCompeleteHandler();
            }
        }

        private Patch FindPatch(string name)
        {
            Patch patch = null;
            serverHotPatchDic.TryGetValue(name, out patch);
            return patch;
        }

        public float GetProgress()
        {
            float loadedSize = alreadyPatchLists.Sum(x => x.Size);
            float curloadSize = curDownloadItem.GetCurProgress() * curDownloadItem.Size;
            float progress = (loadedSize + curloadSize) / hotAllSize;
            return progress;
        }
    }
}

UI测试效果图

unity热更新组件用什么 unity热更新一般更新什么_java_04

unity热更新组件用什么 unity热更新一般更新什么_开发语言_05

还有个问题,这样下载下来的资源会直接被别人拿走使用,为了数据的安全,我们可以对资源进行加密处理,我使用的是AES,也没有什么难点,就是在一键生成AB包后使用AES对文件加密,然后加载资源的时候使用 字节数组加载,LoadFromMemory的缺点就是多占一份内存,对于内

存吃紧的就不适合用了,或者参考:Unity3D加密Assetbundle(不占内存)

private void DecryptAssetBundle()
    {
        string abPath = Path.Combine(PathUtlis.AssetBundlePath, path);
        // 解密被加载的AB包
        byte[] result = AESUtils.AESFileDecryptToByte(abPath,"ENCRYPT_KEY");
        if (result == null)
        {
            Debug.LogError($"AES Decrypt {abPath} file fail");  
            return;
        }
        AssetBundle asset = AssetBundle.LoadFromMemory(result);
    }