關(guān)于AssetBundle的卸載

關(guān)于AssetBundle的卸載

又坐了一天月子,繼續(xù)寫文章找狀態(tài)。

本文是關(guān)于 卸載AssetBundle 的一些知識(shí)點(diǎn)。

下圖是一個(gè)最簡(jiǎn)單的從 AssetBundle 加載 Asset實(shí)例化 的流程:

image

這里的 Bundle 在加載完 資源A 后就沒(méi)用了,我們可以通過(guò) AssetBundle.Unload(false) 把它卸掉,只保留住 資源A。

如果 資源A 也沒(méi)用了,我們可以通過(guò) Destroy 接口或者 Resources.UnloadAsset 接口銷毀它。

題外話,我們需要注意一下 Resources.UnloadAsset 的應(yīng)用場(chǎng)合:

This function can only be called on Assets that are stored on disk.

The referenced asset (assetToUnload) will be unloaded from memory. The object will become invalid and can't be loaded back from disk. Any subsequently loaded Scenes or assets that reference the asset on disk will cause a new instance of the object to be loaded from disk. This new instance will not be connected to the previously unloaded object.

UWA 也有相關(guān)的回答:

Resources.UnloadAsset僅能釋放非GameObject和Component的資源,比如Texture、Mesh等真正的資源。對(duì)于由Prefab加載出來(lái)的Object或Component,則不能通過(guò)該函數(shù)來(lái)進(jìn)行釋放。

好了,回到上圖。

上圖描述的場(chǎng)景過(guò)于簡(jiǎn)單,實(shí)際項(xiàng)目中,資源A 可能依賴 其他資源,并且 其他資源 又被打進(jìn) 不同的Bundle 中,如下圖:

image

這個(gè)時(shí)候,卸載 AssetBundle 就需要一定的 策略 了。

在介紹 卸載策略 之前,我們必須先了解清楚 AssetBundle.Unload 這個(gè)函數(shù)。

關(guān)于AssetBundle.Unload

Unity官方文檔對(duì)于 AssetBundle.Unload 的描述如下:

public void Unload(bool unloadAllLoadedObjects);

Unloads assets in the bundle.

When unloadAllLoadedObjects is false, compressed file data for assets inside the bundle will be unloaded, but any actual objects already loaded from this bundle will be kept intact. Of course you won't be able to load any more objects from this bundle.

When unloadAllLoadedObjects is true, all objects that were loaded from this bundle will be destroyed as well. If there are GameObjects in your Scene referencing those assets, the references to them will become missing.

  • AssetBundle.Unload(false) 會(huì)把 Bundle 卸載,但是已經(jīng)從 Bundle 里加載出來(lái)的 資源 是不會(huì)被卸載的。

  • AssetBundle.Unload(true) 不但會(huì)卸載 Bundle,也會(huì)卸載已經(jīng)從 Bundle 里加載出來(lái)的 所有資源,哪怕這些 資源 還被引用著。

對(duì)于用戶來(lái)說(shuō),如果選擇 AssetBundle.Unload(true),用戶必須確保 Bundle 中已經(jīng)加載的 資源 是沒(méi)有被引用的,否則就會(huì)發(fā)生 資源丟失

如果選擇 AssetBundle.Unload(false),用戶就要承擔(dān)起卸載 已加載資源 的責(zé)任,如果處理不當(dāng),就可能造成 資源重復(fù),如下圖:

image

最后,Unity提供了一個(gè) Resources.UnloadUnusedAssets 接口幫助我們銷毀沒(méi)有任何引用的 野資源,不過(guò)這個(gè)函數(shù)會(huì)掃描全部對(duì)象,開(kāi)銷較大,一般只在 切場(chǎng)景 時(shí)調(diào)用。

卸載AssetBundle的策略

了解 AssetBundle.Unload 的行為后,再來(lái)看一下我們采用過(guò)的策略。

暗黑血統(tǒng)的策略

最早做 暗黑血統(tǒng) 的時(shí)候,我們卸載 AssetBundle 的策略如下:

  • AssetBundle.Unload(false) 來(lái)卸載 Bundle。

  • 加載完資源后立即卸載 葉子節(jié)點(diǎn)的Bunlde,這里 葉子節(jié)點(diǎn) 表示 沒(méi)有被其他Bundle所依賴的Bundle。

  • 對(duì)于 非葉子節(jié)點(diǎn)的Bundle,卸載邏輯完全依靠 引用計(jì)數(shù)。

以下圖為例:

image

紅框標(biāo)注的 Bundle 可以在加載完 資源 后立刻卸載。

