Unity Xlua热更新框架(四):热更新

7. 热更新

7-1. 热更新流程

热更新方案:

  • 整包:
    • 策略:完整更新资源放在包内
    • 优点:首次更新少
    • 缺点:安装包下载时间长,首次安装久
  • 分包:
    • 策略:包内放商量或者不放更新资源
    • 优点:安装包小,下载快,安装急速
    • 缺点:首次更新时间久

整包就是一开始安装既包含了热更新内容又包含了框架,,,分包就只包含了框架,,因此整包首次安装更新少,分包从商店下载快但是后续更新内容久,主要是下载热更新包,,说白了就是一个安装前包含在下载包里了,一个是安装后下载,,,,分包主要是google等海外商店限制安装包尺寸,,,所以海外用分包,国内用整包

image.png
如果是整包模式多了绿色框内的部分,,如果是初次安装,就素要把资源从Application.streamingAssets释放到Application.persistentDataPath
:::info
整包:为了减少逻辑复杂性,避免判断两个路径哪一个路径有所需要的文件,因此直接把资源从Application.streamingAssets释放到Application.persistentDataPath,所有资源的读取都在persistentDataPath去寻找,没有的资源从服务器下载。
:::
image.png
分包模式,直接从服务器下载热更资源,去可读写路径下读取文件
image.png
这个Application.persistentDataPath,是PathUtil里定义的BuildResourcesPath,就是Bundle文件和版本信息文件的路径。

public static string BundleResourcePath
{
    
    
    get
    {
    
    
        if (AppConst.GameMode == GameMode.UpdateMode)
            return Application.persistentDataPath;
        return Application.streamingAssetsPath;
    }
}

7-2. 热更新细节分析

image.png
整个流程就需要这三个流程,下载文件、写入文件、解析filelist
检测初次安装:

  1. 只读目录有热更新
  2. 可读写目录没有热更新资源
  3. 只需判断filelist文件是否存在即可

注意:最后写入filelist
:::info
整包模式下:初次安装,只读目录必然有filelist,如果可读写目录没有filelist需要释放,进行初次安装,filelist必须最后写入,,因为如果释放到一半,filelist写入了,下次再打开游戏没有释放完资源,有问题,,,最后写入即便中途退出,下次启动也会重新释放资源
:::
检查更新:

  1. 下载资源服务器的filelist文件
  2. 对比文件信息和本地是否一致

image.png

7-3. 热更新需要做哪些事

  1. 下载文件(HotUpdate)
  2. 写入文件(FileUtil)
  3. 解析filelist(HotUpdate)
//只读目录
public static readonly string ReadPath = Application.streamingAssetsPath;

//可读写目录
public static readonly string ReadWritePath = Application.persistentDataPath;
//热更新资源链接地址
public const string ResourcesUrl = "http://127.0.0.1/AssetBundles/";

创建热更新脚本。
句柄一般是指获取另一个对象的方法——一个广义的指针,它的具体形式可能是一个整数、一个对象或就是一个真实的指针,而它的目的就是建立起与被访问对象之间的唯一的联系 。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

public class HotUpdate : MonoBehaviour
{
    
    
    internal class DownFileInfo
    {
    
    
        public string url;
        //bundle名
        public string fileName;
        public DownloadHandler fileData;
    }
    //协程方法下载文件
    /// <summary>
    /// 下载单个文件
    /// </summary>
    /// <param name="url"></param>
    /// <param name="">返回一个下载句柄的Action</param>
    /// <returns></returns>
    IEnumerator DownLoadFile(DownFileInfo info, Action<DownFileInfo> Complete)
    {
    
    
        //老版本用WWW,现在已经弃用
        //新版需要使用UnityWebRequest,,,,,引入UnityEngine.Networking
        UnityWebRequest webRequest = UnityWebRequest.Get(info.url);
        //下载一个文件,等待,下载完继续执行
        yield return webRequest.SendWebRequest();

        //if(webRequest.isHttpError || webRequest.isNetworkError)
        if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError)
        {
    
    
            Debug.Log("下载文件出错:" + info.url);
            yield break;
            //下载失败,重试,有次数限制
        }

