版權聲明:本文為博主原創文章,遵循 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