Unity热更新之AB包上传下载
一、概述
1、前置知识点
- AssetBundle(AB包):Unity热更新之AssetBundle
- C#文件读写:C#之文件操作
- C#异步函数:C#学习笔记——各版本新功能语法(伍)
- FTP文件传输:Unity网络开发基础——FTP(叁)
- UnityWebRequest:Unity网络开发基础——HTTP(肆)
等等…
2、关于本文章
本文章是唐老狮的AB包课程的学习笔记,经过个人的简单修改和梳理而成,主要内容如下:
- 上传
- MD5码。
- AB包对比文件生成。
- AB包上传到资源服务器。
- 最终会制作一个AB包上传工具。
- 下载
- 从资源服务器下载文件。
- 解析本地和远端的对比文件,然后进行比对。
- 根据比对结果,下载、更新或删除。
- 最终会制作一个客户端的AB包下载管理器。
课程主要讲解的是AB包上传下载以及热更新这一套流程,课程的代码可能不是特别完美,还是有一些缺陷在的,但是我们学习的目的就是要理解这个制作的核心思路,具体优化部分可以由我们自己完成。
二、上传
(一)MD5码
1、MD5码概念
MD5(Message-Digest Algorithm 5)即信息摘要算法第五版的简称,它是一种广泛使用的密码散列函数,可以生成出一个128位(16个字节)的散列值,用于确保信息的完整一致性。
将数据经过MD5算法计算过后,无论传入的数据有多大,都会生成一个固定长度(16字节)的信息摘要值。相同的数据,每次经过MD5算法计算后的结果都会是一样的,如果数据变化,MD5码将会发生变化。因此我们可以利用MD5码作为文件的唯一标识,判断文件内容是否变化。
2、获取MD5码
获取MD5码需要用到MD5这个类,调用如下两个API:
MD5 md5 = MD5.Create(); // 创建MD5实例
md5.ComputeHash(Stream); // 获取指定Stream的哈希值,返回字节数组
由于MD5返回结果是byte[],我们需要将其转成字符串,可以封装如下方法:
private string GetMD5(string filePath)
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open))
{
// 获取MD5码字节数组
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(fileStream);
// 将MD5码转成十六进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
stringBuilder.Append(hash[i].ToString("x2")); // 十六进制小写
}
return stringBuilder.ToString();
}
}
(二)AB包上传
1、搭建FTP服务器
在FTP文件传输那篇文章里讲过了,如果是本地的话,可以用Serv-U这类软件进行FTP服务器的搭建。或者也可以自己搭建FTP服务器,当然这里还是推荐直接用别人做好的第三方软件进行搭建。
需要创建两个账户:
- 用于上传FTP的管理员账号
- 用于下载FTP的匿名账号(只有读权限)
2、生成资源对比文件
现在让我们制作一个工具,用于生成AB包资源对比文件。
对比文件中,每个AB包之间都用 | 分隔,对于每个AB包,需要记录AB包名,文件大小,MD5码。
在Editor文件夹中,创建如下脚本,对比文件的工具代码如下:
/// <summary>
/// AB包资源对比文件工具
/// </summary>
public class AssetBundleCompareTool
{
// AB包路径
private static string ASSET_BUNDLE_PATH = Application.dataPath + "/Arts/AssetBundle/PC/";
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
[MenuItem("AB包工具/创建对比文件")]
public static void CreateAssetBundleCompareFile()
{
// 获取所有AB包文件
DirectoryInfo directoryInfo = Directory.CreateDirectory(ASSET_BUNDLE_PATH);
FileInfo[] fileInfos = directoryInfo.GetFiles();
// 获取并拼接AB包的相关信息
StringBuilder abCompareInfo = new StringBuilder();
foreach (FileInfo fileInfo in fileInfos)
{
// 该文件夹下,没有后缀的文件是AB包
if (fileInfo.Extension == "")
{
abCompareInfo.Append($"{fileInfo.Name} {fileInfo.Length} {GetMD5(fileInfo.FullName)}|");
}
}
if (abCompareInfo.Length > 0)
{
abCompareInfo.Remove(abCompareInfo.Length - 1, 1); // 去掉最后一个"|"
}
// 将信息存储为对比文件
File.WriteAllText(ASSET_BUNDLE_PATH + COMPARE_INFO_FILE_NAME, abCompareInfo.ToString());
AssetDatabase.Refresh();
Debug.Log("AB包对比文件生成成功");
}
/// <summary>
/// 获取文件MD5码
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>MD5码</returns>
public static string GetMD5(string filePath)
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open))
{
// 获取MD5码字节数组
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(fileStream);
// 将MD5码转成十六进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
stringBuilder.Append(hash[i].ToString("x2")); // 十六进制小写
}
return stringBuilder.ToString();
}
}
}
3、上传AB包
现在我们继续制作一个工具,用于将AB包及其比对文件上传到FTP服务器。
在Editor文件夹中,创建如下脚本,上传AB包的工具代码如下:
/// <summary>
/// AB包上传工具
/// </summary>
public class AssetBundleUploadTool
{
// AB包路径
private static string ASSET_BUNDLE_PATH = Application.dataPath + "/Arts/AssetBundle/PC/";
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
// FTP相关
private static string FTP_PATH = "ftp://127.0.0.1/AssetBundle/PC/";
private static string USER_NAME = "pmm";
private static string PASSWORD = "123456";
[MenuItem("AB包工具/上传AB包")]
public static void UploadAssetBundle()
{
DirectoryInfo directoryInfo = Directory.CreateDirectory(ASSET_BUNDLE_PATH);
FileInfo[] fileInfos = directoryInfo.GetFiles();
foreach (FileInfo fileInfo in fileInfos)
{
// 将比对文件和AB包进行上传
if (fileInfo.Extension == "" || fileInfo.Name == COMPARE_INFO_FILE_NAME)
{
FtpUploadFile(fileInfo.FullName, fileInfo.Name);
}
}
}
private async static void FtpUploadFile(string filePath, string fileName)
{
await Task.Run(() =>
{
try
{
// 创建FTP请求和相关设置
FtpWebRequest request = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
request.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
request.Proxy = null;
request.KeepAlive = false;
request.Method = WebRequestMethods.Ftp.UploadFile;
request.UseBinary = true;
// 上传文件
using (Stream uploadStream = request.GetRequestStream())
{
using (FileStream fileStream = File.OpenRead(filePath))
{
// 读取本地要上传的文件
byte[] bytes = new byte[2048];
// 写入到目标文件
int length;
while ((length = fileStream.Read(bytes, 0, bytes.Length)) > 0)
{
uploadStream.Write(bytes, 0, length);
}
}
}
// 接收响应
using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
{
Debug.Log($"上传完成 [{response.StatusCode}]:{fileName}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"{fileName} 上传失败:{ex.Message}");
}
});
}
}
三、下载
接下来实现AB包的下载部分。
这里将实现一个AB包管理器,用于AB包的下载,当然这里并没有实现资源比对的更新,目前只是单纯的下载,更新将在后面章节进行讲解(即管理器完整部分在"更新"章节会讲解)。这里主要实现了这几个功能:
- DownloadCompareInfoFile:下载AB包资源对比文件。
- LoadRemoteCompareInfo:用于解析下载的资源对比文件。
- DownloadAssetBundleFile:下载所有AB包文件(这里暂时没有比对,目前是全部AB包都进行下载)。
- DownloadFile:用于从FTP服务器下载文件的辅助方法。
/// <summary>
/// AB包管理器
/// </summary>
public class AssetBundleManager : MonoBehaviour
{
public static AssetBundleManager Instance { get; private set; }
private void Awake()
{
Instance = this;
}
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
// FTP相关
private static string FTP_PATH = "ftp://127.0.0.1/AssetBundle/PC/";
private static string USER_NAME = "pmm";
private static string PASSWORD = "123456";
/// <summary>
/// AB包信息类
/// </summary>
private class AssetBundleInfo
{
public string name; // 包名
public long size; // 大小
public string md5; // md5码
public AssetBundleInfo(string name, long size, string md5)
{
this.name = name;
this.size = size;
this.md5 = md5;
}
}
// 存储远端下载的AB包信息
private Dictionary<string, AssetBundleInfo> remoteAssetBundleInfos = new Dictionary<string, AssetBundleInfo>();
// 待下载AB包列表
private List<string> downloadList = new List<string>();
/// <summary>
/// 下载对比文件
/// </summary>
/// <param name="completeCallback">下载完成的回调,若下载成功则为true,否则为false</param>
public async void DownloadCompareInfoFile(Action<bool> completeCallback = null)
{
// 一些变量
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME; // 下载路径
bool isDone = false; // 是否下载成功
int retryCount = 5; // 重新下载次数
// 下载对比文件
while (!isDone && retryCount > 0)
{
await Task.Run(() =>
{
isDone = DownloadFile(COMPARE_INFO_FILE_NAME, downloadPath);
});
retryCount--;
}
completeCallback?.Invoke(isDone);
}
/// <summary>
/// 解析远端对比文件
/// </summary>
public void LoadRemoteCompareInfo()
{
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME; // 文件路径
string compareInfo = File.ReadAllText(downloadPath);
string[] assetBundleInfos = compareInfo.Split("|");
// 解析每个AB包
foreach (string assetBundleInfo in assetBundleInfos)
{
string[] infos = assetBundleInfo.Split(" ");
// 记录AB包信息,方便进行对比
remoteAssetBundleInfos.Add(infos[0], new AssetBundleInfo(infos[0], long.Parse(infos[1]), infos[2]));
}
}
/// <summary>
/// 下载AB包
/// </summary>
/// <param name="completeCallback">下载完成的回调,若全部下载成功则为true,否则为false</param>
/// <param name="progressCallback">下载进度回调,每次下载AB包完成都会调用,参数分别为当前下载数和最大下载数</param>
public async void DownloadAssetBundleFile(Action<bool> completeCallback = null, Action<int,int> progressCallback = null)
{
// 将需要下载的AB包添加进待下载列表
foreach (string fileName in remoteAssetBundleInfos.Keys)
{
downloadList.Add(fileName); // 在这之前可以进行比对,这里暂时先全部下载
}
// 一些变量
string downloadPath = Application.persistentDataPath + "/"; // 下载路径
bool isDone = false; // 是否下载成功
List<string> removeList = new List<string>(); // 记录下载成功的AB包,需要被移除
int retryCount = 5; // 重新下载次数
int downloadCount = 0; // 已下载的AB包数量
int maxDownloadCount = downloadList.Count; // 需要下载的AB包数量
// 下载文件,下载失败后会重新下载,直至下载完毕 或者 超过最大重新下载次数
while (downloadList.Count > 0 && retryCount > 0)
{
for (int i = 0; i < downloadList.Count; i++)
{
// 下载
isDone = false;
string fileName = downloadList[i];
await Task.Run(() =>
{
isDone = DownloadFile(fileName, downloadPath + fileName);
});
// 下载成功则添加进待移除列表
if (isDone)
{
progressCallback?.Invoke(++downloadCount, maxDownloadCount);
removeList.Add(fileName);
}
}
// 移除下载成功的AB包
for (int i = 0; i < removeList.Count; i++)
{
downloadList.Remove(removeList[i]);
}
retryCount--;
}
// 调用回调,全部下载成功则返回true,否则为false
completeCallback?.Invoke(downloadList.Count == 0);
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="savePath">下载保存的路径</param>
private bool DownloadFile(string fileName, string savePath)
{
try
{
// 创建FTP请求和相关设置
FtpWebRequest request = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
request.Credentials = new NetworkCredential(USER_NAME, PASSWORD); // 有匿名账号可以不设置凭证,这里为了安全还是设置了凭证
request.Proxy = null;
request.KeepAlive = false;
request.Method = WebRequestMethods.Ftp.DownloadFile;
request.UseBinary = true;
// 获取响应
using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
{
// 获取下载流
using (Stream downloadStream = response.GetResponseStream())
{
// 将资源服务器的文件写入保存路径
using (FileStream fileStream = File.Create(savePath))
{
// 读取本地要上传的文件
byte[] bytes = new byte[2048];
// 写入到目标文件
int length;
while ((length = downloadStream.Read(bytes, 0, bytes.Length)) > 0)
{
fileStream.Write(bytes, 0, length);
}
}
}
Debug.Log($"下载完成 [{response.StatusCode}]:{fileName}");
return true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"{fileName} 下载失败:{ex.Message}");
return false;
}
}
private void OnDestroy()
{
Instance = null;
}
}
使用如下:
// 先下载资源对比文件
AssetBundleManager.Instance.DownloadCompareInfoFile((isDone) =>
{
if (isDone)
{
// 加载对比文件
AssetBundleManager.Instance.LoadRemoteCompareInfo();
// 再下载AB包
AssetBundleManager.Instance.DownloadAssetBundleFile((isDone) =>
{
if (isDone)
Debug.Log("AB包下载成功");
else
Debug.Log("AB包下载失败");
}, (current, max) => {
Debug.Log($"下载进度:{current}/{max}");
});
}
else
{
Debug.Log("对比文件下载失败");
}
});
四、更新
(一)设置默认资源
有时我们需要给游戏设置一些默认资源,因此这里制作一个工具,用来移动需要设置为默认资源的AB包到StreamingAssets文件夹当中,同时生成资源对比文件,作为游戏最初的AB包资源。
这里工作方式是,鼠标选中想要移动的AB包(Ctrl或者Shift + 鼠标点击),然后点击工具菜单栏按钮即可移动并生成对比文件。工具编写代码如下:
/// <summary>
/// AB包移动工具
/// </summary>
public class AssetBundleMoveTool
{
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
[MenuItem("AB包工具/移动选中的AB包资源到StreamingAssets文件夹")]
private static void MoveAssetBundleToStreamingAssets()
{
// 获取选中的资源
UnityEngine.Object[] selectedAssets = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.DeepAssets);
if (selectedAssets.Length == 0)
return;
// 处理每个选中的资源,拷贝到StreamingAssets文件夹,同时生成资源对比文件
StringBuilder abCompareInfo = new StringBuilder();
foreach (var asset in selectedAssets)
{
// 获取文件名
string assetPath = AssetDatabase.GetAssetPath(asset);
string fileName = assetPath.Substring(assetPath.LastIndexOf('/') + 1);
// 获取文件信息
FileInfo fileInfo = new FileInfo(Application.streamingAssetsPath + "/" + fileName);
if (fileInfo.Extension != "")
continue; // 排除非AB包文件
// 拷贝文件
AssetDatabase.CopyAsset(assetPath, "Assets/StreamingAssets/" + fileName);
// 将AB包信息添加进比对文件中
abCompareInfo.Append($"{fileInfo.Name} {fileInfo.Length} {AssetBundleCompareTool.GetMD5(fileInfo.FullName)}|");
}
if (abCompareInfo.Length > 0)
{
abCompareInfo.Remove(abCompareInfo.Length - 1, 1); // 去掉最后一个"|"
}
// 往StreamingAssets文件夹写入资源对比文件
File.WriteAllText(Application.streamingAssetsPath + "/" + COMPARE_INFO_FILE_NAME, abCompareInfo.ToString());
AssetDatabase.Refresh();
}
}
(二)默认资源的转存问题
1、AB包默认资源转存问题及方案
AB 包存放路径有两种选择
streamAssetsPath:流资源目录,只读路径,编辑器中streamAssetsPath下的文件可随 Unity 打包一起被打包出去。persistentDataPath:持久化目录,可读可写路径,编辑器状态下的persisentDataPath内的文件不会被打包出去。
而我们的AB包默认资源会先放在流资源目录中,热更新过后,资源都是下载到持久化目录中的,因此我们可以先将流资源目录的所有AB包转存到持久化目录中。当然,具体怎么做取决于项目中AB包加载机制,这里有两种方式:
- 若决定都从
persistentDataPath加载,则需要先从streamingAssetsPath转存,这个缺点就是若默认资源较多,则会产生两份数据,浪费磁盘空间。 - 若不想转存,则可以这样:先检查
persistentDataPath是否有资源,若没有再从streamingAssetsPath中加载。
这里我们采用第二种方式。
2、AB包下载更新流程图
下图展示了游戏客户端热更新从下载到更新文件的流程:

(三)资源更新
1、概述
之前实现的资源对比文件下载时,并没有执行对比操作就直接覆盖了,因此需要进一步实现本地资源与远端资源的对比。由于我们不能下载后就直接覆盖本地文件,因此好的做法是先比对本地与远端的资源对比文件,然后根据两者的不同去更新AB包,确认AB包全部下载完成了,最后才覆盖。
因此,比对资源时一定会在内存或者硬盘上存储两份资源对比数据, 一份远程的一份本地的。这里有两种做法:
- 保存到临时文件中,待AB包下载完成后,再用该临时文件覆盖本地对比文件
- 不保存文件,直接通过下载流读取出字节数组数据,转存为字符串,待AB包下载完毕后再保存为本地资源对比文件。
这里我们采用第一种方式。
2、代码实现
(1)将远端对比文件下载为临时文件
我们对原先下载章节的代码进行修改,首先下载远端资源对比文件时,我们不是直接覆盖到本地,而是先保存为临时文件。先声明一个变量,作为临时文件名:
// 远端下载的临时对比文件名
private static string COMPARE_INFO_TEMP_FILE_NAME = "compareInfo_temp.txt";
然后修改两个远端对比文件的方法DownloadCompareInfoFile和LoadRemoteCompareInfo,将文件路径替换成临时文件路径,以DownloadCompareInfoFile方法为例,另一个同理:
从这个:
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME;
替换成这个:
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_TEMP_FILE_NAME;
这一步是为了每次下载文件和加载对比文件时,都从这个临时文件去操作。
(2)获取本地对比文件信息
远端资源对比文件已经可以下载并解析,同样,我们还需要添加一个解析本地对比文件的函数,其内部和LoadRemoteCompareInfo类似。不同的是,这里读取文件需要用UnityWebRequest,这是因为StreamingAssets在安卓等平台上是只读的,无法直接访问(持久化目录也用了是为了保持统一)。
仿照远端对比文件的加载,本地对比文件加载代码如下:
// 存储本地的AB包信息
private Dictionary<string, AssetBundleInfo> localAssetBundleInfos = new Dictionary<string, AssetBundleInfo>();
/// <summary>
/// 解析本地对比文件
/// </summary>
/// <param name="completeCallback">解析本地对比文件完成的回调,若成功则为true,否则为false</param>
public void LoadLocalCompareInfo(Action<bool> completeCallback = null)
{
string persistentPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME;
string streamingPath = Application.streamingAssetsPath + "/" + COMPARE_INFO_FILE_NAME;
// 先检查持久化目录是否存在本地对比文件
if (File.Exists(persistentPath))
{
StartCoroutine(LoadLocalCompareInfoCoroutine(persistentPath, completeCallback));
}
// 再检查流资源目录是否存在本地对比文件
else if (File.Exists(streamingPath))
{
StartCoroutine(LoadLocalCompareInfoCoroutine(streamingPath, completeCallback));
}
// 若两个 if 语句都没有进入,则代表本地没有默认资源
}
// 加载本地对比文件协程
private IEnumerator LoadLocalCompareInfoCoroutine(string filePath, Action<bool> completeCallback = null)
{
// 使用 UnityWebRequest 读取本地文件
UnityWebRequest request = UnityWebRequest.Get(filePath);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// 解析本地对比文件信息
string compareInfo = request.downloadHandler.text;
string[] assetBundleInfos = compareInfo.Split("|");
// 解析每个AB包
foreach (string assetBundleInfo in assetBundleInfos)
{
string[] infos = assetBundleInfo.Split(" ");
// 记录AB包信息,方便进行对比
localAssetBundleInfos.Add(infos[0], new AssetBundleInfo(infos[0], long.Parse(infos[1]), infos[2]));
}
completeCallback?.Invoke(true);
}
else
{
completeCallback?.Invoke(false);
}
}
(3)AB包的更新
目前,我们已经完成了远端对比文件的下载和解析,本地对比文件的解析,并将两者的AB包信息都记录到字典当中,是时候将两者进行比对,然后更新对应AB包了。这里我们无非会遇到这几种情况:
- 更新:远端和本地都有这个AB包,但是两者MD5码不同,则下载并更新本地的AB包。
- 下载:远端有但本地没有的AB包,进行下载。
- 删除:远端没有但本地有的AB包,进行删除。
- 不变:远端和本地都有这个AB包,且两者MD5码相同,则无需变动。
我们开始吧。
首先,我们在“下载”章节中,编写了下载AB包的方法DownloadAssetBundleFile,我们当时是把所有AB包都添加进待列表中,而现在,我们在此之前需要进行比对,所以可以把下面这段代码删掉,让这个方法只需负责下载待下载列表的文件即可,只需待下载列表的添加由外部进行修改。
需要删除的代码块位置如下:
public async void DownloadAssetBundleFile(Action<bool> completeCallback = null, Action<int,int> progressCallback = null)
{
// 删除,这步由其他模块负责
foreach (string fileName in remoteAssetBundleInfos.Keys)
{
downloadList.Add(fileName);
}
// 其他不变...
}
之后,我们编写一个方法,用于比对本地和远端的资源,进行更新、下载和删除,将需要下载的AB包添加进待下载列表中,代码如下:
/// <summary>
/// 更新待下载的AB包列表
/// </summary>
public void UpdateDownloadList()
{
// 资源比对
foreach (string abName in remoteAssetBundleInfos.Keys)
{
// 下载:远端有而本地没有
if (!localAssetBundleInfos.ContainsKey(abName))
{
downloadList.Add(abName);
}
else
{
// 更新:本地和远端都有,但MD5码不同
if (localAssetBundleInfos[abName].md5 != remoteAssetBundleInfos[abName].md5)
{
downloadList.Add(abName);
}
// 每次操作完对本地同名AB包记录进行删除,最后剩余的就是需要被删除的AB包
localAssetBundleInfos.Remove(abName);
}
}
// 删除:删除本地多余的AB包(只删除持久化目录里的,因为默认资源是只读的)
foreach (string abName in localAssetBundleInfos.Keys)
{
if (File.Exists(Application.persistentDataPath + "/" + abName))
{
Debug.Log($"本地多余AB包 {abName} 已删除");
File.Delete(Application.persistentDataPath + "/" + abName);
}
}
}
(4)热更新流程方法
我们前面编写了热更新所需要的各种方法,但是调用步骤比较繁琐,因此可以为外部提供一个方法,用于封装内部的热更新流程,方便外部使用:
这里方法内调用的流程是这样的:
- 下载远端对比文件
- 解析远端对比文件
- 解析本地对比文件
- 进行本地资源与远端资源的比对,根据比对信息删除本地多余AB包,并记录需要下载的远端AB包
- 下载AB包
- 下载完成后用远端的对比文件覆盖本地的对比文件,完成热更新
/// <summary>
/// 检查并进行热更新
/// </summary>
/// <param name="completeCallback">完成后的回调,若操作成功则为true,否则为false</param>
/// <param name="updateInfoCallback">热更新时的步骤信息,可由外部进行调用,如UI展示</param>
public void CheckUpdate(UnityAction<bool> completeCallback = null, UnityAction<string> updateInfoCallback = null)
{
// 清理上次残留信息
remoteAssetBundleInfos.Clear();
localAssetBundleInfos.Clear();
downloadList.Clear();
string persistentPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME; // 对比文件保存路径
string tempPath = Application.persistentDataPath + "/" + COMPARE_INFO_TEMP_FILE_NAME; // 临时对比文件保存路径
//加载远端资源对比文件
updateInfoCallback?.Invoke("开始更新资源");
DownloadCompareInfoFile((isDone) =>
{
if (isDone)
{
updateInfoCallback?.Invoke("对比文件下载结束,开始解析远端对比文件");
LoadRemoteCompareInfo();
updateInfoCallback?.Invoke("远端对比文件解析完成,开始解析本地对比文件");
LoadLocalCompareInfo((isDone) =>
{
if (isDone)
{
updateInfoCallback?.Invoke("本地对比文件解析完成,开始资源比对");
UpdateDownloadList();
updateInfoCallback?.Invoke("资源对比完成,开始下载新的AB包");
DownloadAssetBundleFile((isDone) =>
{
if (isDone)
{
// 写入并覆盖本地对比文件
File.WriteAllText(persistentPath, File.ReadAllText(tempPath));
}
completeCallback?.Invoke(isDone);
},
(current, max) =>
{
updateInfoCallback?.Invoke($"AB包下载进度:{current}/{max}");
});
}
else
{
completeCallback.Invoke(false);
}
});
}
else
{
completeCallback.Invoke(false);
}
});
}
外部调用也非常简单:
AssetBundleManager.Instance.CheckUpdate((isDone) =>
{
if (isDone)
{
Debug.Log("资源热更新完成");
}
else
{
Debug.Log("资源热更新失败");
}
}, (updateInfo) =>
{
Debug.Log(updateInfo);
});
(四)完整的下载和更新流程代码
在完成下载和更新的代码后,就完成了客户端的AB包管理器,这里将其整个代码列出来,并且为了方便阅读,这里简单讲解一下里面的各个内容:
- AssetBundleInfo类:AB包的信息,主要用于存储和资源比对。
- CheckUpdate方法:用于检查和进行资源热更新,封装了热更新的整套流程,方便外部调用。
- DownloadCompareInfoFile方法:下载远端资源对比文件(暂时作为临时文件,不直接覆盖本地文件)。
- LoadRemoteCompareInfo方法:解析远端对比文件。
- LoadLocalCompareInfo方法:解析本地对比文件。
- UpdateDownloadList方法:进行本地和远端资源的对比,内部会对本地多余的AB包进行删除,并更新待下载的AB包列表。
- DownloadAssetBundleFile方法:下载待下载列表中的所有AB包。
- DownloadFile方法:辅助方法,用于从资源服务器下载文件。
/// <summary>
/// AB包管理器
/// </summary>
public class AssetBundleManager : MonoBehaviour
{
public static AssetBundleManager Instance { get; private set; }
private void Awake()
{
Instance = this;
}
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
// 远端下载的临时对比文件名
private static string COMPARE_INFO_TEMP_FILE_NAME = "compareInfo_temp.txt";
// FTP相关
private static string FTP_PATH = "ftp://127.0.0.1/AssetBundle/PC/";
private static string USER_NAME = "pmm";
private static string PASSWORD = "123456";
/// <summary>
/// AB包信息类
/// </summary>
private class AssetBundleInfo
{
public string name; // 包名
public long size; // 大小
public string md5; // md5码
public AssetBundleInfo(string name, long size, string md5)
{
this.name = name;
this.size = size;
this.md5 = md5;
}
}
// 存储远端下载的AB包信息
private Dictionary<string, AssetBundleInfo> remoteAssetBundleInfos = new Dictionary<string, AssetBundleInfo>();
// 存储本地的AB包信息
private Dictionary<string, AssetBundleInfo> localAssetBundleInfos = new Dictionary<string, AssetBundleInfo>();
// 待下载AB包列表
private List<string> downloadList = new List<string>();
/// <summary>
/// 检查并进行热更新
/// </summary>
/// <param name="completeCallback">完成后的回调,若操作成功则为true,否则为false</param>
/// <param name="updateInfoCallback">热更新时的步骤信息,可由外部进行调用,如UI展示</param>
public void CheckUpdate(UnityAction<bool> completeCallback = null, UnityAction<string> updateInfoCallback = null)
{
// 清理上次残留信息
remoteAssetBundleInfos.Clear();
localAssetBundleInfos.Clear();
downloadList.Clear();
string persistentPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME; // 对比文件保存路径
string tempPath = Application.persistentDataPath + "/" + COMPARE_INFO_TEMP_FILE_NAME; // 临时对比文件保存路径
//加载远端资源对比文件
updateInfoCallback?.Invoke("开始更新资源");
DownloadCompareInfoFile((isDone) =>
{
if (isDone)
{
updateInfoCallback?.Invoke("对比文件下载结束,开始解析远端对比文件");
LoadRemoteCompareInfo();
updateInfoCallback?.Invoke("远端对比文件解析完成,开始解析本地对比文件");
LoadLocalCompareInfo((isDone) =>
{
if (isDone)
{
updateInfoCallback?.Invoke("本地对比文件解析完成,开始资源比对");
UpdateDownloadList();
updateInfoCallback?.Invoke("资源对比完成,开始下载新的AB包");
DownloadAssetBundleFile((isDone) =>
{
if (isDone)
{
// 写入并覆盖本地对比文件
File.WriteAllText(persistentPath, File.ReadAllText(tempPath));
}
completeCallback?.Invoke(isDone);
},
(current, max) =>
{
updateInfoCallback?.Invoke($"AB包下载进度:{current}/{max}");
});
}
else
{
completeCallback.Invoke(false);
}
});
}
else
{
completeCallback.Invoke(false);
}
});
}
/// <summary>
/// 下载对比文件
/// </summary>
/// <param name="completeCallback">下载完成的回调,若下载成功则为true,否则为false</param>
public async void DownloadCompareInfoFile(Action<bool> completeCallback = null)
{
// 一些变量
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_TEMP_FILE_NAME; // 下载路径
bool isDone = false; // 是否下载成功
int retryCount = 5; // 重新下载次数
// 下载对比文件
while (!isDone && retryCount > 0)
{
await Task.Run(() =>
{
isDone = DownloadFile(COMPARE_INFO_FILE_NAME, downloadPath);
});
retryCount--;
}
completeCallback?.Invoke(isDone);
}
/// <summary>
/// 解析远端对比文件
/// </summary>
public void LoadRemoteCompareInfo()
{
string downloadPath = Application.persistentDataPath + "/" + COMPARE_INFO_TEMP_FILE_NAME; // 文件路径
string compareInfo = File.ReadAllText(downloadPath);
string[] assetBundleInfos = compareInfo.Split("|");
// 解析每个AB包
foreach (string assetBundleInfo in assetBundleInfos)
{
string[] infos = assetBundleInfo.Split(" ");
// 记录AB包信息,方便进行对比
remoteAssetBundleInfos.Add(infos[0], new AssetBundleInfo(infos[0], long.Parse(infos[1]), infos[2]));
}
}
/// <summary>
/// 解析本地对比文件
/// </summary>
/// <param name="completeCallback">解析本地对比文件完成的回调,若成功则为true,否则为false</param>
public void LoadLocalCompareInfo(Action<bool> completeCallback = null)
{
string persistentPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME;
string streamingPath = Application.streamingAssetsPath + "/" + COMPARE_INFO_FILE_NAME;
// 先检查持久化目录是否存在本地对比文件
if (File.Exists(persistentPath))
{
StartCoroutine(LoadLocalCompareInfoCoroutine(persistentPath, completeCallback));
}
// 再检查流资源目录是否存在本地对比文件
else if (File.Exists(streamingPath))
{
StartCoroutine(LoadLocalCompareInfoCoroutine(streamingPath, completeCallback));
}
// 若两个 if 语句都没有进入,则代表本地没有默认资源
}
// 加载本地对比文件协程
private IEnumerator LoadLocalCompareInfoCoroutine(string filePath, Action<bool> completeCallback = null)
{
// 使用 UnityWebRequest 读取本地文件
UnityWebRequest request = UnityWebRequest.Get(filePath);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// 解析本地对比文件信息
string compareInfo = request.downloadHandler.text;
string[] assetBundleInfos = compareInfo.Split("|");
// 解析每个AB包
foreach (string assetBundleInfo in assetBundleInfos)
{
string[] infos = assetBundleInfo.Split(" ");
// 记录AB包信息,方便进行对比
localAssetBundleInfos.Add(infos[0], new AssetBundleInfo(infos[0], long.Parse(infos[1]), infos[2]));
}
completeCallback?.Invoke(true);
}
else
{
completeCallback?.Invoke(false);
}
}
/// <summary>
/// 更新待下载的AB包列表
/// </summary>
public void UpdateDownloadList()
{
// 资源比对
foreach (string abName in remoteAssetBundleInfos.Keys)
{
// 下载:远端有而本地没有
if (!localAssetBundleInfos.ContainsKey(abName))
{
downloadList.Add(abName);
}
else
{
// 更新:本地和远端都有,但MD5码不同
if (localAssetBundleInfos[abName].md5 != remoteAssetBundleInfos[abName].md5)
{
downloadList.Add(abName);
}
// 每次操作完对本地同名AB包记录进行删除,最后剩余的就是需要被删除的AB包
localAssetBundleInfos.Remove(abName);
}
}
// 删除:删除本地多余的AB包(只删除持久化目录里的,因为默认资源是只读的)
foreach (string abName in localAssetBundleInfos.Keys)
{
if (File.Exists(Application.persistentDataPath + "/" + abName))
{
Debug.Log($"本地多余AB包 {abName} 已删除");
File.Delete(Application.persistentDataPath + "/" + abName);
}
}
}
/// <summary>
/// 下载AB包
/// </summary>
/// <param name="completeCallback">下载完成的回调,若全部下载成功则为true,否则为false</param>
/// <param name="progressCallback">下载进度回调,每次下载AB包完成都会调用,参数分别为当前下载数和最大下载数</param>
public async void DownloadAssetBundleFile(Action<bool> completeCallback = null, Action<int,int> progressCallback = null)
{
// 一些变量
string downloadPath = Application.persistentDataPath + "/"; // 下载路径
bool isDone = false; // 是否下载成功
List<string> removeList = new List<string>(); // 记录下载成功的AB包,需要被移除
int retryCount = 5; // 重新下载次数
int downloadCount = 0; // 已下载的AB包数量
int maxDownloadCount = downloadList.Count; // 需要下载的AB包数量
// 下载文件,下载失败后会重新下载,直至下载完毕 或者 超过最大重新下载次数
while (downloadList.Count > 0 && retryCount > 0)
{
for (int i = 0; i < downloadList.Count; i++)
{
// 下载
isDone = false;
string fileName = downloadList[i];
await Task.Run(() =>
{
isDone = DownloadFile(fileName, downloadPath + fileName);
});
// 下载成功则添加进待移除列表
if (isDone)
{
progressCallback?.Invoke(++downloadCount, maxDownloadCount);
removeList.Add(fileName);
}
}
// 移除下载成功的AB包
for (int i = 0; i < removeList.Count; i++)
{
downloadList.Remove(removeList[i]);
}
retryCount--;
}
// 调用回调,全部下载成功则返回true,否则为false
completeCallback?.Invoke(downloadList.Count == 0);
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="savePath">下载保存的路径</param>
private bool DownloadFile(string fileName, string savePath)
{
try
{
// 创建FTP请求和相关设置
FtpWebRequest request = FtpWebRequest.Create(new Uri(FTP_PATH + fileName)) as FtpWebRequest;
request.Credentials = new NetworkCredential(USER_NAME, PASSWORD); // 有匿名账号可以不设置凭证,这里为了安全还是设置了凭证
request.Proxy = null;
request.KeepAlive = false;
request.Method = WebRequestMethods.Ftp.DownloadFile;
request.UseBinary = true;
// 获取响应
using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
{
// 获取下载流
using (Stream downloadStream = response.GetResponseStream())
{
// 将资源服务器的文件写入保存路径
using (FileStream fileStream = File.Create(savePath))
{
// 读取本地要上传的文件
byte[] bytes = new byte[2048];
// 写入到目标文件
int length;
while ((length = downloadStream.Read(bytes, 0, bytes.Length)) > 0)
{
fileStream.Write(bytes, 0, length);
}
}
}
Debug.Log($"下载完成 [{response.StatusCode}]:{fileName}");
return true;
}
}
catch (Exception ex)
{
Debug.LogWarning($"{fileName} 下载失败:{ex.Message}");
return false;
}
}
private void OnDestroy()
{
Instance = null;
}
}
五、AB包上传工具制作
我们之前使用编辑器制作了一些用于AB包上传的工具:
- AssetBundleCompareTool:用于生成AB包对比文件。
- AssetBundleUploadTool:用于将AB包及其对比文件上传到资源服务器。
- AssetBundleMoveTool:用于将指定AB包移动到StreamingAssets文件夹中,作为默认资源。
现在,我们将制作一个相对完整的AB包上传工具,用于整合上述的这些功能。
我们需要完成这两步:
- 创建一个可视化界面,方便操作。
- 移植之前的这些工具到同一个工具类中,略微修改一下即可,然后这些方法由界面的按钮进行调用。
我们需要编写的工具代码如下:
/// <summary>
/// AB包上传工具
/// </summary>
public class AssetBundleTool : EditorWindow
{
#region 窗口
// 平台选择
private int currentSelectIndex = 0;
private string[] targetPlatform = new string[] { "PC", "IOS", "Android" };
// 资源服务器IP
private string serverIP = "ftp://127.0.0.1";
[MenuItem("AB包工具/打开工具窗口")]
private static void OpenWindow()
{
AssetBundleTool window = EditorWindow.GetWindowWithRect(typeof(AssetBundleTool), new Rect(0, 0, 350, 220)) as AssetBundleTool;
window.Show();
}
private void OnGUI()
{
// 平台页签显示
GUI.Label(new Rect(10, 10, 150, 15), "平台选择");
currentSelectIndex = GUI.Toolbar(new Rect(10, 30, 250, 20), currentSelectIndex, targetPlatform);
// 资源服务器IP地址设置
GUI.Label(new Rect(10, 60, 150, 15), "资源服务器地址");
serverIP = GUI.TextField(new Rect(10, 80, 150, 20), serverIP);
// 一些按钮
if (GUI.Button(new Rect(10, 110, 120, 40), "创建资源对比文件"))
{
CreateAssetBundleCompareFile();
}
if (GUI.Button(new Rect(140, 110, 200, 40), "保存默认资源到StreamingAssets"))
{
MoveAssetBundleToStreamingAssets();
}
if (GUI.Button(new Rect(10, 160, 330, 40), "上传AB包和对比文件"))
{
UploadAssetBundle();
}
}
#endregion
#region 工具方法
// AB包路径,用于上传
private static string ASSET_BUNDLE_PATH = Application.dataPath + "/Arts/AssetBundle/";
// 对比文件名
private static string COMPARE_INFO_FILE_NAME = "compareInfo.txt";
// FTP上传相关账号密码
private static string USER_NAME = "pmm";
private static string PASSWORD = "123456";
/// <summary>
/// 创建AB包对比文件
/// </summary>
private void CreateAssetBundleCompareFile()
{
// 根据窗口选择的平台决定路径
string directoryPath = ASSET_BUNDLE_PATH + targetPlatform[currentSelectIndex] + "/";
// 获取所有AB包文件
DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath);
FileInfo[] fileInfos = directoryInfo.GetFiles();
// 获取并拼接AB包的相关信息
StringBuilder abCompareInfo = new StringBuilder();
foreach (FileInfo fileInfo in fileInfos)
{
// 该文件夹下,没有后缀的文件是AB包
if (fileInfo.Extension == "")
{
abCompareInfo.Append($"{fileInfo.Name} {fileInfo.Length} {GetMD5(fileInfo.FullName)}|");
}
}
if (abCompareInfo.Length > 0)
{
abCompareInfo.Remove(abCompareInfo.Length - 1, 1); // 去掉最后一个"|"
}
// 将信息存储为对比文件
File.WriteAllText(directoryPath + COMPARE_INFO_FILE_NAME, abCompareInfo.ToString());
AssetDatabase.Refresh();
Debug.Log("AB包对比文件生成成功");
}
/// <summary>
/// 获取文件MD5码
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>MD5码</returns>
private string GetMD5(string filePath)
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open))
{
// 获取MD5码字节数组
MD5 md5 = MD5.Create();
byte[] hash = md5.ComputeHash(fileStream);
// 将MD5码转成十六进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
stringBuilder.Append(hash[i].ToString("x2")); // 十六进制小写
}
return stringBuilder.ToString();
}
}
/// <summary>
/// 移动指定AB包到StreamingAssets文件夹
/// </summary>
private void MoveAssetBundleToStreamingAssets()
{
// 获取选中的资源
UnityEngine.Object[] selectedAssets = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.DeepAssets);
if (selectedAssets.Length == 0)
return;
// 处理每个选中的资源,拷贝到StreamingAssets文件夹,同时生成资源对比文件
StringBuilder abCompareInfo = new StringBuilder();
foreach (var asset in selectedAssets)
{
// 获取文件名
string assetPath = AssetDatabase.GetAssetPath(asset);
string fileName = assetPath.Substring(assetPath.LastIndexOf('/') + 1);
// 获取文件信息
FileInfo fileInfo = new FileInfo(Application.streamingAssetsPath + "/" + fileName);
if (fileInfo.Extension != "")
continue; // 排除非AB包文件
// 拷贝文件
AssetDatabase.CopyAsset(assetPath, "Assets/StreamingAssets/" + fileName);
// 将AB包信息添加进比对文件中
abCompareInfo.Append($"{fileInfo.Name} {fileInfo.Length} {AssetBundleCompareTool.GetMD5(fileInfo.FullName)}|");
}
if (abCompareInfo.Length > 0)
{
abCompareInfo.Remove(abCompareInfo.Length - 1, 1); // 去掉最后一个"|"
}
// 往StreamingAssets文件夹写入资源对比文件
File.WriteAllText(Application.streamingAssetsPath + "/" + COMPARE_INFO_FILE_NAME, abCompareInfo.ToString());
AssetDatabase.Refresh();
}
/// <summary>
/// 上传AB包和对比文件到资源服务器
/// </summary>
private void UploadAssetBundle()
{
// 根据窗口选择的平台决定路径
string directoryPath = ASSET_BUNDLE_PATH + targetPlatform[currentSelectIndex] + "/";
// 获取里面的文件信息
DirectoryInfo directoryInfo = Directory.CreateDirectory(directoryPath);
FileInfo[] fileInfos = directoryInfo.GetFiles();
foreach (FileInfo fileInfo in fileInfos)
{
// 将比对文件和AB包进行上传
if (fileInfo.Extension == "" || fileInfo.Name == COMPARE_INFO_FILE_NAME)
{
FtpUploadFile(fileInfo.FullName, fileInfo.Name);
}
}
}
/// <summary>
/// 上传文件到资源服务器的辅助方法
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="fileName">上传后的文件名</param>
private async void FtpUploadFile(string filePath, string fileName)
{
await Task.Run(() =>
{
try
{
// 获取需要上传到服务器的路径
string serverPath = serverIP + "/AssetBundle/" + targetPlatform[currentSelectIndex] + "/" + fileName;
// 创建FTP请求和相关设置
FtpWebRequest request = FtpWebRequest.Create(new Uri(serverPath)) as FtpWebRequest;
request.Credentials = new NetworkCredential(USER_NAME, PASSWORD);
request.Proxy = null;
request.KeepAlive = false;
request.Method = WebRequestMethods.Ftp.UploadFile;
request.UseBinary = true;
// 上传文件
using (Stream uploadStream = request.GetRequestStream())
{
using (FileStream fileStream = File.OpenRead(filePath))
{
// 读取本地要上传的文件
byte[] bytes = new byte[2048];
// 写入到目标文件
int length;
while ((length = fileStream.Read(bytes, 0, bytes.Length)) > 0)
{
uploadStream.Write(bytes, 0, length);
}
}
}
// 接收响应
using (FtpWebResponse response = request.GetResponse() as FtpWebResponse)
{
Debug.Log($"上传完成 [{response.StatusCode}]:{fileName}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"{fileName} 上传失败:{ex.Message}");
}
});
}
#endregion
}
最终效果:

六、AB包下载管理器路径优化
1、概述
上一节我们完善了AB包上传工具,现在我们来优化客户端的AB包下载管理器。
这里主要是进行路径优化,因为不同平台的特殊路径,读取表现是不同的,因此需要针对多平台进行修改。这里会用到这几个知识点:
- 不同平台的特殊路径及读取表现
| 路径分类 & 读取方式 | PC | Android | IOS |
|---|---|---|---|
| persistentDataPath | |||
| └─ UnityWebRequest/WWW读取 | 加 file:/// |
加 file:/// |
加 file:/// |
| └─ C#文件流读取 | 无需处理 | 无需处理 | 无需处理 |
| streamingAssetsPath | |||
| └─ UnityWebRequest/WWW读取 | 加 file:/// |
直接用 | 加 file:/// |
| └─ C#文件流读取 | 无需处理 | 不可用 | 无需处理 |
- 多路测试
预处理器指令#if ... #elif ... #else ... #endif可以选择性的编译C#部分代码。
- 平台测试符号
Unity 支持对脚本使用的平台 #define 指令如下,这里只展示项目会用到的,具体可以在Unity官网查看:Unity - 手册: Unity 脚本符号参考。
| Define | 功能 |
|---|---|
| UNITY_IOS | 用于为 iOS 平台编译/执行代码的 #define 指令。 |
| UNITY_ANDROID | 用于 Android 平台的 #define 指令。 |
2、代码修改
(1)首先修改DownloadFile方法,不同平台的资源服务器下载路径不一样:
private static string FTP_PATH = "ftp://127.0.0.1";
private bool DownloadFile(string fileName, string savePath)
{
try
{
#if UNITY_IOS
string serverPath = FTP_PATH + "/AssetBundle/IOS/" + fileName;
#elif UNITY_ANDROID
string serverPath = FTP_PATH + "/AssetBundle/Android/" + fileName;
#else
string serverPath = FTP_PATH + "/AssetBundle/PC/" + fileName;
#endif
// 创建FTP请求和相关设置
FtpWebRequest request = FtpWebRequest.Create(new Uri(serverPath)) as FtpWebRequest;
// 其他不变...
}
catch (Exception ex)
{
Debug.LogWarning($"{fileName} 下载失败:{ex.Message}");
return false;
}
}
(2)LoadRemoteCompareInfo不用修改,因为这是读取persistentDataPath且用C#流文件读取的。
(3)之后修改LoadLocalCompareInfo方法,这里用到了UnityWebRequest,且用到了流文件目录和持久化目录。因此读取StreamingAssets目录时需要注意,因为Android平台读取该目录时默认会加一个前缀(好像是jar:file:///),不需要我们手动加,所以需要分两种情况,修改如下:
public void LoadLocalCompareInfo(Action<bool> completeCallback = null)
{
// 持久化路径
string persistentPath = Application.persistentDataPath + "/" + COMPARE_INFO_FILE_NAME;
// 流资源路径
string streamingPath = Application.streamingAssetsPath + "/" + COMPARE_INFO_FILE_NAME;
// 先检查持久化目录是否存在本地对比文件
if (File.Exists(persistentPath))
{
StartCoroutine(LoadLocalCompareInfoCoroutine("file:///" + persistentPath, completeCallback));
}
// 再检查流资源目录是否存在本地对比文件
else if (File.Exists(streamingPath))
{
#if UNITY_ANDROID
StartCoroutine(LoadLocalCompareInfoCoroutine(streamingPath, completeCallback));
#else
StartCoroutine(LoadLocalCompareInfoCoroutine("file:///" + streamingPath, completeCallback));
#endif
}
// 若两个 if 语句都没有进入,则代表本地没有默认资源
}
自此,AB包下载管理器的路径部分就修改完了,当然这个管理器代码应该还是有点问题的,这里只测试了PC端的,其他端并不能保证正常运行。这里主要是要掌握不同平台路径优化的思路,具体正确且完整的代码可以交由大家自行完成。