        //下载完成后,给info赋值
        info.fileData = webRequest.downloadHandler;
        //如果下载的是filelist,直接解析,用webRequest.downloadHandler.text.
        //如果是bundle,可以写入,用webRequest.downloadHandler.data
        Complete?.Invoke(info);
        //下载完成后释放掉。
        webRequest.Dispose();
    }

    //下载多个文件的接口,不可能再一个循环中下载1000个文件
    /// <summary>
    /// 下载多个文件
    /// </summary>
    /// <param name="infos">多个文件列表</param>
    /// <param name="Complete">下载一个文件完成的回调,然后写入</param>
    /// <param name="DownLoadAllComplete">所有文件下载完回调,通知用户释放资源、更新</param>
    /// <returns></returns>
    IEnumerator DownLoadFile(List<DownFileInfo> infos, Action<DownFileInfo> Complete, Action DownLoadAllComplete)
    {
    
    
        foreach (DownFileInfo info in infos)
        {
    
    
            //调用单个文件下载的协程
            yield return DownLoadFile(info, Complete);
        }
        DownLoadAllComplete?.Invoke();
    }

	/// <summary>
    /// 获取文件信息
    /// </summary>
    /// <param name="fileData"></param>
    /// <returns></returns>
    private List<DownFileInfo> GetFileList(string fileData, string path)
    {
    
    
        //对string规范化,因为有些符号,,win写入txt会有多余的符号
        string content = fileData.Trim().Replace("\r", "");
        string[] files = content.Split("\n");
        List<DownFileInfo> downFileInfos = new List<DownFileInfo>(files.Length);
        for (int i = 0; i < files.Length; i++)
        {
    
    
            //拿到文件信息
            string[] info = files[i].Split('|');
            DownFileInfo fileInfo = new DownFileInfo();
            fileInfo.fileName = info[1];
            //文件的下载到哪里的地址
            fileInfo.url = Path.Combine(path, info[1]);
            downFileInfos.Add(fileInfo);
        }
        return downFileInfos;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;

/// <summary>
/// 文件工具类:
/// 1查看指定文件是否存在
/// 2往指定目录写文件
/// </summary>
public class FileUtil
{
    
    
    //实用类都是用静态方法
    //检测文件是否存在
    public static bool IsExits(string path)
    {
    
    
        FileInfo file = new FileInfo(path);
        return file.Exists;
    }

    /// <summary>
    /// 写入文件
    /// </summary>
    /// <param name="path">路径</param>
    /// <param name="data">数据</param>
    public static void WriteFile(string path, byte[] data)
    {
    
    
        //获取标准路径
        path = PathUtil.GetStandardPath(path);
        //文件夹的路径
        string dir = path.Substring(0, path.LastIndexOf("/"));
        //判断文件夹存不存在,不存在就创建
        if(!Directory.Exists(dir))
        {
    
    
            Directory.CreateDirectory(dir);
        }
        FileInfo file = new FileInfo(path);
        //并非win的覆盖写入,必须删除再重新创建文件,否则会报错。
        if(file.Exists)
        {
    
    
            file.Delete();
        }
        try
        {
    
    
            //创建文件流,FileMode.Create  如果文件不存在,则重新创建,否则覆盖它,FileAccess.Write写入的方式
            using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
    
    
                fs.Write(data, 0, data.Length);
                //写完文件关闭文件流
                fs.Close();
            }
        }
        catch(IOException e)
        {
    
    
            Debug.Log(e.Message);
        }
    }
}

7-4 热更流程

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

/// <summary>
/// 热更新类:
/// 定义了下载信息DownFileInfo,和用来保存只读目录filelist和服务器filelist的变量
/// 通用UnityWebRequest,既可以从本地下载(释放)文件也可以从服务器下载文件,用DownLoadFile实现了单个或多个文件(封装了单个文件下载)下载
/// GetFileList可以从filelist获取需要更新的文件信息
/// 流程:
/// IsFirstInstall如果是初次安装,就从可读目录释放资源ReleaseResources到可读写目录(然后热更新CheckUpdate),如果可读目录没有资源就从服务器下载资源CheckUpdate到可读写目录。释放与下载逻辑一致
/// ReleaseResources释放可读目录filelist,释放完OnReleaseReadPathFileListComplete下载所有包,回调单个文件下载好OnReleaseFileComplete写入可读写目录,所有文件下载完OnReleaseAllFileComplete把可读目录filelist写入可读写目录,CheckUpdate
/// CheckUpdate下载服务器filelist,下载完OnDownLoadServerFileListComplete下载所有包,回调单个文件下载好OnUpdateFileComplete写入可读写目录,所有文件下载完OnUpdateAllFileComplete把服务器filelist写入可读写目录,进入游戏
/// </summary>
public class HotUpdate : MonoBehaviour
{
    
    
    byte[] m_ReadPathFileListData;
    byte[] m_ServerFileListData;

