Unity开发(三) AssetBundle同步异步引用计数资源加载管理器

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wowo1gt/article/details/100561236
文章目录
前言
AssetBundle加载技术选型
加载去协程化
Update才是王道
外部接口
加载依赖关系配置
加载节点数据结构
依赖加载——递归&引用计数&队列&回调
我要异步加载和同步加载一起用
资源路径管理——字符串转hash
大招——资源管理器完整代码
前言
这篇文章内容巨多,逻辑也复杂,花了4天写出来。(写博客还是费时间啊)
很多设计和逻辑,在脑子中是很清晰的,但用文字表述就会显得很复杂,没有图文对照就更难理解了。

AssetBundle加载技术选型
AssetBundle加载有三套接口,WWW,UnityWebRequest和AssetBundle,大部分文章都推荐AssetBundle,本人也推荐。

关于AssetBundle的加载原理和用法之类的基础知识读者自己百度学习,这边就不进行大量描述了

前两者都要经历将整个文件的二进制流下载或读取到内存中,然后对这段内存文件进行ab资源的读取解析操作,而AssetBundle可以只读取存储于本地的ab文件的头部部分,在需要的情况下,读取ab中的数据段部分(Asset资源)。

所以AssetBundle相对的优势是

不进行下载(不占用下载缓存区内存)
不读取整个文件到内存(不占用原始文件二进制内存)
读取非压缩或LZ4的ab,只读取ab的文件头(约5kb/个)
同步异步加载并行可用
所以,从内存和效率方面,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
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其实协程本质,就是保留现场的回调函数,内部机制也是update的每帧遍历(具体参见IEnumerator原理)。

Update才是王道
既然是加载资源,那必然会有队列,笔者这边依据需求和优化要求,设计成四个队列,准备队列、加载队列、完成队列和销毁队列。

UpdateReady
UpdateLoad
UpdateUnLoad
准备队列
加载队列
完成队列
销毁队列
代码如下

private Dictionary<string, AssetBundleObject> _readyABList; //预备加载的列表
private Dictionary<string, AssetBundleObject> _loadingABList; //正在加载的列表
private Dictionary<string, AssetBundleObject> _loadedABList; //加载完成的列表
private Dictionary<string, AssetBundleObject> _unloadABList; //准备卸载的列表
1
2
3
4
队列之间,队列成员的转移需要一个触发点,而这样的触发点如果都写在加载和销毁逻辑里,耦合度过高,而且逻辑复杂还容易出错。

TIP:为什么没有设计异常队列?

一般资源加载,都是默认资源是存在的
资源如果不存在,一定是策划没有把资源放进去(嗯,一定是这样)
设计上是加载了总依赖关系的Mainfest,是对文件存在性可以进行判断的
从性能的角度,通过File.exists()来判断文件存在性,是效率低下的方式
代码中对异常是有处理的,会有重复加载,下载和修复完整性的逻辑
笔者很喜欢的一种设计,就是通过Update来降低耦合度,这种方式代码清晰,逻辑简单,但缺点也很明显,丢失原始现场。

回到本篇文章,当然是通过Update来运行逻辑,如下

Yes
Yes
Yes
Update
UpdateLoad
UpdateReady
UpdateUnLoad
遍历正在加载的ab是否加载完成
正在加载的ab总数是否低于上限
遍历引用计数为0的ab是否销毁
运行回调函数
创建新的加载
销毁ab
TIP:为什么Update里三个函数的运行顺序跟队列转移顺序不一样?

UpdateReady在UpdateLoad后面,可以实现当前帧就创建新的加载,否则要等到下一帧
UpdateUnLoad放最后,是因为正在加载的资源要等到加载完才能卸载
外部接口
根据上面的逻辑,很容易设计下面的接口逻辑

外部接口
加载依赖关系
异步
同步
卸载
刷新
每帧调用
LoadMainfest
LoadAsync
LoadSync
Unload
Update
加载管理器
主线程
加载依赖关系配置
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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这部分,大部分游戏都大同小异,就是将配置转化成类结构。注意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>(); //依赖项
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
加载节点的数据结构不复杂,看代码就很容易理解。

依赖加载——递归&引用计数&队列&回调
依赖加载,是ab加载逻辑里最难最复杂最容易出bug的地方,也是本文的难点。

难点为一下几点:

加载时,root节点和depend节点引用计数的正确增加
卸载时,root节点和depend节点引用计数的正确减少
还未加载,准备加载,正在加载,已经加载节点关系处理
节点加载完成,回调逻辑的高效和正确性
我们来一一分解
首先,看一下ab节点的引用计数要实现的逻辑

