前言:

  不管对于单机还是网络游戏,热更新已经成了标配。所谓热更,指的就是在无需重新打包的情况下完成资源、数据和代码的更新。

  本篇文章主要针对的是Unity3D开发的项目,其热更思路也可以应用到其他引擎诸如Cocos2D中。当然对于网页游戏或者小程序而言,开发语言使用lua、TyppScript、JavaScript等解释性语言,可以边运行边转换,资源和代码放到网络空间实时更新即可。

第一章  设计思路、本地网络空间的部署与资源划分

一,设计思路

热更包含代码热更、表格数据热更和美术资源热更三部分。

使用MD5效验文件版本,删除不在版本控制内的资源,如有变动替换并下载新的资源。

以下热更均在启动界面完成,热更完毕之后再切换到登录界面。

代码热更:
替换lua脚本后,开启lua解释器。

表格数据热更:
热更完毕后,替换表格数据资源。

------------美术资源分为图片资源和模型资源热更------------
图片资源热更:
热更完毕后进入游戏。

模型资源热更:
热更完毕后进入游戏。

以上流程和思路可以根据具体项目自行调整。

二,本地网络空间部署

  这个本来属于运维服务器的事情,但我们在开发的时候可以先用免费的软件来代替。我选用的是hfs网络文件管理器,在我的资源中有相关链接,需要的同学可以自己下载使用。

unity ios热更新 unity 热更新方案_android

  这里对于网络空间的部署,只是为了方便在客户端中进行热更下载。不管使用什么都可以,只要能提供下载就行。

  将链接拷贝下来,留着测试使用。

三,资源划分 

  按照设计思路,我们可以先划分出不同平台的不同目录,把Bytes、Scripts、Module、UI文件夹,在同级目录下补充一个效验用的Bundle.txt文件。各个文件夹下面具体的资源划分,可以根据项目需要调整。

  这里的文件夹划分仅仅是提供一个思路,只需要有相对应的解析方法,资源的划分完全可以使用自己的设计。

第二章  MD5效验与热更

一,MD5效验

  MD5最初是为加密而设计,但由于其存在漏洞而被舍弃。但它可以为我们提供数据完整性的效验,因为可以用来对比和效验需要热更的文件。

  多数脚本语言中,已经为我们提供好了解析MD5的库,在C#中它是:

using System.Security.Cryptography;//包含MD5库

  由此我们可以总结出“遍历该文件夹下所有子文件,并生成相对应的MD5”的需求,从而引申开发如下脚本,记录所有文件的json并存储为Bundle.txt。

1,先申明所需要用的类,这两个类在相应解析的时候也需要使用:

using System;

//MD5信息
[Serializable]
public class MD5Message
{
    public string file;//文件位置及名字
    public string md5;//MD5效验结果
    public string fileLength;//文件长度
}

//MD5全部信息
[Serializable]
public class FileMD5
{
    public string length;//总长度
    public MD5Message[] files;
}

2,通过遍历所有文件,得出正确的MD5值,删除并更新Bundle.txt

using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库

/// <summary>
/// MD5 效验器
/// 遍历该文件夹下所有子文件,并生成相对应的MD5
/// </summary>
public class MD5ACharm : MonoBehaviour
{
    [MenuItem("MD5效验器/平台/IOS平台")]
    static void BuildReleaseIOSBundle()
    {
        BuildBundleStart("iOS");
    }
    [MenuItem("MD5效验器/平台/Android平台")]
    static void BuildReleaseAndroidBundle()
    {
        BuildBundleStart("Android");
    }

    [MenuItem("MD5效验器/平台/Windows平台")]
    static void BuildReleaseWindowsBundle()
    {
        BuildBundleStart("Win");
    }

    static void BuildBundleStart(string _path)
    {
        ABPath = _path;
        Caching.ClearCache();//清除所有缓存   
        string path = GetTempPath();
        DeleteTempBundles(path);   //删除旧的MD5版本文件
        CreateBundleVersionNumber(path);
        AssetDatabase.Refresh();
    }

    private static Dictionary<string, string> m_BundleMD5Map = new Dictionary<string, string>();

