1. 概況
這個(gè)實(shí)現(xiàn)來(lái)自于我的個(gè)人開(kāi)源項(xiàng)目 UnityGameWheels(以下簡(jiǎn)稱 UGW)组力,并在實(shí)際生產(chǎn)中已有一定的應(yīng)用垛叨。UGW 的代碼地址:
- Core: 純 C# 部分柳琢。其中資產(chǎn)管理和更新相關(guān)內(nèi)容位于 Asset。
- Unity: 和 Unity 結(jié)合的部分。其中資產(chǎn)管理和更新相關(guān)內(nèi)容位于 Asset银受,編輯器相關(guān)位于編輯器。
- Demo:一些示例代碼鸦采。
此間一些設(shè)計(jì)方式參考了我一位老友的 GameFramework宾巍。此外,玩具代碼頗多(比如有個(gè)玩具版 IoC 容器)渔伯,請(qǐng)見(jiàn)諒并無(wú)視顶霞。
1.1. 企圖
- 希望為移動(dòng)平臺(tái)(主要是 iOS 和 Android 系統(tǒng))實(shí)現(xiàn)具有一定通用性的資產(chǎn)管理與更新系統(tǒng)。
- 在使用時(shí)不必過(guò)多顧及資產(chǎn)包(Asset Bundle)锣吼,而是關(guān)注單個(gè)資產(chǎn)(Asset)选浑。
- 對(duì)更新的內(nèi)容,做出一定程度的分組玄叠,實(shí)現(xiàn)邊玩邊下古徒。
1.2. 名詞
- 資產(chǎn)和資產(chǎn)包:即 Unity 中的 Asset 和 AssetBundle。
- 兩種模式:
- 編輯器模式:在編輯器下開(kāi)發(fā)時(shí)读恃,通過(guò)
UnityEditor.AssetDatabase
中的方法直接訪問(wèn)資產(chǎn)文件描函。 - 資產(chǎn)包模式:構(gòu)建資產(chǎn)包使用的模式。這種模式為后文的主要討論對(duì)象狐粱。
- 編輯器模式:在編輯器下開(kāi)發(fā)時(shí)读恃,通過(guò)
- 資源(Resource):在 Core 中指資產(chǎn)包舀寓。這用法也來(lái)自 GameFramework(但是我后悔這么命名了 :P)。
- 索引(Index)文件:專指收集肌蜻、記錄資產(chǎn)和資產(chǎn)包基本信息(類似于 Unity 提供的資產(chǎn)包 manifest 文件的功能)的文件互墓。
- CR:安裝包索引文件。
- RR:遠(yuǎn)端索引文件蒋搜。
- PR:持久化索引文件篡撵。
- Manifest 文件:Unity 構(gòu)建資產(chǎn)包時(shí)生成的數(shù)據(jù)文件判莉,包含資產(chǎn)包和資產(chǎn)的關(guān)系以及資產(chǎn)包間的依賴關(guān)系。
- 資產(chǎn)系統(tǒng):指本文所描述的資產(chǎn)管理和更新系統(tǒng)育谬。
1.3. 主要組成部分
- Core(純 C#)部分
-
AssetService
類(實(shí)現(xiàn)IAssetService
接口)是資產(chǎn)包模式的主入口券盅,提供資產(chǎn)管理與更新的入口。- 通過(guò)
Prepare
方法來(lái)進(jìn)行資產(chǎn)系統(tǒng)的準(zhǔn)備工作膛檀。 - 通過(guò)
CheckUpdate
方法來(lái)檢查是否需要進(jìn)行更新以及哪些內(nèi)容需要更新锰镀。 - 通過(guò)
IResourceUpdater
接口(實(shí)現(xiàn)為AssetService.ResourceUpdater
)來(lái)進(jìn)行資產(chǎn)包(資源)的更新。 - 通過(guò)
LoadAsset
,LoadSceneAsset
,UnloadAsset
等方法來(lái)加載和卸載資源咖刃。
- 通過(guò)
-
- Unity 部分:
- Asset 文件夾提供依賴于 Unity 庫(kù)的實(shí)現(xiàn)泳炉,和編輯器模式下使用的
IAssetService
的實(shí)現(xiàn)。 - Editor/AssetBundle 文件夾提供構(gòu)建資產(chǎn)包相關(guān)的編輯器工具嚎杨。
- Asset 文件夾提供依賴于 Unity 庫(kù)的實(shí)現(xiàn)泳炉,和編輯器模式下使用的
2. 一些重要概念
2.1. 資產(chǎn)包的分組
在 Unity 中花鹅,每個(gè)資產(chǎn)文件至多顯式打入一個(gè)資產(chǎn)包,所以對(duì)資產(chǎn)包分組(Group)枫浙,就相當(dāng)于對(duì)顯式打入資產(chǎn)包的每個(gè)資產(chǎn)都分組刨肃。為什么要分組呢?一方面箩帚,是為了按組為單位做資產(chǎn)包更新之景;另一方面,是控制依賴關(guān)系的復(fù)雜度膏潮。
使用非負(fù)整數(shù)來(lái)標(biāo)記每個(gè)資產(chǎn)包的組號(hào)锻狗。
- 0 代表公共組,可以被其他組依賴焕参。
- 正整數(shù)代表其他組轻纪,不允許組間依賴,但是都可以依賴 0 組叠纷。
在分組更新的基礎(chǔ)上刻帚,這樣的限制帶來(lái)的好處是,不需要為了更新一個(gè)分組的內(nèi)容而大量更新其他分組的內(nèi)容涩嚣。當(dāng)然崇众,這種選擇同時(shí)也是一種局限。
在應(yīng)用啟動(dòng)過(guò)程中航厚,“正式”進(jìn)入游戲之前顷歌,應(yīng)將 0 組的內(nèi)容更新完畢。
2.2. 索引文件
Unity 自身在構(gòu)建資產(chǎn)包時(shí)幔睬,提供了 manifest 文件眯漩,用于指明每個(gè)資產(chǎn)包中包含哪些資產(chǎn)以及依賴于哪些其他資產(chǎn)包。索引文件,在此基礎(chǔ)上加入了包括資產(chǎn)之間的依賴關(guān)系赦抖、資產(chǎn)包分組(后文解釋)在內(nèi)的若干其他信息舱卡。編輯器工具構(gòu)建資產(chǎn)包時(shí)會(huì)生成三個(gè)文件夾:
- Client:用于放在 StreamingAssets 中、打入首包的資產(chǎn)包队萤;
- ClientFull:用于放在 StreamingAssets 中的全量資產(chǎn)包轮锥,適合調(diào)試或者關(guān)閉更新功能的情形。
- Server:用于放在 CDN 上用于更新的全量資產(chǎn)包要尔。
這三個(gè)文件夾中各自會(huì)有一個(gè)索引文件舍杜。前兩者自然格式一致,稱為安裝包索引文件(記為 CR盈电,其中 C 代表 Client, R 代表 Resource)蝴簇,隨首包發(fā)布杯活。Server 文件夾中的索引文件稱為遠(yuǎn)端索引文件(記為 RR匆帚,其中第一個(gè) R 代表 Remote)。在資源更新和使用的過(guò)程中旁钧,本地持久化目錄中會(huì)存放一份索引文件吸重,稱為持久化索引文件(記為 PR,其中 P 代表 Persistent)歪今,它記載的是本地保存的那些資產(chǎn)包的信息嚎幸。
注意,在 Server 文件夾中的每個(gè)文件都會(huì)后綴它自身的 CRC-32 校驗(yàn)和寄猩,用于下載之后的校驗(yàn)嫉晶。
2.3. 版本號(hào)
資產(chǎn)系統(tǒng)使用的資產(chǎn)包版本號(hào)包括兩部分,是由應(yīng)用程序版本號(hào) VerApp (其實(shí)是 UnityEngine.Application.version
的值)和資源內(nèi)部版本號(hào) VerRes 拼接而成田篇,對(duì)于每一個(gè) VerApp替废,在每個(gè)平臺(tái)上,打包的時(shí)候 VerRes 最好從 1 開(kāi)始自增泊柬。如 VerApp 為 1.0.1椎镣,在這個(gè)應(yīng)用程序版本下,Android 平臺(tái)第 19 次資產(chǎn)包構(gòu)建兽赁,其版本號(hào)為 1.0.1.19状答。如果應(yīng)用程序版本升為 1.1.0,則再度打 Android 資源包的時(shí)候版本號(hào)就是 1.1.0.1刀崖。這也是后文講的資產(chǎn)包構(gòu)建器的默認(rèn)行為惊科。
應(yīng)用程序運(yùn)行時(shí),如果開(kāi)啟了資源更新亮钦,則本系統(tǒng)只是根據(jù)輸入的信息來(lái)判定應(yīng)該下載哪個(gè) RR译断,而不會(huì)去檢查版本的新舊。標(biāo)準(zhǔn)的做法或悲,是應(yīng)用程序從某個(gè)服務(wù)器獲取當(dāng)前 VerApp 對(duì)應(yīng)的最新的 VerRes孙咪,以及相應(yīng)的文件尺寸堪唐、CRC-32 等信息,來(lái)判定是否需要下載這個(gè)版本的 RR翎蹈。
3. 更新資產(chǎn)包
3.1. 初始化和準(zhǔn)備階段
構(gòu)造 AssetService
對(duì)象時(shí)需要傳入一些配置信息淮菠,包括但不限于 CDN 服務(wù)器的根目錄、同時(shí)進(jìn)行的資產(chǎn)加載任務(wù)數(shù)量限制荤堪、同時(shí)進(jìn)行的資產(chǎn)包加載任務(wù)數(shù)量限制等內(nèi)容合陵。
系統(tǒng)初始化之后,通過(guò) AssetService.Prepare
方法進(jìn)行的準(zhǔn)備工作澄阳,其實(shí)就是要把 CR 和 PR 從各自所在的文件系統(tǒng)中載入內(nèi)存拥知。CR 是必須要存在的,而 PR 一開(kāi)始的時(shí)候不存在碎赢,就認(rèn)為存在一個(gè)空的 PR低剔。
3.2. 更新檢測(cè)階段
在準(zhǔn)備階段完成之后(這要是都沒(méi)成功就別玩兒了),就要通過(guò) AssetService.CheckUpdate
來(lái)檢測(cè)是否有需要更新的內(nèi)容肮塞。這里需要傳入一個(gè) AssetIndexRemoteFileInfo
(索引文件信息)對(duì)象襟齿,是使用者從相關(guān)服務(wù)器獲取的關(guān)于 RR 的信息,其中包括如下一些字段:
<img src='remote_index_info.jpg' height=150/>
其中 InternalAssetVersion
就是前面所說(shuō)的 VerRes枕赵,指這個(gè) RR 對(duì)應(yīng)的資產(chǎn)包版本猜欺,Crc32
是該 RR 的 CRC-32 校驗(yàn)和,FileSize
是該文件的大锌酱堋(字節(jié))开皿。后面這兩個(gè)字段都是為了下載之后的校驗(yàn)。
更新檢測(cè)又有幾種情況篮昧。
- 如果關(guān)閉了更新赋荆,則直接使用 CR 作為 PR。此時(shí)恋谭,認(rèn)為安裝包中 StreamingAssets 目錄下的內(nèi)容是完整可用的(即從前述之 ClientFull 文件夾復(fù)制而來(lái)糠睡,如果之前下載了任何資源,我們都認(rèn)為是沒(méi)用的疚颊。
- 如果打開(kāi)更新狈孔,且本地緩存的 RR 的
Crc32
和FileSize
均和AssetIndexRemoteFileInfo
中提供的數(shù)據(jù)一致,說(shuō)明不需要從服務(wù)器下載 RR材义,用本地緩存的即可均抽。 - 其余情況,需要從遠(yuǎn)端下載 RR其掂。UGW 中有支持文件下載系統(tǒng)的實(shí)現(xiàn)油挥,超出本文范疇,不贅述。
對(duì)于上述后兩種情形深寥,系統(tǒng)會(huì)對(duì) CR, RR, PR 做三方比較攘乒,來(lái)決定哪些資產(chǎn)包是需要下載的,哪些資產(chǎn)包是需要(從持久化目錄刪除的)惋鹅。具體地:
- RR 中沒(méi)有的資產(chǎn)包(說(shuō)明已經(jīng)沒(méi)用了)则酝,如果 PR 中有,則應(yīng)該從本地持久化目錄中刪除闰集。
- RR 中有和 CR 中相同(通過(guò)比較 Unity 生成的 Hash 值和文件尺寸來(lái)決定)的資產(chǎn)包沽讹,則刪去 PR 中包含的那個(gè)版本(如果有的話)。
- 對(duì) RR 中有武鲁,但是 CR 中缺少或內(nèi)容不同(通過(guò)比較 Unity 生成的 Hash 值和文件尺寸來(lái)決定)的資產(chǎn)包爽雄,需要更新。
在三方比較的同時(shí)沐鼠,系統(tǒng)還會(huì)對(duì)每個(gè)資產(chǎn)包分組構(gòu)造資產(chǎn)包更新摘要信息挚瘟。這摘要由 ResourceGroupUpdateSummary
類描述,包含其所指向的資產(chǎn)包分組中的資產(chǎn)包總量迟杂、剩余下載量等信息刽沾。這些摘要對(duì)象將用于后面的資產(chǎn)包更新本慕。
3.3. 更新
前述準(zhǔn)備工作完成后排拷,就可以使用 AssetService.ResourceUpdater
更新器對(duì)象進(jìn)行更新了,通過(guò)它(實(shí)現(xiàn) IResourceUpdater
接口)可以:
- 獲取可用的資產(chǎn)包分組都有哪些锅尘。
- 對(duì)給定的資產(chǎn)包分組监氢,獲取其中資源狀態(tài)(需要更新、正在更新藤违、已經(jīng)最新)浪腐。
- 對(duì)某一組的資產(chǎn)包開(kāi)始、停止更新顿乒;
- 通過(guò)前述
ResourceGroupUpdateSummary
類议街,獲取各組資產(chǎn)包更新進(jìn)度和狀態(tài)(是否在更新、是否已經(jīng)最新等)璧榄。
更新資產(chǎn)包的過(guò)程中特漩,會(huì)更新 PR 中的內(nèi)容并在適當(dāng)?shù)臅r(shí)候保存到持久化目錄中。對(duì)于每個(gè)資產(chǎn)包分組骨杂,一定要全部更新完才可使用其中的內(nèi)容涂身。
4. 使用資產(chǎn)
資產(chǎn)系統(tǒng)中提供了一些輔助方法,來(lái)判定資產(chǎn)是否已經(jīng)可以使用搓蚪,也就是判斷資產(chǎn)的存在性蛤售、以及所屬的資產(chǎn)包分組是否已經(jīng)更新完畢。在此基礎(chǔ)上,使用者可以使用(邏輯層面的)加載悴能、卸載接口來(lái)使用和釋放資產(chǎn)揣钦。
4.1. 加載接口與資產(chǎn)訪問(wèn)器
AssetService
提供 LoadAsset
和 LoadSceneAsset
方法來(lái)加載一般資產(chǎn)和場(chǎng)景資產(chǎn)。鑒于后者沒(méi)有進(jìn)行仔細(xì)測(cè)試漠酿,此處暫時(shí)僅對(duì)前者做出說(shuō)明拂盯。LoadAsset
的函數(shù)簽名為
IAssetAccessor LoadAsset(string assetPath, LoadAssetCallbackSet callbackSet, object context);
使用者將資產(chǎn)路徑(從 "Assets/" 開(kāi)始)、回調(diào)函數(shù)和可選的自定義上下文對(duì)象傳入记靡,即可同步地獲得一個(gè) IAssetAccessor
谈竿,即資產(chǎn)訪問(wèn)器(簡(jiǎn)稱 AA)。AA 的引入摸吠,是由于加載資產(chǎn)操作在概念上是異步的(盡管由于內(nèi)部緩存等原因可能實(shí)際上是同步完成的)空凸。如果在加載未完成的情況下,使用者不想用這個(gè)資產(chǎn)了寸痢,通過(guò)這個(gè)訪問(wèn)器可以卸載資產(chǎn)呀洲。通過(guò) IAssetAccessor
接口,使用者可以獲取資產(chǎn)路徑啼止、資產(chǎn)對(duì)象(如果已經(jīng)加載完成)以及其狀態(tài)道逗。
一般情況下,任何使用某一資產(chǎn)的代碼献烦,都應(yīng)通過(guò) LoadAsset
獲得一個(gè)該資產(chǎn)的訪問(wèn)器滓窍。資產(chǎn)和訪問(wèn)器是一對(duì)多的關(guān)系。
4.2. 卸載接口
AssetService
提供 UnloadAsset
方法來(lái)(從邏輯上)卸載資產(chǎn)巩那。
void UnloadAsset(IAssetAccessor assetAccessor);
卸載資產(chǎn)時(shí)吏夯,只需要傳入 AA 即可。要注意即横,一個(gè)資產(chǎn)訪問(wèn)器只允許卸載一次噪生。卸載之后,就不可再使用/引用這個(gè) AA 對(duì)象东囚,否則可能造成很難查找的 bug跺嗽。
4.3. 內(nèi)部實(shí)現(xiàn)的基本數(shù)據(jù)模型
在 AssetService
內(nèi)部,用資產(chǎn)緩存(AssetService.Loader.AssetCache
內(nèi)部類页藻,簡(jiǎn)稱 ACache)來(lái)描述一個(gè)資產(chǎn)桨嫁,用資產(chǎn)包緩存(AssetService.Loader.ResourceCache
內(nèi)部類,簡(jiǎn)稱 RCache)來(lái)描述一個(gè)資產(chǎn)包惕橙。這兩種緩存內(nèi)部都保存了自己代表的資產(chǎn)(包)的引用計(jì)數(shù)瞧甩。
首先,一個(gè) ACache 可對(duì)應(yīng)多個(gè)資產(chǎn)訪問(wèn)器弥鹦。每個(gè) AA 都綁定一個(gè) ACache肚逸,ACache 的狀態(tài)變化會(huì)反應(yīng)到訪問(wèn)器中爷辙。
其次,ACache 內(nèi)部會(huì)記錄它所代表的資產(chǎn)依賴于哪些其他資產(chǎn)和資產(chǎn)包(從索引文件 PR 中獲得)朦促,這些信息用來(lái)維護(hù) ACache 和 RCache 的引用計(jì)數(shù)膝晾,最終決定資產(chǎn)和資產(chǎn)包的何時(shí)釋放。這里要注意务冕,單獨(dú)看 ACache 的時(shí)候血当,它們構(gòu)成有向無(wú)環(huán)圖(即不允許資產(chǎn)間的依賴構(gòu)成環(huán)路)。而即使有資產(chǎn)包分組間的依賴關(guān)系限制禀忆,和資產(chǎn)間不允許依賴成環(huán)路的限制臊旭,RCache 之間仍然可能構(gòu)成環(huán)路,如下圖所示(實(shí)現(xiàn)代表依賴關(guān)系箩退,虛線代表資產(chǎn)和資產(chǎn)包的從屬關(guān)系)离熏。
由于上圖中資產(chǎn) a 依賴于資產(chǎn) c,c 又依賴于依賴于資產(chǎn) b戴涝,而 a, b 屬于資產(chǎn)包 x滋戳,c 屬于資產(chǎn)包 y,因此 x 和 y 是相互依賴的啥刻。
注意:AA奸鸯、ACache、RCache 實(shí)際上都有相應(yīng)的對(duì)象池來(lái)管理可帽,以便減少運(yùn)行時(shí)的 GC Alloc娄涩。
4.4. 加載資產(chǎn)的過(guò)程
當(dāng)嘗試(通過(guò)文件路徑)加載一個(gè)資產(chǎn)的時(shí)候(即調(diào)用 AssetService.LoadAsset
方法時(shí)),如果沒(méi)有相應(yīng)的 ACache 對(duì)象蘑拯,則從對(duì)象池獲取一個(gè)或創(chuàng)建一個(gè)钝满;否則兜粘,這資產(chǎn)應(yīng)該已經(jīng)被要求加載過(guò)申窘,直接使用已有的 ACache 對(duì)象即可。不論哪種情況孔轴,一個(gè) AA 將和這個(gè) ACache 綁定(并增加 ACache 的引用計(jì)數(shù)使之一定為正的)并同步返回剃法。
ACache 初始化的時(shí)候,會(huì)做以下事情:
- 遞歸的初始化它依賴的資產(chǎn)的 ACache(如果需要的話)路鹰,增加后者的引用計(jì)數(shù)贷洲,并觀察后者的狀態(tài)變化。由于 ACache 構(gòu)成有向無(wú)環(huán)圖晋柱,所以簡(jiǎn)單遞歸即可完成這步操作优构。
- 初始化自身指向的資產(chǎn)所在的資產(chǎn)包的 RCache 對(duì)象(如果需要的話),增加后者的引用計(jì)數(shù)雁竞,并作為后者狀態(tài)變化的觀察者钦椭。
- 從自身所屬資產(chǎn)包的 RCache 對(duì)象出發(fā)拧额,在 RCache 構(gòu)成的圖結(jié)構(gòu)中做遍歷,增加過(guò)程中每個(gè) RCache 的引用計(jì)數(shù)彪腔。
由于依賴關(guān)系相關(guān)問(wèn)題都在 ACache 中處理侥锦,RCache 的業(yè)務(wù)相對(duì)簡(jiǎn)單,只是負(fù)責(zé)自身指向的資產(chǎn)包的
加載和發(fā)送狀態(tài)變化的通知給觀察者德挣。
ACache 會(huì)等待自己代表的資產(chǎn)所屬的資產(chǎn)包的 RCache 加載完成恭垦,以及自己依賴的其他 ACache 加載完成,之后再加載自身代表的資產(chǎn)。于是窟赏,只要一個(gè) ACache 加載完成(其資產(chǎn)對(duì)象對(duì)所有綁定到自身的 AA 都已可用)囱晴,它所依賴的(顯式打資產(chǎn)包的)資產(chǎn)都加載完成了,于是相關(guān)聯(lián)的資產(chǎn)包也是加載完成了的建芙。
使用者需要注意:
- 本資產(chǎn)系統(tǒng)中,加載失敗即為錯(cuò)誤情況懂扼,不可繼續(xù)使用禁荸。使用者在加載一個(gè)資產(chǎn)時(shí),需要確定它是可用的阀湿,比如資產(chǎn)本身是否存在赶熟、所在資產(chǎn)包分組是否更新完畢等。
- 某些 Android 設(shè)備上陷嘴,文件 IO 很容易出現(xiàn)問(wèn)題映砖,盡管 Unity 層的實(shí)現(xiàn)(
ResourceLoadingTaskImpl
類)增加了重試機(jī)制,仍然可能在從文件創(chuàng)建資產(chǎn)包的時(shí)候失斣职ぁ(連續(xù)失敗多次)邑退。目前只能降低同時(shí)加載的資產(chǎn)包的數(shù)量限制來(lái)減少出問(wèn)題的概率。
4.5. 卸載資產(chǎn)的過(guò)程
卸載資產(chǎn)時(shí)(AssetService.UnloadAsset
方法)劳澄,使用者進(jìn)行的操作實(shí)際上是歸還 AA 對(duì)象地技,歸還時(shí)不需要在意真實(shí)的資產(chǎn)是否仍處于正在被加載的狀態(tài)。資產(chǎn)系統(tǒng)會(huì)清理 AA 內(nèi)部保存的回調(diào)(通過(guò) AssetService.LoadAsset
方法傳入)秒拔,以防止在 AA 被完全清理之前恰好有回調(diào)發(fā)生莫矗。此時(shí)對(duì)于使用者,這個(gè) AA 對(duì)象已經(jīng)失效砂缩,不應(yīng)在以任何方式引用或使用它作谚。后面系統(tǒng)進(jìn)行輪詢的時(shí)候會(huì)回收或丟棄被卸載的 AA 對(duì)象。依前述 AA, ACache, RCache 之間的關(guān)系庵芭,相關(guān)的 ACache 和 RCache 的引用計(jì)數(shù)會(huì)減少妹懒。
如果一個(gè) ACache 或 RCache 的引用計(jì)數(shù)減少到 0,它將進(jìn)入一個(gè)集合双吆,以便進(jìn)行清理眨唬。真正清理將也在系統(tǒng)輪詢時(shí)進(jìn)行滔悉,主要步驟是:
- 清理被歸還的 AA 對(duì)象。
- 按資產(chǎn)間依賴關(guān)系单绑,遞歸清理引用為 0 的 ACache回官。因?yàn)?Unity 實(shí)際上不允許取消加載資產(chǎn)的操作,所以如果 ACache 指向的資產(chǎn)正在被加載搂橙,就暫緩清理歉提。注意,雖然清理了 ACache 對(duì)象区转,但不會(huì)真的卸載單個(gè)資產(chǎn)苔巨,這算是一種實(shí)現(xiàn)選擇。
- 隔一段時(shí)間废离,或者使用者要求清理時(shí)侄泽,如果引用計(jì)數(shù)為 0 的這些 RCache 中,其指向的資產(chǎn)包均不處于加載狀態(tài)蜻韭,則將它們一同卸載悼尾。這時(shí)候 Unity 層的實(shí)現(xiàn)部分是會(huì)真實(shí)調(diào)用
AssetBundle.Unload(true)
方法,將資產(chǎn)包真正卸載肖方。
對(duì)引用計(jì)數(shù)為 0 的資產(chǎn)包的同時(shí)卸載規(guī)則闺魏,主要是為了保障,彼此存在依賴關(guān)系的資產(chǎn)包會(huì)被一起卸載掉俯画,否則可能出現(xiàn)一些很難查明的資產(chǎn)丟失 bug析桥。
4.6. 資產(chǎn)包的規(guī)劃
一個(gè)相對(duì)獨(dú)立的功能,從直覺(jué)上說(shuō)艰垂,可以打成一個(gè)或多個(gè)放在一個(gè)分組中的資產(chǎn)包泡仗。實(shí)際操作中,在一個(gè)功能內(nèi)部猜憎,經(jīng)常是按文件夾來(lái)分割資產(chǎn)包的娩怎,而文件夾又經(jīng)常是按資產(chǎn)類型分的。
考慮一個(gè)問(wèn)題:如果一個(gè)貼圖文件夾中有很多貼圖拉宗,在同一個(gè)功能的兩個(gè)不同界面 p, q 上使用峦树,由于這個(gè)文件夾打在一個(gè)資產(chǎn)包中,它只會(huì)作為一個(gè)總體釋放旦事。界面 p 可能是掛在游戲主界面上的,長(zhǎng)期存在急灭,只使用了少量貼圖姐浮;而界面 q 是這個(gè)功能的主界面,使用了大量貼圖葬馋。在運(yùn)行時(shí)卖鲤,p 的生命期明顯比 q 長(zhǎng)肾扰,一旦加載了 q 使用的貼圖資產(chǎn),只是關(guān)閉和銷毀 q蛋逾,是釋放不掉 q 使用的這些貼圖的集晚。直到 p 也被銷毀,這些貼圖才會(huì)一并被卸載区匣。如果有很復(fù)雜的資產(chǎn)包間的依賴關(guān)系偷拔,這個(gè)釋放來(lái)得可能很晚。
可以通過(guò)按“生命期”劃分資產(chǎn)包(從文件夾層面就可以這樣做)亏钩,以及簡(jiǎn)化資產(chǎn)包之間的依賴關(guān)系來(lái)規(guī)避這樣的問(wèn)題莲绰。
5. 編輯器
5.1. 資產(chǎn)包組織器
編輯器層面提供了一個(gè)資產(chǎn)包組織器類 AssetBundleOrganizer
來(lái)配置將哪些資產(chǎn)打入哪些資產(chǎn)包,并配有一個(gè)簡(jiǎn)單的可視化工具(AssetBundleOrganizerEditorWindow
)來(lái)進(jìn)行編輯姑丑。
組織器可視化工具的功能大致如下:
- 左數(shù)第一欄為資產(chǎn)根目錄(可以有多個(gè))蛤签,設(shè)置將哪些目錄視為根目錄并從中讀取資產(chǎn),以及讀取什么類型的資產(chǎn)栅哀。
- 左數(shù)第二欄為資產(chǎn)目錄震肮,森林結(jié)構(gòu),每個(gè)資產(chǎn)根目錄下的資產(chǎn)在為一棵樹(shù)留拾。
- 左數(shù)第三欄為資產(chǎn)包目錄結(jié)構(gòu)钙蒙,可在其中添加、刪除间驮、編輯資產(chǎn)包躬厌,指定分組等。
- 左數(shù)第四欄展示在第三欄中選中的資產(chǎn)包內(nèi)的資產(chǎn)內(nèi)容竞帽。
結(jié)合右邊三欄扛施,可以選中資產(chǎn)文件或目錄分配如資產(chǎn)包中,也可以從資產(chǎn)包中刪除內(nèi)容屹篓。
此外疙渣,組織器還支持一個(gè)忽略某些資產(chǎn)的標(biāo)簽(AssetBundleOrganizer.IgnoreAssetLabel
屬性),給資產(chǎn)文件加上指定的標(biāo)簽(Label)堆巧,組織器將忽略這些資產(chǎn)妄荔,從而不會(huì)顯示將它們打入資產(chǎn)包。
組織器會(huì)將信息存放在一個(gè) xml 文件中谍肤,如上圖左下角的 Config path 所示啦租。對(duì)于規(guī)模較小的項(xiàng)目,直接用這個(gè)可視化工具也許就夠了荒揣。但如果項(xiàng)目規(guī)模較大篷角,則建議使用 AssetBundleOrganizer
提供的 API 來(lái)編寫“規(guī)則”代碼,來(lái)動(dòng)態(tài)生成這些內(nèi)容系任。
5.2. 資產(chǎn)包信息提供器
資產(chǎn)包信息提供器由類 AssetBundleInfosProvider
實(shí)現(xiàn)恳蹲,用于將組織器中的數(shù)據(jù)轉(zhuǎn)換成構(gòu)建資產(chǎn)包可用的數(shù)據(jù)虐块。譬如,資產(chǎn)包組織器中可以將某個(gè)目錄分配到某個(gè)資產(chǎn)包中嘉蕾,但是實(shí)際構(gòu)建資產(chǎn)包需要將目錄中的資產(chǎn)文件和資產(chǎn)包對(duì)應(yīng)起來(lái)贺奠。資產(chǎn)包信息提供器就能進(jìn)行此轉(zhuǎn)換。此外错忱,還可以檢測(cè)(打包用的)資產(chǎn)間依賴關(guān)系儡率、資產(chǎn)包間的依賴關(guān)系是否合法(比如前述資產(chǎn)包編輯器可視化工具中的 Check Dependency Legality 按鈕)等等。
5.3. 構(gòu)建
資產(chǎn)包構(gòu)建器(AssetBundleBuilder
類)封裝了構(gòu)建資產(chǎn)包的過(guò)程(方法 BuildPlatform
)航背。主要步驟如下:
- 通過(guò)資產(chǎn)包組織器和信息提供器喉悴,得到資產(chǎn)和資產(chǎn)包的對(duì)應(yīng)關(guān)系,構(gòu)造 Unity 的
AssetBundleBuild
列表玖媚。 - 調(diào)用 Unity 的方法箕肃,構(gòu)建資產(chǎn)包,獲得 manifest 文件今魔。
- 利用 manifest 文件和其他數(shù)據(jù)勺像,生成在索引文件中需要的資產(chǎn)包信息,如分組错森、CRC-32 校驗(yàn)和吟宦、Unity 生成的 Hash 值等。
- 生成 Client, ClientFull, Server 文件夾及相應(yīng)的索引文件涩维。
使用者可以通過(guò)實(shí)現(xiàn) IAssetBundleBuilderHandler
接口來(lái)指定構(gòu)建各個(gè)階段的回調(diào)殃姓。例如:使用 Lua 腳本的項(xiàng)目可以在自己的 IAssetBundleBuilderHandler
實(shí)現(xiàn)中,用 OnPreBeforeBuild
回調(diào)來(lái)給 .lua 后綴的文件改名為 .txt 之類的后綴瓦阐,以便能被 Unity 識(shí)別為文本資產(chǎn)(Text asset)蜗侈;同樣,在 OnBuildSuccess
和 OnBuildFailure
回調(diào)中將重命名的文件復(fù)原睡蟋。
6. 局限性
- 目前對(duì)已經(jīng)發(fā)起的資產(chǎn)加載調(diào)用是沒(méi)有優(yōu)先級(jí)的踏幻,內(nèi)部又有一些 Hash 存儲(chǔ),不能保證實(shí)際的加載順序和發(fā)起加載調(diào)用的順序一致戳杀。
- 內(nèi)存中同時(shí)有資產(chǎn)間的依賴關(guān)系和資產(chǎn)包間的依賴關(guān)系该面,不知道是否可以舍棄后者,還能保證邏輯正確信卡,不出現(xiàn)資產(chǎn)丟失的問(wèn)題隔缀。
- 加載資產(chǎn)名義上是異步,但實(shí)際上有可能是同步返回的坐求。實(shí)際使用時(shí)蚕泽,為了便利起見(jiàn)可以增加中間層。
- 目前采用“集總式”索引文件桥嗤,可能一次解析的內(nèi)容較多须妻,在游戲啟動(dòng)階段造成一些卡頓現(xiàn)象。
- 未能支持子資產(chǎn)(Sub-asset)或泛型加載資產(chǎn)泛领。例如:對(duì)圖集(如 Texture Packer 這類插件輸出的)這種類型的資產(chǎn)荒吏,需要用一個(gè)
SerializableObject
來(lái)存放其中精靈圖的引用。