我們看一下包含 資源A和B的Bundle,如果我們只加載了 A,然后就把 Bundle 卸載了,然后我們?cè)偌虞d B,這個(gè)時(shí)候 Bundle 又要被重新加載,如果我再?gòu)倪@個(gè) Bundle 加載 A,這個(gè)時(shí)候不是就有 2個(gè)A 了?

事實(shí)上,因?yàn)?第一個(gè)A 依然還被我們的 AssetManager 管理著,上層邏輯不會(huì)直接從 Bundle 中去加載 A,而是從 AssetManager的緩存 中去拿,所以上述情況并不會(huì)出現(xiàn)。

我們?cè)倏匆幌掳?資源C的Bundle,如果我們加載完 C 后就把 Bundle 卸載了,然后我們?cè)偃ゼ虞d G,由于 G依賴C,C所在的Bundle 會(huì)再次被加載,同時(shí)加載出一個(gè) 新的C,這就是真正的 資源重復(fù) 了。

此外,因?yàn)槲覀兊?AssetManager 只會(huì)管理 直接加載的資源,假設(shè)我們先加載了 GC 做為 G 的依賴被 間接加載,此時(shí)我們?cè)偃ブ苯蛹虞d C 就無(wú)法命中 AssetManager 的緩存了。對(duì)于這種情況,AssetManager 必須做一些額外記錄,稍微有點(diǎn)蛋疼。

最后,考慮一下 引用計(jì)數(shù),假設(shè)我們已經(jīng)銷毀了 G,那么 G 依賴的所有Bundle引用計(jì)數(shù)會(huì)減 1,假設(shè) 包含D的Bundle 以及 包含H的Bundle 引用計(jì)數(shù)都為 了,選擇 AssetBundle.Unload(false) 的結(jié)果是被 間接加載D和HBundle卸載 后依然存活著,我們的 AssetManager 并沒(méi)有管理到它們,它們變成了 野資源。

這一類 野資源 最終得依靠 Resources.UnloadUnusedAssets 來(lái)卸載,一般我們?cè)?場(chǎng)景切換 時(shí)才做這個(gè)操作。

當(dāng)前項(xiàng)目的策略

暗黑血統(tǒng)的策略在線上工作良好,不過(guò)因?yàn)?AssetManager 只會(huì)管理 直接加載 的資源,AssetBundle.Unload(false) 必須配合 Resources.UnloadUnusedAssets 才能完成一次徹底的資源清除。

當(dāng)前項(xiàng)目,AssetManager 依然只管理 直接加載 的資源,不過(guò)我選擇了 AssetBundle.Unload(true) 的策略,并且不再區(qū)分 Bundle是否是葉子節(jié)點(diǎn),一切卸載的依據(jù)都是 引用計(jì)數(shù)

正所謂 Bundle在,資源在,Bundle亡,資源亡,:)

以下圖為例,最左邊一列會(huì)標(biāo)記出每個(gè) Bundle 的引用計(jì)數(shù):

image

當(dāng)我們銷毀 資源A,引用計(jì)數(shù)變化如下:

image

當(dāng)我們?cè)黉N毀 資源B,部分 Bundle 的引用計(jì)數(shù)變?yōu)?,調(diào)用 AssetBundle.Unload(true) 就能把資源及時(shí)清理干凈了,如下圖:

image

只要引用計(jì)數(shù)沒(méi)問(wèn)題,理論上 Resources.UnloadUnusedAssets 也就不用了。

總結(jié)

當(dāng)前項(xiàng)目的方案在 資源回收的及時(shí)性 上要優(yōu)于 暗黑血統(tǒng) 的方案,但是 暗黑血統(tǒng) 因?yàn)?提前卸載Bundle 的策略,Bundle數(shù)量 會(huì)更少一點(diǎn)。

其實(shí)早期 暗黑血統(tǒng)提前卸載Bundle 的方案上做得更加激進(jìn):只有 被共享的Bundle 才不會(huì)被提前卸載,而非我上文所說(shuō)的 葉子節(jié)點(diǎn),當(dāng)然,要堵不少bug。

考慮到目前 LZ4 的壓縮策略,以及 AssetBundle.LoadFromFile 的加載方式,多一點(diǎn) Bundle 的內(nèi)存占用不會(huì)很高,但是多一點(diǎn) 資源 內(nèi)存占用就比較高了。

題外話,下圖是 UWA 關(guān)于 LZ4LZMA 的總結(jié),留圖備忘:

image

本文就到這里,今年有機(jī)會(huì)也可以試試Unity推薦的 Addressable Assets。

個(gè)人主頁(yè)

本文的個(gè)人主頁(yè)鏈接:https://baddogzz.github.io/2020/02/07/Unload-Resources/

好了,拜拜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容