    /// <summary>
    /// 删除指定文件
    /// </summary>
    /// <param name="target"></param>
    static void DeleteTempBundles(string path)
    {
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);
        string[] bundleFiles = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
        foreach (string s in bundleFiles)
        {
            if(s== "Bundle.txt") File.Delete(s);
        }
    } 
    
    static void CreateBundleVersionNumber(string bundlePath)
    {     
        FileMD5 _file = new FileMD5();
        string[] contents = Directory.GetFiles(bundlePath, "*.*", SearchOption.AllDirectories);
        string extension;
        string fileName = "";
        string fileMD5 = "";
        long allLength = 0;
        int fileLen;
        m_BundleMD5Map.Clear();
        for (int i = 0; i < contents.Length; i++)
        {
            fileName = contents[i].Replace(GetTempPath(), "").Replace("\\", "/");
            extension = Path.GetExtension(contents[i]);
            if (extension != ".meta")
            {
                fileMD5 = GetMD5HashFromFile(contents[i]);
                fileLen = File.ReadAllBytes(contents[i]).Length;
                allLength += fileLen;
                m_BundleMD5Map.Add(fileName, fileMD5 + "+" + fileLen);
            }
        }

        var _list = new List<MD5Message>();
        foreach (KeyValuePair<string, string> kv in m_BundleMD5Map)
        {
            string[] nAndL = kv.Value.Split('+');
            MD5Message _md5 = new MD5Message();
            _md5.file = kv.Key;
            _md5.md5 = nAndL[0];
            _md5.fileLength = nAndL[1];
            _list.Add(_md5);
        }
        var _md5All = new MD5Message[_list.Count];
        for (var _i = 0; _i < _list.Count; _i++)
        {
            _md5All[_i] = _list[_i];
        }

        _file.length = "" + allLength;

        _file.files = _md5All;

        var _filePath = JsonUtility.ToJson(_file);

        Debug.LogError(_filePath);

        File.WriteAllText(GetTempPath() + "Bundle.txt", _filePath);
        m_BundleMD5Map.Clear();

    } 

    /// <summary>获取文件的md5校验码</summary>
    static string GetMD5HashFromFile(string fileName)
    {
        if (File.Exists(fileName))
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
                sb.Append(retVal[i].ToString("x2"));
            return sb.ToString();
        }
        return null;

    }

    //指定下载路径
    static string ABPath = "Win";
    static string GetTempPath(string _path="")
    {
        var _str = GetPathName() + "/MD5" + ABPath + "/"+_path;       
        return _str;
    }
   
    //网络空间的位置
    static string GetPathName()
    {
        return "E:/MyServer/Chief";
    }
}

  这里提供了三个常见的IOS、Android和Win平台,以提供不同平台的使用。

  有个知识点就是使用Directory.GetFiles(bundlePath, "*.*", SearchOption.AllDirectories);的方法,可以获取到该文件夹下的所有文件。

  我在需要热更的文件夹下放了一些测试资源,测试步骤及结果如下:

文件目录:

unity ios热更新 unity 热更新方案_unity ios热更新_02

点击生成:

unity ios热更新 unity 热更新方案_unity ios热更新_03

生成结果:

unity ios热更新 unity 热更新方案_unity_04

unity ios热更新 unity 热更新方案_bundle_05

3,根据MD5效验并进行热更

  根据设计思路,我们需要先下载Bundle.txt,从而获得热更资源的版本信息。首先对于不在版本管理内的资源进行删除,尔后对于不符合MD5效验的旧资源进行删除和替代:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;