    internal class DownFileInfo
    {
    
    
        public string url;
        //bundle名
        public string fileName;
        public DownloadHandler fileData;
    }
    //协程方法下载文件
    /// <summary>
    /// 下载单个文件
    /// </summary>
    /// <param name="url"></param>
    /// <param name="">返回一个下载句柄的Action</param>
    /// <returns></returns>
    IEnumerator DownLoadFile(DownFileInfo info, Action<DownFileInfo> Complete)
    {
    
    
        //老版本用WWW,现在已经弃用
        //新版需要使用UnityWebRequest,,,,,引入UnityEngine.Networking
        UnityWebRequest webRequest = UnityWebRequest.Get(info.url);
        //下载一个文件,等待,下载完继续执行
        yield return webRequest.SendWebRequest();

        //if(webRequest.isHttpError || webRequest.isNetworkError)
        if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError)
        {
    
    
            Debug.Log("下载文件出错:" + info.url);
            yield break;
            //下载失败,重试,有次数限制
        }

        //下载完成后,给info赋值
        info.fileData = webRequest.downloadHandler;
        //如果下载的是filelist,直接解析,用webRequest.downloadHandler.text.
        //如果是bundle,可以写入,用webRequest.downloadHandler.data
        Complete?.Invoke(info);
        //下载完成后释放掉。
        webRequest.Dispose();
    }

    //下载多个文件的接口,不可能再一个循环中下载1000个文件
    /// <summary>
    /// 下载多个文件
    /// </summary>
    /// <param name="infos">多个文件列表</param>
    /// <param name="Complete">下载一个文件完成的回调,然后写入</param>
    /// <param name="DownLoadAllComplete">所有文件下载完回调,通知用户释放资源、更新</param>
    /// <returns></returns>
    IEnumerator DownLoadFile(List<DownFileInfo> infos, Action<DownFileInfo> Complete, Action DownLoadAllComplete)
    {
    
    
        foreach (DownFileInfo info in infos)
        {
    
    
            //调用单个文件下载的协程
            yield return DownLoadFile(info, Complete);
        }
        DownLoadAllComplete?.Invoke();
    }

    /// <summary>
    /// 获取文件信息
    /// </summary>
    /// <param name="fileData"></param>
    /// <returns></returns>
    private List<DownFileInfo> GetFileList(string fileData, string path)
    {
    
    
        //对string规范化,因为有些符号,,win写入txt会有多余的符号
        string content = fileData.Trim().Replace("\r", "");
        string[] files = content.Split("\n");
        List<DownFileInfo> downFileInfos = new List<DownFileInfo>(files.Length);
        for (int i = 0; i < files.Length; i++)
        {
    
    
            //拿到文件信息
            string[] info = files[i].Split('|');
            DownFileInfo fileInfo = new DownFileInfo();
            fileInfo.fileName = info[1];
            //文件的下载到哪里的地址
            fileInfo.url = Path.Combine(path, info[1]);
            downFileInfos.Add(fileInfo);
        }
        return downFileInfos;
    }

    private void Start()
    {
    
            
        if(IsFirstInstall())
        {
    
    
            //如果是初次安装,先按照可读目录的filelist释放资源到可读写目录,再更新
            ReleaseResources();
        }
        else
        {
    
    
            CheckUpdate();
        }
    }       
    /// <summary>
    /// 是否初次安装
    /// </summary>
    /// <returns></returns>
    private bool IsFirstInstall()
    {
    
    
        //判断只读目录是否存在版本文件
        bool isExistsReadPath = FileUtil.IsExits(Path.Combine(PathUtil.ReadPath, AppConst.FileListName));

        //判断可读写目录是否存在版本文件
        bool isExistsReadWritePath = FileUtil.IsExits(Path.Combine(PathUtil.ReadWritePath, AppConst.FileListName));

        return isExistsReadPath && !isExistsReadWritePath;
    }
   
    /// <summary>
    /// 根据可读目录的filelist释放资源
    /// </summary>
    private void ReleaseResources()
    {
    
    
        string url = Path.Combine(PathUtil.ReadPath, AppConst.FileListName);
        DownFileInfo info = new DownFileInfo();
        info.url = url;
        //UnityWebRequest可以从本地下载
        //先读取filelist里需要释放的文件
        StartCoroutine(DownLoadFile(info, OnDownLoadReadPathFileListComplete));
    }

    /// <summary>
    /// 释放完filelist的回调,用于释放filelist里的所有文件
    /// </summary>
    /// <param name="file"></param>
    private void OnDownLoadReadPathFileListComplete(DownFileInfo file)
    {
    
    
        //从只读目录加载完filelist后,保存filelist的内容
        m_ReadPathFileListData = file.fileData.data;
        //获取到filelist里的所有要释放的文件的文件信息
        List<DownFileInfo> fileInfos = GetFileList(file.fileData.text, PathUtil.ReadPath);
        StartCoroutine(DownLoadFile(fileInfos, OnReleaseFileComplete, OnReleaseAllFileComplete));
    }

    /// <summary>
    /// 释放到可读写目录一个文件
    /// </summary>
    /// <param name="fileinfo"></param>
    private void OnReleaseFileComplete(DownFileInfo fileinfo)
    {
    
    
        //可读写目录加bundle目录
        string writeFile = Path.Combine(PathUtil.ReadWritePath, fileinfo.fileName);
        FileUtil.WriteFile(writeFile, fileinfo.fileData.data);
    }

    /// <summary>
    /// 所有文件释放到可读写目录完成后,写入filelist
    /// </summary>
    private void OnReleaseAllFileComplete()
    {
    
    
        //所有文件都释放完成后,再把filelist写入可读写目录
        FileUtil.WriteFile(Path.Combine(PathUtil.ReadWritePath, AppConst.FileListName), m_ReadPathFileListData);
        //释放完成后,检查更新
        CheckUpdate();
    }

    /// <summary>
    /// 检查更新
    /// </summary>
    private void CheckUpdate()
    {
    
    
        //获取filelist再资源服务器上的地址
        string url = Path.Combine(AppConst.ResourcesUrl, AppConst.FileListName);
        DownFileInfo info = new DownFileInfo();
        info.url = url;
        StartCoroutine(DownLoadFile(info, OnDownLoadServerFileListComplete));
    }

    private void OnDownLoadServerFileListComplete(DownFileInfo file)
    {
    
    
        //保存最新的filelist
        m_ServerFileListData = file.fileData.data;
        //获取资源服务器的文件信息目录
        List<DownFileInfo> fileInfos = GetFileList(file.fileData.text, AppConst.ResourcesUrl);
        //定义需要下载的文件集合
        List<DownFileInfo> downListFiles = new List<DownFileInfo>();

        //遍历资源服务器的文件信息
        for (int i = 0; i < fileInfos.Count; i++)
        {
    
    
            string localFile = Path.Combine(PathUtil.ReadWritePath, fileInfos[i].fileName);
            //判断本地是否存在,如果不存在就下载
            if(!FileUtil.IsExits(localFile))
            {
    
    
                fileInfos[i].url = Path.Combine(AppConst.ResourcesUrl, fileInfos[i].fileName);
                downListFiles.Add(fileInfos[i]);
            }
        }
        if (downListFiles.Count > 0)
        {
    
    
            StartCoroutine(DownLoadFile(fileInfos, OnUpdateFileComplete, OnUpdateAllFileComplete));
        }
        else
        {
    
    
            EnterGame();
        }
    }

    private void OnUpdateFileComplete(DownFileInfo file)
    {
    
    
        //下载新文件
        string writeFile = Path.Combine(PathUtil.ReadWritePath, file.fileName);
        FileUtil.WriteFile(writeFile, file.fileData.data);
    }

    private void OnUpdateAllFileComplete()
    {
    
    
        //所有文件下载完,写入最新的filelist
        FileUtil.WriteFile(Path.Combine(PathUtil.ReadWritePath, AppConst.FileListName), m_ServerFileListData);
        EnterGame();
    }

    private void EnterGame()
    {
    
    
        throw new NotImplementedException();
    }
}

