这篇文章内容巨多,逻辑也复杂,花了4天写出来。(写博客还是费时间啊)
很多设计和逻辑,在脑子中是很清晰的,但用文字表述就会显得很复杂,没有图文对照就更难理解了。
Unity资源类型,按加载流程顺序,有三种
AssetBundle
资源以压缩包文件存在(Resources目录下资源打成包体后也是以ab格式存在)Asset
资源在内存中的存在格式GameObject
针对Prefab导出的Asset,可实例化针对AssetBundle
的加载,本文会作讲解,并提供整套方案和代码,
针对Asset
的加载,读者可以参阅Asset同步异步引用计数资源加载管理器
针对GameObject
的加载,读者可以参阅Prefab加载自动化管理引用计数管理器
AssetBundle加载有三套接口,WWW
,UnityWebRequest
和AssetBundle
,大部分文章都推荐AssetBundle
,本人也推荐。
关于AssetBundle的加载原理和用法之类的基础知识读者自己百度学习,这边就不进行大量描述了
前两者都要经历将整个文件的二进制流下载或读取到内存中,然后对这段内存文件进行ab资源的读取解析操作,而AssetBundle
可以只读取存储于本地的ab文件的头部部分,在需要的情况下,读取ab中的数据段部分(Asset资源)。
所以AssetBundle
相对的优势是
所以,从内存和效率方面,AssetBundle
会是目前最优解,而使用非压缩或LZ4读者自己评断(推荐LZ4)
AssetBundle
加载方式最重要的接口(接口用法读者自己百度学习)
AssetBundle.LoadFromFile 从本地文件同步加载ab
AssetBundle.LoadFromFileAsync 从本地文件异步加载ab
AssetBundle.Unload 卸载,注意true和false区别
AssetBundle.LoadAsset 从ab同步加载Asset
AssetBundle.LoadAssetAsync 从ab异步加载Asset
使用异步AssetBundle
加载的时候,大部分开发者都喜欢使用协程的方式去加载,当然这已经成为通用做法。但这种做法弊端也很明显:
大量依赖ab等待加载,逻辑复杂
ab加载状态切换的复杂化
协程顺序的不确定性,增加难度
ab卸载和加载同时进行处理难
ab同步和异步同时进行处理难
协程在某些情况确实可以让开发简单化,但在耦合高的代码中非常容易导致逻辑复杂化。
这里笔者提供一种使用Update去协程化的方案。
我们都知道,使用协程的地方,大部分都是需要等待线程返回逻辑的,而这样的等待逻辑可以使用Update每帧访问的方式,确定线程逻辑是否结束
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
IEnumerator LoadAssetBundle()
{
yield return request;
//do something
}
转变为
void Update()
{
if(request.isDone)
{
//do something
}
}
其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator
原理)。
既然是加载资源,那必然会有队列,笔者这边依据需求和优化要求,设计成四个队列,准备队列
、加载队列
、完成队列
和销毁队列
。
代码如下
private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
队列之间,队列成员的转移需要一个触发点,而这样的触发点如果都写在加载和销毁逻辑里,耦合度过高,而且逻辑复杂还容易出错。
TIP:为什么没有设计异常队列
?
- 一般资源加载,都是默认资源是存在的
- 资源如果不存在,一定是策划没有把资源放进去(嗯,一定是这样)
- 设计上是加载了总依赖关系的Mainfest,是对文件存在性可以进行判断的
- 从性能的角度,通过
File.exists()
来判断文件存在性,是效率低下的方式- 代码中对异常是有处理的,会有重复加载,下载和修复完整性的逻辑
笔者很喜欢的一种设计,就是通过Update来降低耦合度
,这种方式代码清晰,逻辑简单,但缺点也很明显,丢失原始现场。
回到本篇文章,当然是通过Update来运行逻辑,如下
TIP:为什么Update里三个函数的运行顺序跟队列转移顺序不一样?
- UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
- UpdateUnLoad放最后,是因为正在加载的资源要等到加载完才能卸载
根据上面的逻辑,很容易设计下面的接口逻辑
LoadMainfest
是用来加载文件列表和依赖关系的,一般在游戏热更之后,游戏登录界面之前进行游戏初始化的时候。加载的配置文件是Unity导出AssetBundle时生成的主Mainfest文件,具体逻辑如下
_dependsDataList.Clear();
AssetBundle ab = AssetBundle.LoadFromFile(path);
AssetBundleManifest mainfest = ab.LoadAsset("AssetBundleManifest") as AssetBundleManifest;
foreach(string assetName in mainfest.GetAllAssetBundles())
{
string hashName = assetName.Replace(".ab", "");
string[] dps = mainfest.GetAllDependencies(assetName);
for (int i = 0; i < dps.Length; i++)
dps[i] = dps[i].Replace(".ab", "");
_dependsDataList.Add(hashName, dps);
}
ab.Unload(true);
ab = null;
这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意ab.Unload(true);
用完要销毁。
public delegate void AssetBundleLoadCallBack(AssetBundle ab);
private class AssetBundleObject
{
public string _hashName; //hash标识符
public int _refCount; //引用计数
public List<AssetBundleLoadCallBack> _callFunList = new List<AssetBundleLoadCallBack>(); //回调函数
public AssetBundleCreateRequest _request; //异步加载请求
public AssetBundle _ab; //加载到的ab
public int _dependLoadingCount; //依赖计数
public List<AssetBundleObject> _depends = new List<AssetBundleObject>(); //依赖项
}
加载节点的数据结构不复杂,看代码就很容易理解。
依赖加载,是ab加载逻辑里最难最复杂最容易出bug的地方,也是本文的难点。
难点为一下几点:
还未加载
,准备加载
,正在加载
,已经加载
节点关系处理我们来一一分解
首先,看一下ab节点的引用计数要实现的逻辑
注: 上图显示加载和销毁都需要递归标记依赖节点的依赖节点
TIP:为什么引用计数一定要递归标记所有子节点?
我们需要确定一个节点是否需要销毁,是通过引用计数是否为零来判断的,很多语言使用的内存回收机制就是引用计数。
如果只标记当前节点和其一层依赖项,当其依赖项也作为主加载节点,我就没办法判断二层依赖节点是否需要销毁了。
例如按上述逻辑,
- 加载A,标记A+1,C+1
- 加载C,标记A+1,C+2,D+1
- 卸载C,标记A+1,C+1,D+0
- 这里就会卸载D,而实际上,D仍然是需要保留的,不能卸载
所以,带依赖关系的引用计数,需要递归标记所有子节点,才能确认任意一个节点是否需要卸载。
每次加载,都要递归标记,会不会有效率问题?
很幸运,在绝大多数情况,依赖节点关系不会超过三层,依赖节点总数量不超过10个(生成最小依赖树情况下),一般游戏至少一半以上ab节点都是单节点,不包含需要拆分的依赖关系。
用引用计数的方法,可以确定一个资源是否需要销毁。代码逻辑表示为(代码简化了部分逻辑)
private void DoDependsRef(AssetBundleObject abObj)
{
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
DoDependsRef(dpObj); //递归依赖项,加载完
}
}
private AssetBundleObject LoadAssetBundleAsync(string _hashName)
{
AssetBundleObject abObj = null;
if (_ABList.ContainsKey(_hashName)) //队列有
{
abObj = _ABList[_hashName];
DoDependsRef(abObj); //递归引用计数
return abObj;
}
//创建一个加载节点
abObj = new AssetBundleObject();
abObj._hashName = _hashName;
abObj._refCount = 1;
//加载依赖项
string[] dependsData = _dependsDataList[_hashName];
abObj._dependLoadingCount = dependsData.Length;
foreach(var dpAssetName in dependsData)
{
var dpObj = LoadAssetBundleAsync(dpAssetName);
abObj._depends.Add(dpObj);
}
DoLoad(abObj); //调用unity接口开始加载
_ABList.Add(_hashName, abObj); //加入队列
return abObj;
}
上述代码构造了引用计数,递归,加入队列
。理解起来其实不难,难在写出符合设想的逻辑代码。
上面构造了递归引用计数的逻辑,我们再加入队列的逻辑。
队列逻辑在上文已经描述过了,总结几个要点
准备队列
或加载队列
。完成队列
销毁队列
。对应到开启异步加载
和销毁
时,代码如下
private AssetBundleObject LoadAssetBundleAsync(string _hashName)
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName)) //已经加载
{
abObj = _loadedABList[_hashName];
DoDependsRef(abObj);
return abObj;
}
else if (_loadingABList.ContainsKey(_hashName)) //在加载中
{
abObj = _loadingABList[_hashName];
DoDependsRef(abObj);
return abObj;
}
else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
{
abObj = _readyABList[_hashName];
DoDependsRef(abObj);
return abObj;
}
//....................
//创建一个ab节点........
//....................
if (_loadingABList.Count < MAX_LOADING_COUNT) //正在加载的数量不能超过上限
{
DoLoad(abObj); //调用unity接口开始加载
_loadingABList.Add(_hashName, abObj);
}
else _readyABList.Add(_hashName, abObj);
return abObj;
}
private void UnloadAssetBundleAsync(string _hashName)
{
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName))
abObj = _loadedABList[_hashName];
else if (_loadingABList.ContainsKey(_hashName))
abObj = _loadingABList[_hashName];
else if (_readyABList.ContainsKey(_hashName))
abObj = _readyABList[_hashName];
abObj._refCount--;
foreach (var dpObj in abObj._depends)
{
UnloadAssetBundleAsync(dpObj._hashName);
}
if (abObj._refCount == 0)
{//这里只是加入销毁队列,并没有真正销毁,真正销毁要在Update里
_unloadABList.Add(abObj._hashName, abObj);
}
}
从这里,上文已经完成了整个异步加载的逻辑,已经实现创建到销毁的代码。但异步加载还有一个问题没有解决——判读ab节点加载完成
。
我们需要在ab节点及其依赖ab节点都加载完后,告诉上层调用逻辑,ab资源加载完了。简单地做法就是,在Update里逻辑判断一个节点及其子节点都加载完了。我们会有下面这样的代码结构
注:圆角方形表示ab自身加载完成,箭头表示依赖关系
图1-递归判定,如果需要知道A是否加载完,需要依次判定D,E,C,A四个节点,
//不高效的逻辑判定方式
bool IsAssetBundleLoaded(AssetBundleObject abObj)
{
if(abObj._dependLoadingCount == 0 && abObj._ab != null) return true;
foreach (var dpObj in abObj._depends)
{
if(!IsAssetBundleLoaded(dpObj)) return false;
}
return true;
}
很明显的弊端,上述代码需要关心子依赖节点以及孙依赖节点,这样的代码不管是效率还是设计,都不是一种优秀的方式。
那么有没有一种更好的方式呢,笔者提供一种解耦的方式——回调
我们先用图示表示加载A和B到完成的整个过程
注:圆角方形表示ab自身加载完成,箭头表示依赖关系
上图,会按以下回调逻辑
按照上述逻辑,读者应该能够理解回调在解决的问题了吧。
回调
可以将父子孙的树形图结构,解耦成子父的边结构。关键代码如下
private void DoLoadedCallFun(AssetBundleObject abObj)
{
//提取ab
if (abObj._request != null)
{
abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
abObj._request = null;
_loadingABList.Remove(abObj._hashName);
_loadedABList.Add(abObj._hashName, abObj);
}
//运行回调
foreach (var callback in abObj._callFunList)
{
callback(abObj._ab);
}
abObj._callFunList.Clear();
}
private AssetBundleObject LoadAssetBundleAsync(string _hashName, AssetBundleLoadCallBack _callFun)
{//这里只是展示代码逻辑,代码非完整
AssetBundleObject abObj = new AssetBundleObject();
abObj._hashName = _hashName;
abObj._refCount = 1;
abObj._callFunList.Add(_callFun); //保存回调
//加载依赖项
string[] dependsData = _dependsDataList[_hashName];
abObj._dependLoadingCount = dependsData.Length;
foreach(var dpAssetName in dependsData)
{
var dpObj = LoadAssetBundleAsync(dpAssetName
//这里是构造回调函数
(AssetBundle _ab) =>
{
abObj._dependLoadingCount--;
if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
{//依赖加载完,自身也加载完,回调被依赖项
DoLoadedCallFun(abObj);
}
}
);
abObj._depends.Add(dpObj);
}
return abObj;
}
private void UpdateLoad()
{//每帧调用,用于触发加载完成
if (_loadingABList.Count == 0) return;
//检测加载完的
tempLoadeds.Clear();
foreach (var abObj in _loadingABList.Values)
{
if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
{//依赖加载完,自身也加载完,回调被依赖项
tempLoadeds.Add(abObj);
}
}
//回调中有可能对_loadingABList进行操作,提取后回调
foreach (var abObj in tempLoadeds)
{
//加载完进行回调
DoLoadedCallFun(abObj);
}
}
到这里,超级复杂的依赖加载问题就解决啦,我们可以欢快地开始使用异步加载
啦!!!
异步加载已经很复杂了,如果还要在异步加载的基础上,使用同步加载,是不是感觉很头大!!!
没关系,这边会给你提供整套解决方案。
如果没有异步加载,同步加载是不是很开心地如下代码:
private AssetBundleObject LoadAssetBundleSync(string _hashName)
{
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName)) //已经加载
{
abObj = _loadedABList[_hashName];
DoDependsRef(abObj);
return abObj;
}
//创建一个加载
abObj = new AssetBundleObject();
abObj._hashName = _hashName;
abObj._refCount = 1;
string path = GetAssetBundlePath(_hashName);
abObj._ab = AssetBundle.LoadFromFile(path);
//加载依赖项
string[] dependsData = _dependsDataList[_hashName];
abObj._dependLoadingCount = 0;
foreach (var dpAssetName in dependsData)
{
var dpObj = LoadAssetBundleSync(dpAssetName);
abObj._depends.Add(dpObj);
}
_loadedABList.Add(abObj._hashName, abObj);
return abObj;
}
写出同步加载代码后,你会发现难点就一个——正在加载的节点如何强制加载完
。
我们这里有四个队列,准备队列
、加载队列
、完成队列
和销毁队列
。
销毁队列
不用管,是一个标记队列,用于延迟卸载,不影响加载逻辑完成队列
也很简单,只用增加引用计数就可以了准备队列
还没开始加载,只需要解决引用计数和依赖关系回调加载队列
正在加载中,除了解决引用计数和依赖关系回调,还要解决ab异步转同步的问题总结一下,就是三个问题——引用计数
、依赖关系回调
和ab异步转同步
引用计数
可以很简单啦,递归一下所有依赖节点,都+1就解决了。
注意:同步加载和异步加载会导致引用计数是2次,需要调用2次Unload才会卸载
依赖关系回调
需要强制手动运行被依赖项的回调函数,然后改变队列
ab异步转同步
,很幸运的,Unity提供了同步转异步的方式
在异步请求一个AssetBundle的时候,会返回一个AssetBundleCreateRequest对象,Unity的官方文档上写
AssetBundleCreateRequest.assetBundle的时候这样说:
“
Description Asset object being loaded (Read Only).
“
Note that accessing asset before isDone is true will stall the loading process.经测试,在isDone是false的时候,直接调用request.assetBundle,可以拿到同步加载的结果
好啦,现在三个问题解决啦,看代码:
private void DoLoadedCallFun(AssetBundleObject abObj)
{
//提取ab
if (abObj._request != null)
{
abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
abObj._request = null;
_loadingABList.Remove(abObj._hashName);
_loadedABList.Add(abObj._hashName, abObj);
}
//运行回调
foreach (var callback in abObj._callFunList)
{
callback(abObj._ab);
}
abObj._callFunList.Clear();
}
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName)) //已经加载
{
abObj = _loadedABList[_hashName];
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,附加引用计数
}
return abObj;
}
else if (_loadingABList.ContainsKey(_hashName)) //在加载中,异步改同步
{
abObj = _loadingABList[_hashName];
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
}
DoLoadedCallFun(abObj, false); //强制完成,回调
return abObj;
}
else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
{
abObj = _readyABList[_hashName];
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
}
string path1 = GetAssetBundlePath(_hashName);
abObj._ab = AssetBundle.LoadFromFile(path1);
_readyABList.Remove(abObj._hashName);
_loadedABList.Add(abObj._hashName, abObj);
DoLoadedCallFun(abObj, false); //强制完成,回调
return abObj;
}
好啦,到这里,同步加载也完美解决啦
下面的代码,是笔者使用的hash方式。
private string GetHashName(string _assetName)
{//读者可以自己定义hash方式,对内存有要求的话,可以hash成uint(或uint64)节省内存
return _assetName.ToLower();
}
private string GetFileName(string _hashName)
{//读者可以自己实现自己的对应关系
return _hashName + ".ab";
}
// 获取一个资源的路径
private string GetAssetBundlePath(string _hashName)
{//读者可以自己实现的对应关系,笔者这里有多语言和文件版本的处理
string lngHashName = GetHashName(LocalizationMgr.I.GetAssetPrefix() + _hashName);
if (_dependsDataList.ContainsKey(lngHashName))
_hashName = lngHashName;
return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
}
资源管理,一定逃不开的路径管理,上面的三个函数,封装了必要的路径需求,读者有需求的话,可以使用针对项目的路径管理方案,这边笔者就当抛砖引玉啦。
这边再提供一个内存优化方案,将_assetName
Hash成uint
值,这样可以没有大量字符串(依赖项配置和路径字符串)保存在内存中
public static uint GetHashName(string _assetName)
{
if (string.IsNullOrEmpty(_assetName)) return 0;
char[] bitarray = _assetName.ToCharArray();
int count = bitarray.Length;
uint hash = 0;
while (count-- > 0)
{
hash = hash * seed + (bitarray[count]);
}
return hash;
}
private string GetFileName(uint _hashName)
{//读者可以自己实现自己的对应关系
return _hashName + ".ab";
}
private string GetAssetBundlePath(string _hashName)
{
return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
}
使用上述代码, 需要LoadMainfest()
配合,还需要在AssetBundle打包导出时,将路径和依赖项路径Hash成uint,然后作为导出的文件名,具体实现参照这篇文章的导出根节点和依赖节点的GetAbName(ABNode abNode)
函数。
上文讲了那么多内容,开始放大招——资源管理器完整代码。
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class AssetBundleLoadMgr
{
public delegate void AssetBundleLoadCallBack(AssetBundle ab);
private class AssetBundleObject
{
public string _hashName;
public int _refCount;
public List<AssetBundleLoadCallBack> _callFunList = new List<AssetBundleLoadCallBack>();
public AssetBundleCreateRequest _request;
public AssetBundle _ab;
public int _dependLoadingCount;
public List<AssetBundleObject> _depends = new List<AssetBundleObject>();
}
private static AssetBundleLoadMgr _Instance = null;
public static AssetBundleLoadMgr I
{
get {
if (_Instance == null) _Instance = new AssetBundleLoadMgr();
return _Instance;
}
}
private const int MAX_LOADING_COUNT = 10; //同时加载的最大数量
private List<AssetBundleObject> tempLoadeds = new List<AssetBundleObject>(); //创建临时存储变量,用于提升性能
private Dictionary<string, string[]> _dependsDataList;
private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
private AssetBundleLoadMgr()
{
_dependsDataList = new Dictionary<string, string[]>();
_readyABList = new Dictionary<string, AssetBundleObject>();
_loadingABList = new Dictionary<string, AssetBundleObject>();
_loadedABList = new Dictionary<string, AssetBundleObject>();
_unloadABList = new Dictionary<string, AssetBundleObject>();
}
public void LoadMainfest()
{
string path = FileVersionMgr.I.GetFilePathByExist("Assets");
if (string.IsNullOrEmpty(path)) return;
_dependsDataList.Clear();
AssetBundle ab = AssetBundle.LoadFromFile(path);
if(ab == null)
{
string errormsg = string.Format("LoadMainfest ab NULL error !");
Debug.LogError(errormsg);
return;
}
AssetBundleManifest mainfest = ab.LoadAsset("AssetBundleManifest") as AssetBundleManifest;
if (mainfest == null)
{
string errormsg = string.Format("LoadMainfest NULL error !");
Debug.LogError(errormsg);
return;
}
foreach(string assetName in mainfest.GetAllAssetBundles())
{
string hashName = assetName.Replace(".ab", "");
string[] dps = mainfest.GetAllDependencies(assetName);
for (int i = 0; i < dps.Length; i++)
dps[i] = dps[i].Replace(".ab", "");
_dependsDataList.Add(hashName, dps);
}
ab.Unload(true);
ab = null;
Debug.Log("AssetBundleLoadMgr dependsCount=" + _dependsDataList.Count);
}
private string GetHashName(string _assetName)
{//读者可以自己定义hash方式,对内存有要求的话,可以hash成uint(或uint64)节省内存
return _assetName.ToLower();
}
private string GetFileName(string _hashName)
{//读者可以自己实现自己的对应关系
return _hashName + ".ab";
}
// 获取一个资源的路径
private string GetAssetBundlePath(string _hashName)
{//读者可以自己实现的对应关系,笔者这里有多语言和文件版本的处理
string lngHashName = GetHashName(LocalizationMgr.I.GetAssetPrefix() + _hashName);
if (_dependsDataList.ContainsKey(lngHashName))
_hashName = lngHashName;
return FileVersionMgr.I.GetFilePath(GetFileName(_hashName));
}
public bool IsABExist(string _assetName)
{
string hashName = GetHashName(_assetName);
return _dependsDataList.ContainsKey(hashName);
}
//同步加载
public AssetBundle LoadSync(string _assetName)
{
string hashName = GetHashName(_assetName);
var abObj = LoadAssetBundleSync(hashName);
return abObj._ab;
}
//异步加载(已经加载直接回调),每次加载引用计数+1
public void LoadAsync(string _assetName, AssetBundleLoadCallBack callFun)
{
string hashName = GetHashName(_assetName);
LoadAssetBundleAsync(hashName, callFun);
}
//卸载(异步),每次卸载引用计数-1
public void Unload(string _assetName)
{
string hashName = GetHashName(_assetName);
UnloadAssetBundleAsync(hashName);
}
private AssetBundleObject LoadAssetBundleSync(string _hashName)
{
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName)) //已经加载
{
abObj = _loadedABList[_hashName];
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,附加引用计数
}
return abObj;
}
else if (_loadingABList.ContainsKey(_hashName)) //在加载中,异步改同步
{
abObj = _loadingABList[_hashName];
abObj._refCount++;
foreach(var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
}
DoLoadedCallFun(abObj, false); //强制完成,回调
return abObj;
}
else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
{
abObj = _readyABList[_hashName];
abObj._refCount++;
foreach (var dpObj in abObj._depends)
{
LoadAssetBundleSync(dpObj._hashName); //递归依赖项,加载完
}
string path1 = GetAssetBundlePath(_hashName);
abObj._ab = AssetBundle.LoadFromFile(path1);
_readyABList.Remove(abObj._hashName);
_loadedABList.Add(abObj._hashName, abObj);
DoLoadedCallFun(abObj, false); //强制完成,回调
return abObj;
}
//创建一个加载
abObj = new AssetBundleObject();
abObj._hashName = _hashName;
abObj._refCount = 1;
string path = GetAssetBundlePath(_hashName);
abObj._ab = AssetBundle.LoadFromFile(path);
if(abObj._ab == null)
{
try
{
//同步下载解决
byte[] bytes = AssetsDownloadMgr.I.DownloadSync(GetFileName(abObj._hashName));
if (bytes != null && bytes.Length != 0)
abObj._ab = AssetBundle.LoadFromMemory(bytes);
}
catch (Exception ex)
{
Debug.LogError("LoadAssetBundleSync DownloadSync" + ex.Message);
}
}
//加载依赖项
string[] dependsData = null;
if (_dependsDataList.ContainsKey(_hashName))
{
dependsData = _dependsDataList[_hashName];
}
if (dependsData != null && dependsData.Length > 0)
{
abObj._dependLoadingCount = 0;
foreach (var dpAssetName in dependsData)
{
var dpObj = LoadAssetBundleSync(dpAssetName);
abObj._depends.Add(dpObj);
}
}
_loadedABList.Add(abObj._hashName, abObj);
return abObj;
}
private void UnloadAssetBundleAsync(string _hashName)
{
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName))
abObj = _loadedABList[_hashName];
else if (_loadingABList.ContainsKey(_hashName))
abObj = _loadingABList[_hashName];
else if (_readyABList.ContainsKey(_hashName))
abObj = _readyABList[_hashName];
if (abObj == null)
{
string errormsg = string.Format("UnLoadAssetbundle error ! assetName:{0}",_hashName);
Debug.LogError(errormsg);
return;
}
if (abObj._refCount == 0)
{
string errormsg = string.Format("UnLoadAssetbundle refCount error ! assetName:{0}", _hashName);
Debug.LogError(errormsg);
return;
}
abObj._refCount--;
foreach (var dpObj in abObj._depends)
{
UnloadAssetBundleAsync(dpObj._hashName);
}
if (abObj._refCount == 0)
{
_unloadABList.Add(abObj._hashName, abObj);
}
}
private AssetBundleObject LoadAssetBundleAsync(string _hashName, AssetBundleLoadCallBack _callFun)
{
AssetBundleObject abObj = null;
if (_loadedABList.ContainsKey(_hashName)) //已经加载
{
abObj = _loadedABList[_hashName];
DoDependsRef(abObj);
_callFun(abObj._ab);
return abObj;
}
else if(_loadingABList.ContainsKey(_hashName)) //在加载中
{
abObj = _loadingABList[_hashName];
DoDependsRef(abObj);
abObj._callFunList.Add(_callFun);
return abObj;
}
else if (_readyABList.ContainsKey(_hashName)) //在准备加载中
{
abObj = _readyABList[_hashName];
DoDependsRef(abObj);
abObj._callFunList.Add(_callFun);
return abObj;
}
//创建一个加载
abObj = new AssetBundleObject();
abObj._hashName = _hashName;
abObj._refCount = 1;
abObj._callFunList.Add(_callFun);
//加载依赖项
string[] dependsData = null;
if (_dependsDataList.ContainsKey(_hashName))
{
dependsData = _dependsDataList[_hashName];
}
if (dependsData != null && dependsData.Length > 0)
{
abObj._dependLoadingCount = dependsData.Length;
foreach(var dpAssetName in dependsData)
{
var dpObj = LoadAssetBundleAsync(dpAssetName,
(AssetBundle _ab) =>
{
if(abObj._dependLoadingCount <= 0)
{
string errormsg = string.Format("LoadAssetbundle depend error ! assetName:{0}", _hashName);
Debug.LogError(errormsg);
return;
}
abObj._dependLoadingCount--;
//依赖加载完
if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
{
DoLoadedCallFun(abObj);
}
}
);
abObj._depends.Add(dpObj);
}
}
if (_loadingABList.Count < MAX_LOADING_COUNT) //正在加载的数量不能超过上限
{
DoLoad(abObj);
_loadingABList.Add(_hashName, abObj);
}
else _readyABList.Add(_hashName, abObj);
return abObj;
}
private void DoDependsRef(AssetBundleObject abObj)
{
abObj._refCount++;
if (abObj._depends.Count == 0) return;
foreach (var dpObj in abObj._depends)
{
DoDependsRef(dpObj); //递归依赖项,加载完
}
}
private void DoLoad(AssetBundleObject abObj)
{
if (AssetsDownloadMgr.I.IsNeedDownload(GetFileName(abObj._hashName)))
{//这里是关联下载逻辑,可以实现异步下载再异步加载
AssetsDownloadMgr.I.DownloadAsync(GetFileName(abObj._hashName),
() =>
{
string path = GetAssetBundlePath(abObj._hashName);
abObj._request = AssetBundle.LoadFromFileAsync(path);
if (abObj._request == null)
{
string errormsg = string.Format("LoadAssetbundle path error ! assetName:{0}", abObj._hashName);
Debug.LogError(errormsg);
}
}
);
}
else
{
string path = GetAssetBundlePath(abObj._hashName);
abObj._request = AssetBundle.LoadFromFileAsync(path);
if (abObj._request == null)
{
string errormsg = string.Format("LoadAssetbundle path error ! assetName:{0}", abObj._hashName);
Debug.LogError(errormsg);
}
}
}
private void DoLoadedCallFun(AssetBundleObject abObj, bool isAsync = true)
{
//提取ab
if(abObj._request != null)
{
abObj._ab = abObj._request.assetBundle; //如果没加载完,会异步转同步
abObj._request = null;
_loadingABList.Remove(abObj._hashName);
_loadedABList.Add(abObj._hashName, abObj);
}
if (abObj._ab == null)
{
string errormsg = string.Format("LoadAssetbundle _ab null error ! assetName:{0}", abObj._hashName);
string path = GetAssetBundlePath(abObj._hashName);
errormsg += "\n File " + File.Exists(path) + " Exists " + path;
try
{//尝试读取二进制解决
if(File.Exists(path))
{
byte[] bytes = File.ReadAllBytes(path);
if (bytes != null && bytes.Length != 0)
abObj._ab = AssetBundle.LoadFromMemory(bytes);
}
}
catch (Exception ex)
{
Debug.LogError("LoadAssetbundle ReadAllBytes Error " + ex.Message);
}
if (abObj._ab == null)
{
//同步下载解决
byte[] bytes = AssetsDownloadMgr.I.DownloadSync(GetFileName(abObj._hashName));
if (bytes != null && bytes.Length != 0)
abObj._ab = AssetBundle.LoadFromMemory(bytes);
if (abObj._ab == null)
{//同步下载还不能解决,移除
if (_loadedABList.ContainsKey(abObj._hashName)) _loadedABList.Remove(abObj._hashName);
else if (_loadingABList.ContainsKey(abObj._hashName)) _loadingABList.Remove(abObj._hashName);
Debug.LogError(errormsg);
if (isAsync)
{//异步下载解决
AssetsDownloadMgr.I.AddDownloadSetFlag(GetFileName(abObj._hashName));
}
}
}
}
//运行回调
foreach (var callback in abObj._callFunList)
{
callback(abObj._ab);
}
abObj._callFunList.Clear();
}
private void UpdateLoad()
{
if (_loadingABList.Count == 0) return;
//检测加载完的
tempLoadeds.Clear();
foreach (var abObj in _loadingABList.Values)
{
if (abObj._dependLoadingCount == 0 && abObj._request != null && abObj._request.isDone)
{
tempLoadeds.Add(abObj);
}
}
//回调中有可能对_loadingABList进行操作,提取后回调
foreach (var abObj in tempLoadeds)
{
//加载完进行回调
DoLoadedCallFun(abObj);
}
}
private void DoUnload(AssetBundleObject abObj)
{
//这里用true,卸载Asset内存,实现指定卸载
if(abObj._ab == null)
{
string errormsg = string.Format("LoadAssetbundle DoUnload error ! assetName:{0}", abObj._hashName);
Debug.LogError(errormsg);
return;
}
abObj._ab.Unload(true);
abObj._ab = null;
}
private void UpdateUnLoad()
{
if (_unloadABList.Count == 0) return;
tempLoadeds.Clear();
foreach (var abObj in _unloadABList.Values)
{
if (abObj._refCount == 0 && abObj._ab != null)
{//引用计数为0并且已经加载完,没加载完等加载完销毁
DoUnload(abObj);
_loadedABList.Remove(abObj._hashName);
tempLoadeds.Add(abObj);
}
if (abObj._refCount > 0)
{//引用计数加回来(销毁又瞬间重新加载,不销毁,从销毁列表移除)
tempLoadeds.Add(abObj);
}
}
foreach(var abObj in tempLoadeds)
{
_unloadABList.Remove(abObj._hashName);
}
}
private void UpdateReady()
{
if (_readyABList.Count == 0) return;
if (_loadingABList.Count >= MAX_LOADING_COUNT) return;
tempLoadeds.Clear();
foreach (var abObj in _readyABList.Values)
{
DoLoad(abObj);
tempLoadeds.Add(abObj);
_loadingABList.Add(abObj._hashName, abObj);
if (_loadingABList.Count >= MAX_LOADING_COUNT) break;
}
foreach (var abObj in tempLoadeds)
{
_readyABList.Remove(abObj._hashName);
}
}
public void Update()
{
UpdateLoad();
UpdateReady();
UpdateUnLoad();
}
}
整篇文章到这里就结束啦!!!如果对上述的逻辑不是很理解的话,没有关系,上述代码可以无缝嵌入任何一个Unity游戏——就是这么666。