需求:要做一个能及时更新的界面,界面里面放ui(用来做定制化的内容),不同的用户群体加载不同的ui,ui上还有一个点击下载的功能键,点击下载后会从服务器上下载资源(3d模型),然后在场景中生成,用户要可以和这个新的3d模型进行互动
这个需求的重点之一就是怎么对ab包进行校验
毫无疑问这个需要用到热更新,之前有使用AB和xlua进行热更的学习,由于这个功能目前看起来还不需要对代码进行更新,因此主要还是如何进行资源的更新
AB包资源的热更新本人在使用的时候有以下几点下了功夫
1AB包的加载方式
2AB包的校验方式
3AB包的卸载方式
4AB包的打包方式
Assetbundle使用
AB包的加载方式
在打包AB包的时候一个内容会生成两个部分,一个是资源包,一个是manifest依赖文件
这两个都要加载到缓存中,然后对其进行使用
本地加载
加载资源包的话使用AssetBunndle.LoadFromFile这个api就行了,这是同步的,异步的也有,不过还没研究(目前加载的内容不多)
这个api里需要传入路径,我的项目里服务器下拉的数据第一时间是写到本地,然后也用这个api去读取加载资源,所以没用到loadfrommemory和loadfromstream
加载后进行使用
获得对应的ab后,当你需要里面的预制体时,可用ab.loadAsset<>()这个api
<>里面填写强转的类型,()里填写这个包里对应预制体的名称(可以通过ab.getallAseetsname()这个api获得这个包里所有的资源名称)
然后进行instaniate初始化到场景中即可
AB包的校验方式
这个是我写这个需求里花的时间比较多的内容, 在网上查了很多相关的帖子,也去官网找了一番
有的大佬是直接用的hash(ab.GetHashCode)或者id(ab.GetInstanceID)去进行本地和服务器上ab包的校验的
我初步尝试了一下,发现有的时候就算本地和服务器上的资源是不一样的,生成的hash也有可能是一致的,有的大佬也说了这个问题,因此这个方法我是不打算使用的
在查看使用官方的插件asset bundle brower(我是unity2019的,直接在package manger搜然后下载就行了)打包出来的内容的时候,我看到了所有的资源包对应的manifest文件里面有CRC的数据,这个在每次更新和打包的时候他都会有所变化
于是我决定用这个作为校验的标准,但是官方并没有相应的api供我使用,查了很久也没有其他的方法,网上的帖子有的是用的自己的打包方式(这里提一下,自己打包的话里面的hash是可以唯一的,但是因为时间关系我并不打算自己手lu一个自定义的打包功能(毕竟随时可能跑路)所以对ab包的校验有时间的也可以用这个方法)
有的大佬是自己生成的CRC或者MD5之类的,放在自己的服务器上,然后校验(没有服务器后台的我们自然是不可能用这个方法的),后面想了一下反正这个manifest就是个文件,索性自己解析一下这个文件然后自己拿里面的crc数据,然后进行校验不就行了
(当然这个方法存在一定的漏洞,比如本地的这个文件可以随意修改,我在测试的使用就用了这个漏洞,在下载的时候必须把资源包对应的manifest文件也下载下来,虽然不大但也终归是有数据的)
综合以上,我的最终的解决方案是通过官方打包生成的mainifest里的CRC进行AB包本地和服务器的校验
思路就是通过file.readalltext(路径)获得manifest文件,然后解析里面的内容拿到CRC的数据
根据这个crc去校验资源包是否一致,不一致则以服务器的为主,从服务器下拉资源包和对应的mianifest文件
AB包的卸载方式
1通过查看官方文档和大佬们的帖子,以及自己敲代码时候遇到的问题,我发现每一个assetbundle都只能通过unload这个方法去卸载当前这个ab包里的资源,如果不进行卸载,是无法对这个ab包进行数据的覆盖和更新的,如果一个ab包已经加载过一个文件了,你还想拿他去加载其他的ab包,他会直接报错,提示你这个ab包里已经有资源了
2卸载的api unload这个方法需要填写一个布尔值,true代表完全卸载(他会清空当前ab包的资源,包括切断当前ab包的资源和场景中用这个资源生成的内容的联系,简单的说就是这个资源里对应的依赖都会消失,然后场景中的iamge之类的东西就会出现missing sprite之类的情况);而填写false的话他在切断自己和场景中用自己生成的资源的内容的联系之前会备份一个出来,然后让场景中的内容引用那个备份,然后再清空自己身上的资源,这个也就意味着你下次用这个ab包去加载的时候他不会报错,但是他在内存中多了一个上次的资源,久而久之会很消耗性能和资源(当你的ab包很大的时候更顶),所以官方是推荐使用true的,如果非要用false,也得记得在切换场景的时候或者对那个备份的场景中的资源不再使用的时候去及时清除
综上就是我用unload(true)
补充一点,ab包更新,对应在场景中的内容如果也要更新的话建议先销毁(destroy)然后重新生成,别问,问就是直接gameobject = 新的gameobject这个覆盖的处理是行不通的(会出现资源引用失效的情况,原因尚不得知)
AB包的打包方式
这个我用的是官方的asset bundle brower进行打包的没什么好说的
题外话,我的unity2019在预制体那里选择ab包名称的时候,如图
经常出现不能创建新的名称的问题,解决方法是在asset bundle brower里直接拖你要更新的预制体内容,然后重命名
参考内容:
http://t.zoukankan.com/AaronBlogs-p-6837828.html
https://www.pudn.com/news/625d3cabbe9ad24cfa7c3657.html
https://www.jianshu.com/p/59450c09f718
https://blog.csdn.net/qq_25189313/article/details/77930070?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-77930070-blog-83685845.pc_relevant_multi_platform_featuressortv2dupreplace&spm=1001.2101.3001.4242.1&utm_relevant_index=3
https://qianxi.blog.csdn.net/article/details/80513882?spm=1001.2101.3001.6650.5&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-5-80513882-blog-107308583.pc_relevant_multi_platform_featuressortv2dupreplace&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-5-80513882-blog-107308583.pc_relevant_multi_platform_featuressortv2dupreplace&utm_relevant_index=8
贴代码,我是草考了一个大佬的帖子写的(校验方式不同,后面的更新方式也不同),不过大佬(楼下这个)给了我很多启发
https://blog.csdn.net/hl1991825/article/details/84327622
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using LitJson;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class AssetsBundleManager : MonoBehaviour
{
AssetBundle mainAb = null;
AssetBundleManifest mainManifest = null;
public Slider progress;
UnityWebRequest webRequest = null;
Dictionary<string, AssetBundle> abDicts = new Dictionary<string, AssetBundle>();
string[] abKeys = null;
private string aBUrl
{
get
{
#if UNITY_EDITOR || UNITY_STANDALONE
return "你的服务器上pc端ab包的路径";
#elif UNITY_ANDROID
return "你的服务器上安卓端ab包的路径";
#endif
}
}
private string mainABName
{
get
{
#if UNITY_EDITOR || UNITY_STANDALONE
return "StandaloneWindows";
#elif UNITY_IPHONE
return "IOS";
#elif UNITY_ANDROID
return "Android";
#endif
}
}
//各个平台下的基础路径 --- 利用宏判断当前平台下的streamingAssets路径
private string basePath
{
get
{
//使用StreamingAssets路径注意AB包打包时 勾选copy to streamingAssets
#if UNITY_EDITOR || UNITY_STANDALONE
return Application.dataPath + "/StreamingAssets/";
#elif UNITY_IPHONE
return Application.dataPath + "/Raw/";
#elif UNITY_ANDROID
return Application.persistentDataPath +"/";
#endif
}
}
// Start is called before the first frame update
void Start()
{
}
/// <summary>
/// 脚本隐藏时
/// </summary>
private void OnDisable()
{
if (mainAb != null)
{
mainAb.Unload(true);
}
}
public void UpdateAB()
{
StartCoroutine(UpdateABIenumerator());
}
IEnumerator UpdateABIenumerator()
{
//先从服务器下拉ab包的数据,和当前版本不一样则下拉新的ab包资源
//逻辑是:先从本地的streamingassets读取ab包,如果为空,则下拉服务器最新的ab包;
//如果不为空,则下拉服务器的主ab包,校验本地和服务器两者ab包的内容是否有需要更新的, 有则进行更新和覆盖的操作,没有则跳过
//先检查本地是否有ab包
yield return null;
if (File.Exists(basePath + mainABName))
{
//说明本地有ab包,开始进行校验
StartCoroutine(UpdateAbFromLocal());
}
else
{
//本地没有ab包,直接从服务器下拉最新的ab包
StartCoroutine(UpdateABFromServer());
}
}
Dictionary<string, GameObject> abGOLists = new Dictionary<string, GameObject>();
IEnumerator UpdateABFromServer()
{
//直接从服务器下拉ab包
webRequest = UnityWebRequest.Get(aBUrl + mainABName);
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
//显示进度条
progress.value = webRequest.downloadProgress;
yield return null;
}
//从循环跳出则说明已下载完成
Debug.Log("ab resources download is done");
//获取下拉的资源包括manifest,写入本地
byte[] data = webRequest.downloadHandler.data;
File.WriteAllBytes(basePath + mainABName, data);
//下载manifest文件
webRequest = UnityWebRequest.Get(aBUrl + mainABName + ".manifest");
yield return webRequest.SendWebRequest();
File.WriteAllBytes(basePath + mainABName + ".manifest", webRequest.downloadHandler.data);
//获取主包下的其他分包资源
//加载主包
mainAb = AssetBundle.LoadFromFile(basePath + mainABName);
mainManifest = mainAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
//abKeys = new string[mainManifest.GetAllAssetBundles().Length];
//需要对分包也进行manifest的校验
//这是用来记录需要更新的分包的名称
StartCoroutine(CheckSum((tempList) =>
{
Debug.Log(tempList.Count);
StartCoroutine(UpdateSecondAB(tempList));
}));
}
IEnumerator UpdateAbFromLocal()
{
//本地的逻辑
//校验本地和服务器的主包是否一致,一致则直接用本地的,不一致则遍历主包中的分包,获取其他的不一致,从服务器下拉并保存最新的分包内容(一致的分包则不再进行下载操作)
//开始校验主包的mainfest,主包的hash是一定不一致的,所以需要获得主包的assetbundlemainfest然后获得分包的哈希值,校验这个才有效
//服务器的manifest
//分包校验的新模式,先下载manifest文件,把里面的crc提取出来和本地的进行校验
yield return null;
//读取本地主包的manifest
string mainfestLocal = File.ReadAllText(basePath + mainABName + ".manifest");
string CRCLoca = mainfestLocal.Split(':')[2];
//读取服务器主包的manifest
webRequest = UnityWebRequest.Get(aBUrl + mainABName + ".manifest");
yield return webRequest.SendWebRequest();
string CRCServer = webRequest.downloadHandler.text.Split(':')[2];
//如果不同则覆盖最新的主包
if (CRCLoca == CRCServer)
{
Debug.Log("主包版本相同无需更新");
//直接加载主包的依赖
if (mainAb == null)
{
mainAb = AssetBundle.LoadFromFile(basePath + mainABName);
mainManifest = mainAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}
abKeys = new string[mainManifest.GetAllAssetBundles().Length];
abKeys = mainManifest.GetAllAssetBundles();
for (int i = 0; i < abKeys.Length; i++)
{
if (abDicts.ContainsKey(abKeys[i]) && abGOLists.ContainsKey(abKeys[i]))
{
continue;
}
abDicts.Add(abKeys[i], AssetBundle.LoadFromFile(basePath + abKeys[i]));
//初始化该预制体并保存到集合中方便调用
GameObject temObj = abDicts[abKeys[i]].LoadAsset<GameObject>(abKeys[i]);
abGOLists.Add(abKeys[i], temObj);
}
ShowUI();
}
else
{
//需要更新主包以及里面有所更新的分包
//初始化assetbundle
//重置ab有关的内容
if (mainAb != null)
{
mainAb.Unload(true);
}
//abDicts = new Dictionary<string, AssetBundle>();
Debug.Log("正在从服务器下拉最新数据");
StartCoroutine(UpdateABFromServer());
}
}
void ShowUI()
{
//实现简单的需求,ui上有普通资源和需要从服务器下拉的资源
//点击下载开始从服务器下拉资源,并且显示进度条,下载完成(这个方法不用下载ab包,普通的下载方法即可)后更新ui界面
foreach (var item in abGOLists.Keys)
{
GameObject tem = null;
if (GameObject.Find(item))
{
//tem = GameObject.Find(item);
//tem = abGOLists[item];
//continue;
Destroy(GameObject.Find(item));
}
tem = Instantiate(abGOLists[item], GameObject.Find("Canvas").transform);
tem.name = item;
}
}
IEnumerator CheckSum(Action<List<string>> action)
{
List<string> templist = new List<string>();
//拿到服务器上本地没有以及crc不一致的分包的名称
string[] secondABManifests = mainManifest.GetAllAssetBundles();
for (int i = 0; i < secondABManifests.Length; i++)
{
Debug.Log(secondABManifests[i]);
//拿着分包的名称去读取他们的manifest文件里的crc,和服务器的crc进行比较,不同则加入templist
//先检查一下本地有没有这个文件
if (!File.Exists(basePath + secondABManifests[i] + ".manifest"))
{
//本地没有这个文件,直接到服务器下载,无需校验
templist.Add(secondABManifests[i]);
continue;
}
string secondABManifestLocal = File.ReadAllText(basePath + secondABManifests[i] + ".manifest").Split(':')[2];
webRequest = UnityWebRequest.Get(aBUrl + secondABManifests[i] + ".manifest");
yield return webRequest.SendWebRequest();
string secondABMainfestServer = webRequest.downloadHandler.text.Split(':')[2];
if (secondABManifestLocal != secondABMainfestServer)
{
//此时进入templist
templist.Add(secondABManifests[i]);
}
}
action(templist);
}
IEnumerator UpdateSecondAB(List<string> tempList)
{
int index = 0;
foreach (var secondAB in tempList)
{
Debug.Log(aBUrl + secondAB);
//根据名称下载对应的分包资源
webRequest = UnityWebRequest.Get(aBUrl + secondAB);
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
progress.value = webRequest.downloadProgress;
//协程里的所有while都要写yield return 否则会卡死
yield return null;
}
Debug.Log("分包:" + secondAB + "下载完成...");
//写入本地
byte[] data = webRequest.downloadHandler.data;
File.WriteAllBytes(basePath + secondAB, data);
//下载manifest文件
webRequest = UnityWebRequest.Get(aBUrl + secondAB + ".manifest");
yield return webRequest.SendWebRequest();
File.WriteAllBytes(basePath + secondAB + ".manifest", webRequest.downloadHandler.data);
index++;
}
abKeys = new string[mainManifest.GetAllAssetBundles().Length];
abKeys = mainManifest.GetAllAssetBundles();
foreach (var item in abKeys)
{
//记录分包的内容
//如果字典里已经有了,则进行覆盖操作
if (abDicts.ContainsKey(item))
{
//卸载原有的ab资源
abDicts[item].Unload(true);
abDicts[item] = AssetBundle.LoadFromFile(basePath + item);
abGOLists.Remove(item);
}
else
{
abDicts.Add(item, AssetBundle.LoadFromFile(basePath + item));
}
GameObject temObj = abDicts[item].LoadAsset<GameObject>(item);
temObj.name = item;
abGOLists.Add(item, temObj);
}
//所有资源下载完毕后开始加载到场景中
ShowUI();
}
public void UpdateAssetBundleByName(string abName)
{
//这个方法是用来下载和更新指定的ab包的
StartCoroutine(UpdateAssetBundleByNameIE(abName));
}
IEnumerator UpdateAssetBundleByNameIE(string abName)
{
AssetBundle temAssetBundle = null;
if (temAssetBundle != null)
{
temAssetBundle.Unload(true);
}
//先根据包名校验ab包资源是否需要从服务器更新
//先检查一下本地有没有这个文件
if (!File.Exists(basePath + abName + ".manifest"))
{
//本地没有这个文件,直接到服务器下载资源包和对应的manifest文件,无需校验
//先下载资源包
webRequest = UnityWebRequest.Get(aBUrl + abName);
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
progress.value = webRequest.downloadProgress;
Debug.Log(webRequest.downloadProgress);
yield return null;
}
//写入本地
File.WriteAllBytes(basePath + abName, webRequest.downloadHandler.data);
//加载到内存中,放在内存中随时调用
temAssetBundle = AssetBundle.LoadFromFile(basePath + abName);
abDicts.Add(abName, temAssetBundle);
//保存为go
GameObject tem = temAssetBundle.LoadAsset<GameObject>(abName);
tem.name = abName;
abGOLists.Add(abName, tem);
//下载manifest文件
webRequest = UnityWebRequest.Get(aBUrl + abName+".manifest");
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
progress.value = webRequest.downloadProgress;
Debug.Log(webRequest.downloadProgress);
yield return null;
}
//写入本地
File.WriteAllBytes(basePath + abName + ".manifest", webRequest.downloadHandler.data);
yield break;
}
//本地有这个文件,校验文件是否需要更新
string secondABManifestLocal = "";
try
{
secondABManifestLocal = File.ReadAllText(basePath + abName + ".manifest").Split(':')[2];
}
catch
{
Debug.Log("本地manifest已损坏...");
secondABManifestLocal = "";
}
webRequest = UnityWebRequest.Get(aBUrl + abName + ".manifest");
yield return webRequest.SendWebRequest();
string secondABMainfestServer = webRequest.downloadHandler.text.Split(':')[2];
if (secondABManifestLocal != secondABMainfestServer)
{
//此时才从服务器下拉最新的zb资源包
webRequest = UnityWebRequest.Get(aBUrl + abName);
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
progress.value = webRequest.downloadProgress;
Debug.Log(webRequest.downloadProgress);
yield return null;
}
//写入本地
File.WriteAllBytes(basePath + abName, webRequest.downloadHandler.data);
//加载到内存中,放在内存中随时调用
temAssetBundle = AssetBundle.LoadFromFile(basePath + abName);
abDicts.Add(abName, temAssetBundle);
//保存为go
GameObject tem = temAssetBundle.LoadAsset<GameObject>(abName);
tem.name = abName;
abGOLists.Add(abName, tem);
//temAssetBundle.Unload(true);
//下载manifest文件
webRequest = UnityWebRequest.Get(aBUrl + abName + ".manifest");
webRequest.SendWebRequest();
while (!webRequest.isDone)
{
progress.value = webRequest.downloadProgress;
Debug.Log(webRequest.downloadProgress);
yield return null;
}
//写入本地
File.WriteAllBytes(basePath + abName + ".manifest", webRequest.downloadHandler.data);
}
else
{
//无需更新,直接用本地的ab包资源
temAssetBundle = AssetBundle.LoadFromFile(basePath+abName);
abDicts.Add(abName, temAssetBundle);
//保存为go
GameObject tem = temAssetBundle.LoadAsset<GameObject>(abName);
//Instantiate(tem);
tem.name = abName;
abGOLists.Add(abName, tem);
//temAssetBundle.Unload(true);
}
}
}
同天更新:
加多了一个添加指定ab包的方法,用来实现需求,根据ab包的包名去校验和下载
unload方法最好在需要重新加载的时候再调用