7-5. 热更新测试

public static string BundleResourcePath
    {
    
    
        get
        {
    
    
			//直接使用定义好的路径,避免使用Application,减少GC
            if (AppConst.GameMode == GameMode.UpdateMode)
                return ReadWritePath;
            return ReadPath;
        }
    }

注释掉ResourceManager中的Start函数和OnComplete函数,在HotUpdate中实现

private void EnterGame()
{
    
    
    Manager.Resource.ParseVersionFile();
    Manager.Resource.LoadUI("Login/LoginUI", OnComplete);       
}

private void OnComplete(UnityEngine.Object obj)
{
    
    
    GameObject go = Instantiate(obj) as GameObject;
    go.transform.SetParent(this.transform);
    go.SetActive(true);
    go.transform.localPosition = Vector3.zero;
}

root下创建Manager挂载Manager脚本,,,,删除之前物体挂载的test等脚本。
Canvas挂载hotUpdate脚本

public class GameStart : MonoBehaviour
{
    
    
    public GameMode GameMode;
    // Start is called before the first frame update
    void Awake()
    {
    
    
        AppConst.GameMode = this.GameMode;
        DontDestroyOnLoad(this);
    }
}

还需要用到一个工具,在网上进行下载。NetBox2,用来模拟本地服务器
把StreamingAssets文件下打包好的文件全部复制到NetBox目录的AssetBundles文件夹下面,删除多余的meta等文件
测试释放,首先要保证可读写目录是空的。C:\Users\zhe1123\AppData\LocalLow\DefaultCompany\XLuaFramework

猜你喜欢

转载自blog.csdn.net/weixin_42264818/article/details/128211160
今日推荐