7. 热更新
7-1. 热更新流程
热更新方案:
- 整包:
- 策略:完整更新资源放在包内
- 优点:首次更新少
- 缺点:安装包下载时间长,首次安装久
- 分包:
- 策略:包内放商量或者不放更新资源
- 优点:安装包小,下载快,安装急速
- 缺点:首次更新时间久
整包就是一开始安装既包含了热更新内容又包含了框架,,,分包就只包含了框架,,因此整包首次安装更新少,分包从商店下载快但是后续更新内容久,主要是下载热更新包,,说白了就是一个安装前包含在下载包里了,一个是安装后下载,,,,分包主要是google等海外商店限制安装包尺寸,,,所以海外用分包,国内用整包
如果是整包模式多了绿色框内的部分,,如果是初次安装,就素要把资源从Application.streamingAssets释放到Application.persistentDataPath
:::info
整包:为了减少逻辑复杂性,避免判断两个路径哪一个路径有所需要的文件,因此直接把资源从Application.streamingAssets释放到Application.persistentDataPath,所有资源的读取都在persistentDataPath去寻找,没有的资源从服务器下载。
:::
分包模式,直接从服务器下载热更资源,去可读写路径下读取文件
这个Application.persistentDataPath,是PathUtil里定义的BuildResourcesPath,就是Bundle文件和版本信息文件的路径。
public static string BundleResourcePath
{
get
{
if (AppConst.GameMode == GameMode.UpdateMode)
return Application.persistentDataPath;
return Application.streamingAssetsPath;
}
}
7-2. 热更新细节分析
整个流程就需要这三个流程,下载文件、写入文件、解析filelist
检测初次安装:
- 只读目录有热更新
- 可读写目录没有热更新资源
- 只需判断filelist文件是否存在即可
注意:最后写入filelist
:::info
整包模式下:初次安装,只读目录必然有filelist,如果可读写目录没有filelist需要释放,进行初次安装,filelist必须最后写入,,因为如果释放到一半,filelist写入了,下次再打开游戏没有释放完资源,有问题,,,最后写入即便中途退出,下次启动也会重新释放资源
:::
检查更新:
- 下载资源服务器的filelist文件
- 对比文件信息和本地是否一致
7-3. 热更新需要做哪些事
- 下载文件(HotUpdate)
- 写入文件(FileUtil)
- 解析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