/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
    private bool IS_ANDROID = false;

    private static Text fillAmountTxt;

    void Start()
    {        
        transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
        fillAmountTxt = transform.Find("Text").GetComponent<Text>();
        TestPrint();
    }

    //输出测试
    void TestPrint()
    {
        Debug.Log("*******测试打印所有文件目录*******");
        var _str = "";
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            Debug.Log("替换过程:" + idx + "   " + GetTerracePath() + "   " + _s);
            _str += _s + "\n";
        }
        transform.Find("CeshiText").GetComponent<Text>().text = _str;
        Debug.Log("**************结束打印************");
    }

    //更新版本
    private void StartUpdate()
    {
        StartCoroutine(VersionUpdate());
    }

    private int allfilesLength = 0;
    /// <summary>
    /// 版本更新       
    /// </summary>
    /// <returns></returns>
    IEnumerator VersionUpdate()
    {      
        WWW www = new WWW("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
        yield return www;
        if (www.isDone && string.IsNullOrEmpty(www.error))
        {
            List<BundleInfo> bims = new List<BundleInfo>();
            FileMD5 date = JsonUtility.FromJson<FileMD5>(www.text);

            DeleteOtherBundles(date);//删除所有不受版本控制的文件

            Debug.LogError(www.text);
      
            //Debug.Log(data.Contains());
            var _list = date.files;

            string md5, file, path;
            int lenth;
            for (int i = 0; i <_list.Length; i++)
            {
                MD5Message _md5 = _list[i];

                Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
                file = _md5.file;

                path = PathUrl(file);

                md5 = GetMD5HashFromFile(path);

                if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
                {
                    bims.Add(new BundleInfo()
                    {
                        Url = HttpDownLoadUrl(file),
                        Path = path
                    });
                    lenth = int.Parse(_md5.fileLength);
                    allfilesLength += lenth;
                }
            }
            if (bims.Count > 0)
            {
                Debug.LogError("开始尝试更新");
                StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
                    OpenLodingShow("自动更新中...", progress, allfilesLength);
                }, (isfinish) => {
                    if (isfinish)                        
                        StartCoroutine(VersionUpdateFinish());
                    else
                    {                 
                        StartCoroutine(VersionUpdate());
                    }
                }));
            }
            else
            {               
                StartCoroutine(VersionUpdateFinish());
            }
        }
    }

    // 删除所有不受版本控制的所有文件
    void DeleteOtherBundles(FileMD5 _md5)
    {
        Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            if (!FindNameInFileMD5(_md5,_s))
            {
                File.Delete(idx);
                Debug.LogError(_s + "不存在");
            }                     
        }
        Debug.Log("~~~~~~~结束删除~~~~~~~");
    }

    /// <summary>获取文件的md5校验码</summary>
    public string GetMD5HashFromFile(string fileName)
    {
        if (File.Exists(fileName))
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
                sb.Append(retVal[i].ToString("x2"));
            return sb.ToString();
        }
        return null;
    }

    static bool FindNameInFileMD5(FileMD5 date,string _name)
    {
        foreach (var _m in date.files)
        {
            if (_m.file == _name) return true;
        }
        return false;
    }

    //脚本替换(lua等)验证
    IEnumerator VersionUpdateFinish()
    {           
        Debug.Log("lua验证");
        string sPath = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt";
        WWW www = new WWW(sPath);
        yield return www;
        Debug.LogError(www.text);
        transform.Find("CeshiText").GetComponent<Text>().text = www.text;

        //StartCoroutine(VersionUpdateImage());
        //StartCoroutine(VersionUpdateModel());
    }

    图片验证
    //IEnumerator VersionUpdateImage()
    //{
    //    Debug.Log("图片替换验证");
    //    string sPath = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
    //    WWW www = new WWW(sPath);
    //    yield return www;
    //    if (www.isDone && string.IsNullOrEmpty(www.error))
    //    {
    //        var _img = www.texture;
    //        Texture2D tex = new Texture2D(_img.width,_img.height);
    //        www.LoadImageIntoTexture(tex);
    //        Sprite sprite = Sprite.Create(tex, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
    //        transform.Find("Button").GetComponent<Image>().sprite = sprite;
    //    }

    //}

    模型验证
    //IEnumerator VersionUpdateModel()
    //{        
    //    string s = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
    //    string progress = null;
    //    WWW w = new WWW(s);
    //    while (!w.isDone)
    //    {
    //        progress = (((int)(w.progress * 100)) % 100) + "%";
    //        Debug.Log("加载模型:" + progress);
    //        yield return null;
    //    }
    //    yield return w;
    //    if (w.error != null)
    //    {
    //        Debug.Log("error:" + w.url + "\n" + w.error);
    //    }
    //    else
    //    {            
    //        AssetBundle bundle = w.assetBundle;
    //        GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
    //        GameObject modelClone = Instantiate(modelPre);
    //        AssetBundle.UnloadAllAssetBundles(false);
    //    }
    //}

    /// <summary>
    /// 路径比对
    /// </summary>  
    public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
    {
        //Debug.Log("开始路径对比");
        int num = 0;
        string dir;
        for (int i = 0; i < infos.Count; i++)
        {
            BundleInfo info = infos[i];
            Debug.LogError(info.Url);
            WWW www = new WWW(info.Url);
            yield return www;
            if (www.isDone && string.IsNullOrEmpty(www.error))
            {               
                try
                {
                    string filepath = info.Path;
                    dir = Path.GetDirectoryName(filepath);
                    if (!Directory.Exists(dir))
                        Directory.CreateDirectory(dir);
                    File.WriteAllBytes(filepath, www.bytes);
                    num++;
                    if (LoopCallBack != null)
                        LoopCallBack.Invoke((float)num / infos.Count);
                    Debug.Log(dir+"下载完成");
                }
                catch (Exception e)
                {
                    Debug.Log("下载失败"+e);
                }
            }
            else
            {
                Debug.Log("下载错误"+www.error);
            }
        }
        if (CallBack != null)
            CallBack.Invoke(num == infos.Count);
    }

    /// <summary>
    /// 记录信息
    /// </summary>
    public struct BundleInfo
    {
        public string Path { get; set; }
        public string Url { get; set; }
        public override bool Equals(object obj)
        {
            return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
        }
        public override int GetHashCode()
        {
            return Url.GetHashCode();
        }
    }

    /// <summary>
    /// loadpage展示
    /// </summary>
    /// <param name="text"></param>
    /// <param name="progress"></param>
    /// <param name="filealllength"></param>
    public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
    {
        Debug.LogError(text+"   "+ progress + "   " + filealllength);
        fillAmountTxt.text = text + progress + "  文件序列:" + filealllength;
        if (progress >= 1) fillAmountTxt.text = "更新完成";
    }

    void Update()
    {
       
    }



    private void OnDestroy()
    {
        //if (DestoryLua != null)
        //    DestoryLua();
        //UpdateLua = null;
        //DestoryLua = null;
        //StartLua = null;
        // LuaEnvt.Dispose();
    }

    
    string HttpDownLoadUrl(string _str)
    {
        return "http://192.168.6.178/Chief/MD5Android/" + _str;
    }
    //根据不同路径,对下载路径进行封装
    string PathUrl(string _str)
    {
        var _path= GetTerracePath() + "/Android/" + _str;
        return _path;
    }

    //获得不同平台的路径
    string GetTerracePath(bool _isAndroid=false)
    {
        string sPath = Application.persistentDataPath;
        if(_isAndroid) sPath = "file://" + sPath;        
        return sPath;
    }
}

  这里为了方便阅读,屏蔽了图片加载和模型加载的测试。点击运行结果如下:

unity ios热更新 unity 热更新方案_unity ios热更新_06

unity ios热更新 unity 热更新方案_android_07

第三章  测试用例

  这里只做简单的引申,在我们项目中,常用的也就是图片、txt文件以及AB包。

  除了第二章中使用txt文件热更,在这里添加了图片与AB包的测试;具体打包方法可以使用官方插件,全部代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;

/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
    private bool IS_ANDROID = false;

    private static Text fillAmountTxt;

    void Start()
    {        
        transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
        fillAmountTxt = transform.Find("Text").GetComponent<Text>();
        TestPrint();
    }

    //输出测试
    void TestPrint()
    {
        Debug.Log("*******测试打印所有文件目录*******");
        var _str = "";
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            Debug.Log("替换过程:" + idx + "   " + GetTerracePath() + "   " + _s);
            _str += _s + "\n";
        }
        transform.Find("CeshiText").GetComponent<Text>().text = _str;
        Debug.Log("**************结束打印************");
    }

    //更新版本
    private void StartUpdate()
    {
        StartCoroutine(VersionUpdate());
    }

    private int allfilesLength = 0;
    /// <summary>
    /// 版本更新       
    /// </summary>
    /// <returns></returns>
    IEnumerator VersionUpdate()
    {      
        WWW www = new WWW("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
        yield return www;
        if (www.isDone && string.IsNullOrEmpty(www.error))
        {
            List<BundleInfo> bims = new List<BundleInfo>();
            FileMD5 date = JsonUtility.FromJson<FileMD5>(www.text);

            DeleteOtherBundles(date);//删除所有不受版本控制的文件

            Debug.LogError(www.text);
      
            //Debug.Log(data.Contains());
            var _list = date.files;

            string md5, file, path;
            int lenth;
            for (int i = 0; i <_list.Length; i++)
            {
                MD5Message _md5 = _list[i];

                Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
                file = _md5.file;

                path = PathUrl(file);

                md5 = GetMD5HashFromFile(path);

                if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
                {
                    bims.Add(new BundleInfo()
                    {
                        Url = HttpDownLoadUrl(file),
                        Path = path
                    });
                    lenth = int.Parse(_md5.fileLength);
                    allfilesLength += lenth;
                }
            }
            if (bims.Count > 0)
            {
                Debug.LogError("开始尝试更新");
                StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
                    OpenLodingShow("自动更新中...", progress, allfilesLength);
                }, (isfinish) => {
                    if (isfinish)                        
                        StartCoroutine(VersionUpdateFinish());
                    else
                    {                 
                        StartCoroutine(VersionUpdate());
                    }
                }));
            }
            else
            {               
                StartCoroutine(VersionUpdateFinish());
            }
        }
    }

    // 删除所有不受版本控制的所有文件
    void DeleteOtherBundles(FileMD5 _md5)
    {
        Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            if (!FindNameInFileMD5(_md5,_s))
            {
                File.Delete(idx);
                Debug.LogError(_s + "不存在");
            }                     
        }
        Debug.Log("~~~~~~~结束删除~~~~~~~");
    }

    /// <summary>获取文件的md5校验码</summary>
    public string GetMD5HashFromFile(string fileName)
    {
        if (File.Exists(fileName))
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
                sb.Append(retVal[i].ToString("x2"));
            return sb.ToString();
        }
        return null;
    }

    static bool FindNameInFileMD5(FileMD5 date,string _name)
    {
        foreach (var _m in date.files)
        {
            if (_m.file == _name) return true;
        }
        return false;
    }

    //脚本替换(lua等)验证
    IEnumerator VersionUpdateFinish()
    {           
        Debug.Log("lua验证");
        string sPath = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt";
        WWW www = new WWW(sPath);
        yield return www;
        Debug.LogError(www.text);
        transform.Find("CeshiText").GetComponent<Text>().text = www.text;

        StartCoroutine(VersionUpdateImage());
        StartCoroutine(VersionUpdateModel());
    }

    //图片验证
    IEnumerator VersionUpdateImage()
    {
        Debug.Log("图片替换验证");
        string sPath = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
        WWW www = new WWW(sPath);
        yield return www;
        if (www.isDone && string.IsNullOrEmpty(www.error))
        {
            var _img = www.texture;
            Texture2D tex = new Texture2D(_img.width, _img.height);
            www.LoadImageIntoTexture(tex);
            Sprite sprite = Sprite.Create(tex, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
            transform.Find("Button").GetComponent<Image>().sprite = sprite;
        }

    }

    //模型验证
    IEnumerator VersionUpdateModel()
    {
        string s = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
        string progress = null;
        WWW w = new WWW(s);
        while (!w.isDone)
        {
            progress = (((int)(w.progress * 100)) % 100) + "%";
            Debug.Log("加载模型:" + progress);
            yield return null;
        }
        yield return w;
        if (w.error != null)
        {
            Debug.Log("error:" + w.url + "\n" + w.error);
        }
        else
        {
            AssetBundle bundle = w.assetBundle;
            GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
            GameObject modelClone = Instantiate(modelPre);
            AssetBundle.UnloadAllAssetBundles(false);
        }
    }

    /// <summary>
    /// 路径比对
    /// </summary>  
    public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
    {
        //Debug.Log("开始路径对比");
        int num = 0;
        string dir;
        for (int i = 0; i < infos.Count; i++)
        {
            BundleInfo info = infos[i];
            Debug.LogError(info.Url);
            WWW www = new WWW(info.Url);
            yield return www;
            if (www.isDone && string.IsNullOrEmpty(www.error))
            {               
                try
                {
                    string filepath = info.Path;
                    dir = Path.GetDirectoryName(filepath);
                    if (!Directory.Exists(dir))
                        Directory.CreateDirectory(dir);
                    File.WriteAllBytes(filepath, www.bytes);
                    num++;
                    if (LoopCallBack != null)
                        LoopCallBack.Invoke((float)num / infos.Count);
                    Debug.Log(dir+"下载完成");
                }
                catch (Exception e)
                {
                    Debug.Log("下载失败"+e);
                }
            }
            else
            {
                Debug.Log("下载错误"+www.error);
            }
        }
        if (CallBack != null)
            CallBack.Invoke(num == infos.Count);
    }

    /// <summary>
    /// 记录信息
    /// </summary>
    public struct BundleInfo
    {
        public string Path { get; set; }
        public string Url { get; set; }
        public override bool Equals(object obj)
        {
            return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
        }
        public override int GetHashCode()
        {
            return Url.GetHashCode();
        }
    }

    /// <summary>
    /// loadpage展示
    /// </summary>
    /// <param name="text"></param>
    /// <param name="progress"></param>
    /// <param name="filealllength"></param>
    public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
    {
        Debug.LogError(text+"   "+ progress + "   " + filealllength);
        fillAmountTxt.text = text + progress + "  文件序列:" + filealllength;
        if (progress >= 1) fillAmountTxt.text = "更新完成";
    }

    void Update()
    {
       
    }



    private void OnDestroy()
    {
        //if (DestoryLua != null)
        //    DestoryLua();
        //UpdateLua = null;
        //DestoryLua = null;
        //StartLua = null;
        // LuaEnvt.Dispose();
    }

    
    string HttpDownLoadUrl(string _str)
    {
        return "http://192.168.6.178/Chief/MD5Android/" + _str;
    }
    //根据不同路径,对下载路径进行封装
    string PathUrl(string _str)
    {
        var _path= GetTerracePath() + "/Android/" + _str;
        return _path;
    }

    //获得不同平台的路径
    string GetTerracePath(bool _isAndroid=false)
    {
        string sPath = Application.persistentDataPath;
        if(_isAndroid) sPath = "file://" + sPath;        
        return sPath;
    }
}