1图-初始
2图-加载A
3图-加载E
A+0
B+0
C+0
D+0
E+0
A+1
B+0
C+1
D+1
E+0
A+1
B+0
C+1
D+2
E+1
4图-卸载A
5图-加载B
6图-卸载E
A+0
B+0
C+0
D+1
E+1
A+0
B+1
C+1
D+2
E+1
A+0
B+1
C+1
D+1
E+0
注: 上图显示加载和销毁都需要递归标记依赖节点的依赖节点
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
上述代码构造了引用计数,递归,加入队列。理解起来其实不难,难在写出符合设想的逻辑代码。

上面构造了递归引用计数的逻辑,我们再加入队列的逻辑。

队列逻辑在上文已经描述过了,总结几个要点

当一个节点引用计数由0变为1时,需要创建ab节点,加入准备队列或加载队列。
当一个节点加载完ab,将其加入完成队列
当一个节点引用计数由1变为0时,需要加入销毁队列。
对应到开启异步加载和销毁时,代码如下

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);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
从这里,上文已经完成了整个异步加载的逻辑,已经实现创建到销毁的代码。但异步加载还有一个问题没有解决——判读ab节点加载完成。

我们需要在ab节点及其依赖ab节点都加载完后,告诉上层调用逻辑,ab资源加载完了。简单地做法就是,在Update里逻辑判断一个节点及其子节点都加载完了。我们会有下面这样的代码结构

图1-递归判定
判定
判定
判定
A+1
B+0
C+1
D+1
E+1
注:圆角方形表示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;
}
1
2
3
4
5
6
7
8
9
10
很明显的弊端,上述代码需要关心子依赖节点以及孙依赖节点,这样的代码不管是效率还是设计,都不是一种优秀的方式。

那么有没有一种更好的方式呢,笔者提供一种解耦的方式——回调
我们先用图示表示加载A和B到完成的整个过程

图1-同时加载A和B
图2-D加载完
图3-C加载完
回调
A+1
B+1
C+2
D+2
E+2
A+1
B+1
C+2
D+2
E+2
A+1
B+1
C+2
D+2
E+2
图4-B加载完
图5-E加载完
图6-A加载完
回调
回调
回调
A+1
B+1
C+2
D+2
E+2
A+1
B+1
C+2
D+2
E+2
A+1
B+1
C+2
D+2
E+2
注:圆角方形表示ab自身加载完成,箭头表示依赖关系
上图,会按以下回调逻辑

同时加载A和B,标记引用计数
D自身加载完,会回调C;
C自身没有加载完,然后C会记录子依赖加载情况
C自身加载完,但子依赖没加载完,不操作
B自身加载完,但子依赖没加载完,不操作
E自身加载完,会回调C;
C的子依赖加载完了,C自己也加载完了,回调A和B;
A自己没加载完,不操作;
B自己已经加载完了,子依赖也加载完了,B完成加载
A自身加载完,子依赖已经加载完了,A完成加载
按照上述逻辑,读者应该能够理解回调在解决的问题了吧。

回调可以将父子孙的树形图结构,解耦成子父的边结构。关键代码如下

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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
到这里,超级复杂的依赖加载问题就解决啦,我们可以欢快地开始使用异步加载啦!!!

我要异步加载和同步加载一起用
异步加载已经很复杂了,如果还要在异步加载的基础上,使用同步加载,是不是感觉很头大!!!
没关系,这边会给你提供整套解决方案。
如果没有异步加载,同步加载是不是很开心地如下代码:

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
写出同步加载代码后,你会发现难点就一个——正在加载的节点如何强制加载完。
我们这里有四个队列,准备队列、加载队列、完成队列和销毁队列。

销毁队列不用管,是一个标记队列,用于延迟卸载,不影响加载逻辑
完成队列也很简单,只用增加引用计数就可以了
准备队列还没开始加载,只需要解决引用计数和依赖关系回调
加载队列正在加载中,除了解决引用计数和依赖关系回调,还要解决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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
好啦,到这里,同步加载也完美解决啦

资源路径管理——字符串转hash
下面的代码,是笔者使用的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));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
资源管理,一定逃不开的路径管理,上面的三个函数,封装了必要的路径需求,读者有需求的话,可以使用针对项目的路径管理方案,这边笔者就当抛砖引玉啦。

这边再提供一个内存优化方案,将_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));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用上述代码, 需要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();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
整篇文章到这里就结束啦!!!如果对上述的逻辑不是很理解的话,没有关系,上述代码可以无缝嵌入任何一个Unity游戏——就是这么666。
————————————————
版权声明:本文为CSDN博主「无为战士」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wowo1gt/article/details/100561236

posted @ 2019-10-11 14:22  00000000O  阅读(...)  评论(...编辑  收藏