测试结果如下:

unity ios热更新 unity 热更新方案_unity ios热更新_08

真机测试结果如下:

unity ios热更新 unity 热更新方案_unity ios热更新_09

unity ios热更新 unity 热更新方案_android_10

unity ios热更新 unity 热更新方案_System_11

第四章  总结

  在实际测试过程中,AB包可以使用官方插件来处理,但需要注意的是导出的时候,shader要放入设置里,否则容易出现材质球丢失的问题。

unity ios热更新 unity 热更新方案_unity_12

  本篇博客的设计思路是,上传效验的文件,本地比对更新。测试用例中引申了图片、模型和txt文本的读取,但就实际代码热更的时候,需要考虑所用的lua框架;这部分暂时不放入热更之中。

  简而言之,本篇博客只提供上传和下载的方法。具体的使用,还需要根据项目实际划分。最后,使用链接已经上传到我的资源中。

第五章  WWW类已过时

  如果现在用新版本的朋友,应该会发现WWW类提示已过时的字样,原因是IOS9.0以上版本已经禁止了Http协议。部分安卓手机如三星高版本,在海外测试的时候也有类似问题。

  所以当Unity官方提示你更新的时候,别犟~按照官方要求去改就行。新的测试方法代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;
using UnityEngine.Networking;

/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
    private bool IS_ANDROID = false;

    private static Text fillAmountTxt;

    void Start()
    {
        transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
        fillAmountTxt = transform.Find("Text").GetComponent<Text>();
        TestPrint();
    }

    //输出测试
    void TestPrint()
    {
        Debug.Log("*******测试打印所有文件目录*******");
        var _str = "";
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            Debug.Log("替换过程:" + idx + "   " + GetTerracePath() + "   " + _s);
            _str += _s + "\n";
        }
        transform.Find("CeshiText").GetComponent<Text>().text = _str;
        Debug.Log("**************结束打印************");
    }

    //更新版本
    private void StartUpdate()
    {
        StartCoroutine(VersionUpdate());
    }

    private int allfilesLength = 0;
    /// <summary>
    /// 版本更新       
    /// </summary>
    /// <returns></returns>
    IEnumerator VersionUpdate()
    {
        var uri = GameProp.Inst.HttpDownLoadUrl("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
        var request = UnityWebRequest.Get(uri);
        yield return request.SendWebRequest();

        var _isGet = !(request.isNetworkError || request.isNetworkError);
        if (_isGet)
        {
            List<BundleInfo> bims = new List<BundleInfo>();
            FileMD5 date = JsonUtility.FromJson<FileMD5>(request.downloadHandler.text);

            DeleteOtherBundles(date);//删除所有不受版本控制的文件

            Debug.LogError(request.downloadHandler.text);

            //Debug.Log(data.Contains());
            var _list = date.files;

            string md5, file, path;
            int lenth;
            for (int i = 0; i < _list.Length; i++)
            {
                MD5Message _md5 = _list[i];

                Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
                file = _md5.file;

                path = PathUrl(file);

                md5 = GetMD5HashFromFile(path);

                if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
                {
                    bims.Add(new BundleInfo()
                    {
                        Url = HttpDownLoadUrl(file),
                        Path = path
                    });
                    lenth = int.Parse(_md5.fileLength);
                    allfilesLength += lenth;
                }
            }
            if (bims.Count > 0)
            {
                Debug.LogError("开始尝试更新");
                StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
                    OpenLodingShow("自动更新中...", progress, allfilesLength);
                }, (isfinish) => {
                    if (isfinish)
                        StartCoroutine(VersionUpdateFinish());
                    else
                    {
                        StartCoroutine(VersionUpdate());
                    }
                }));
            }
            else
            {
                StartCoroutine(VersionUpdateFinish());
            }
        }

    }

    // 删除所有不受版本控制的所有文件
    void DeleteOtherBundles(FileMD5 _md5)
    {
        Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
        string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
        foreach (string idx in bundleFiles)
        {
            var _r = @"\Android\";
            if (IS_ANDROID) _r = "/Android/";
            var _s = idx.Replace(GetTerracePath() + _r, "");
            _s = _s.Replace(@"\", "/");
            if (!FindNameInFileMD5(_md5, _s))
            {
                File.Delete(idx);
                Debug.LogError(_s + "不存在");
            }
        }
        Debug.Log("~~~~~~~结束删除~~~~~~~");
    }

    /// <summary>获取文件的md5校验码</summary>
    public string GetMD5HashFromFile(string fileName)
    {
        if (File.Exists(fileName))
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
                sb.Append(retVal[i].ToString("x2"));
            return sb.ToString();
        }
        return null;
    }

    static bool FindNameInFileMD5(FileMD5 date, string _name)
    {
        foreach (var _m in date.files)
        {
            if (_m.file == _name) return true;
        }
        return false;
    }

    //脚本替换(lua等)验证
    IEnumerator VersionUpdateFinish()
    {
        Debug.Log("lua验证");
        var uri = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt"; 
        var request = UnityWebRequest.Get(uri);
        yield return request.SendWebRequest();

        var _isGet = !(request.isNetworkError || request.isNetworkError);
        if (_isGet)
        {
            Debug.LogError(request.downloadHandler.text);
            transform.Find("CeshiText").GetComponent<Text>().text = request.downloadHandler.text;
        }
      
        StartCoroutine(VersionUpdateImage());
        StartCoroutine(VersionUpdateModel());
    }

    //图片验证
    IEnumerator VersionUpdateImage()
    {
        Debug.Log("图片替换验证");

        var uri = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
        var request = UnityWebRequest.Get(uri);
        yield return request.SendWebRequest();

        var _isGet = !(request.isNetworkError || request.isNetworkError);
        if (_isGet)
        {
            var _img = DownloadHandlerTexture.GetContent(request); ;           
            Sprite sprite = Sprite.Create(_img, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
            transform.Find("Button").GetComponent<Image>().sprite = sprite;
        }
    }

    //模型验证
    IEnumerator VersionUpdateModel()
    {
        var uri = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
        var request = UnityWebRequest.Get(uri);
        yield return request.SendWebRequest();

        var _isGet = !(request.isNetworkError || request.isNetworkError);
        if (_isGet)
        {
            AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
            GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
            GameObject modelClone = Instantiate(modelPre);
            AssetBundle.UnloadAllAssetBundles(false);
        }
        else
        {
            Debug.Log("error:" + request.url + "\n" + request.error);
        }
    }

    /// <summary>
    /// 路径比对
    /// </summary>  
    public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
    {
        //Debug.Log("开始路径对比");
        int num = 0;
        string dir;
        for (int i = 0; i < infos.Count; i++)
        {
            BundleInfo info = infos[i];
            Debug.LogError(info.Url);
            var uri = info.Url;
            var request = UnityWebRequest.Get(uri);
            yield return request.SendWebRequest();

            var _isGet = !(request.isNetworkError || request.isNetworkError);
            if (_isGet)
            {
                try
                {
                    string filepath = info.Path;
                    dir = Path.GetDirectoryName(filepath);
                    if (!Directory.Exists(dir))
                        Directory.CreateDirectory(dir);
                    File.WriteAllBytes(filepath, request.downloadHandler.data);
                    num++;
                    if (LoopCallBack != null)
                        LoopCallBack.Invoke((float)num / infos.Count);
                    Debug.Log(dir + "下载完成");
                }
                catch (Exception e)
                {
                    Debug.Log("下载失败" + e);
                }
            }
            else
            {
                Debug.Log("下载错误" + request.error);
            }
        }
        if (CallBack != null)
            CallBack.Invoke(num == infos.Count);
    }

    /// <summary>
    /// 记录信息
    /// </summary>
    public struct BundleInfo
    {
        public string Path { get; set; }
        public string Url { get; set; }
        public override bool Equals(object obj)
        {
            return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
        }
        public override int GetHashCode()
        {
            return Url.GetHashCode();
        }
    }

    /// <summary>
    /// loadpage展示
    /// </summary>
    /// <param name="text"></param>
    /// <param name="progress"></param>
    /// <param name="filealllength"></param>
    public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
    {
        Debug.LogError(text + "   " + progress + "   " + filealllength);
        fillAmountTxt.text = text + progress + "  文件序列:" + filealllength;
        if (progress >= 1) fillAmountTxt.text = "更新完成";
    }

    void Update()
    {

    }



    private void OnDestroy()
    {
        //if (DestoryLua != null)
        //    DestoryLua();
        //UpdateLua = null;
        //DestoryLua = null;
        //StartLua = null;
        // LuaEnvt.Dispose();
    }


    string HttpDownLoadUrl(string _str)
    {
        return "http://192.168.6.178/Chief/MD5Android/" + _str;
    }
    //根据不同路径,对下载路径进行封装
    string PathUrl(string _str)
    {
        var _path = GetTerracePath() + "/Android/" + _str;
        return _path;
    }

    //获得不同平台的路径
    string GetTerracePath(bool _isAndroid = false)
    {
        string sPath = Application.persistentDataPath;
        if (_isAndroid) sPath = "file://" + sPath;
        return sPath;
    }
}

  这里将所有WWW方法替换为了UnityWebRequest,另外API:DownloadHandlerAssetBundle.GetContent十分好用。

  不过由于项目中代码,现在已经与测试版本相差过大,所以这个测试版本没测试,可能有部分细微差别,请大家自行取用。