2017年4月24日 星期一

Asset Bundles與Resources的記憶體大對決

作者:Ryan Caltabiano 原文
潤稿:Kelvin Lo

*連結全部導向英文頁面

近期有不少Unity開發者詢問有關Asset Bundle和舊的資源系統Resources的相關問題:為何Asset Bundle載入Asset時消耗的記憶體要比Resources多。


首先說明事實並非如此。或者說從長遠來看,如果用好Asset Bundle中一些Resources系統所沒有的新特性,Asset Bundle的記憶體消耗會小的多。如果您不熟悉Asset Bundle,可以參考Unity手冊Asset BundleResources指南

根據我們收到的一些Bug回報,基本上說的都是同一件事情:當從一個Asset Bundle載入某個Asset時,記憶體使用量會衝高好幾MB,但在使用Resources時卻沒發生這樣的情況。我們嘗試重現這些問題時,看到的結果也非常相似:啟動時記憶體正常,載入Asset後記憶體激增,並且不會回到原來的水平。

Asset Bundle的記憶體使用情況



Resources的記憶體使用情況




下面我們就透過記憶體系統就關聯關係、資料保存方式、記憶體使用量的含意和記憶體使用效率幾個方面來分析一下這個問題的原理。注意:本文使用的Unity版本為Unity 5.5.0f3。

Unity的原生記憶體系統會使用1MB到32MB(平均1MB到4MB)之間幾個不同固定大小的塊記憶體分配器。具體大小根據分配的工作類型而定,例如主執行緒還是背景執行緒或根據當下執行的平台而定。

保留總量(Reserved Total)是作業系統分配的所有區塊的總量,已用總量(Used Total)是其中正由Unity使用的記憶體量。每個區域標籤、FMOD、Porfiler等等,表示系統列出的相應分配器或大概的外部記憶體。這些區域標籤資訊在Memory Profiler手冊頁面中可以查詢。有幾點手冊上沒說明的,像是Used and Reserved totals並不包含FMOD數值(Unity 5.5.0f3),
這問題我們已經有個修復更新。

總系統記憶體使用量(Total System Memory Usage)是由平台系統提供的虛擬記憶體大小,不支援此功能的平台會顯示為0。最後,已用總量並不包括物件的標頭(Header)或位元組對齊(byte alignment),但會保留總計。因此,要對比Asset Bundle和Resources間的記憶體使用量,我們會主要針對已用總量和保留總量中的Unity區域標籤。

另外,瞭解Asset Bundle以及Resources資料在磁碟上的保存方式對於理解分析器的原理也十分必要。

Resources和Asset Bundle在底層資料結構上非常相似,它們都有一個用來存放每個物件序列化資料的檔,一些用來有效率非同步載入的額外資源檔(貼圖、音效等),以及一個包含序列化物件Asset檔路徑
映射表。Asset Bundle將這些檔都打包在一個壓縮包中,映射表存在Asset Bundle物件的序列化資料中。Resources將映射表保存在一個名為ResourceManager的全域單例(global singleton)中,其他檔則散落在磁碟各處。此外,與Resources系統不同的是,Asset可以分散在不同的Asset Bundle中,因此可以透過載入資料子集來最大限度地減少記憶體使用量。關於Asset Bundle的內部結構資料可以參閱Unity手冊

Asset Bundles



Resources




瞭解Unity的記憶體與檔案系統之後,我們再詳細解釋下這些記憶體使用量數值的意義。

第一個凸顯的問題是Unity用於Asset Bundle的保留區域多了10MB,而用於Resources系統的則沒有增長。這是為什麼呢?這主要是因為前面提到的區塊記憶體分配器。就這個特定的記憶體使用率測試而言,我們使用的是非同步載入API 協程(Coroutines)的AsyncBundleLoader.cs行為。值得注意的是,這種組合實際上會使用不同的區塊記憶體分配器,這些區塊記憶體分配器在此時其實尚未被使用。所以,10MB的占用源於兩個區塊記憶體分配器初始化區塊的行為,其中一個區塊記憶體分配器需要為新物件分配更多記憶體,所以它分配了一個4MB的區塊。兩個新分配器中,一個為Asset Bundle非同步載入分配了一個2MB的區塊,另一個則為類型樹(type trees)分配了一個4MB的區塊。這些區塊的大小是專為同時載入多個Asset與Asset Bundle而優化的。例如,您可以同時從4 - 5個Asset Bundle中載入物件,而無須為Asset Bundle非同步載入或需要新塊的類型樹創建新的分配器。當然,這具體還是要根據Asset Bundle的大小,採取的壓縮方式,以及這些包中所使用的唯一腳本類型的數量而定。

在這些分配的區塊中,用於類型樹的4MB區塊,僅在從Asset Bundle載入物件時使用。操作完成後這個塊理論來說會釋放。但是,由於範例中構建協程的方式,導致AssetBundleRequest物件一直處於作用中,沒有被垃圾回收器(GC)清除。而用於Asset Bundle非同步載入的2MB區塊,它是讀取Asset Bundle檔案時的緩衝區,在沒有包的內部引用後會被釋放。最後的4MB區塊的使用者是負責我們所有物件存儲的主分配器,因此不會被釋放。

通常在一個專案中,物件的建立/刪除非常頻繁,我們會使用記憶體池來重用記憶體而非將它釋放回分配器。觀察最終卸載後保留區域的Unity數值時,您會發現使用Asset Bundle(64.1MB)與使用Resources系統(63.3MB)的差異很小,和區塊記憶體分配器獲得新塊的順序有關。

我們一直都在討論保留記憶體,那Asset Bundle與Resources之間的保留記憶體實際使用效率差別又有多大呢?

這個問題非常簡單,因為已使用中的Unity區域已經告訴了我們答案。使用Asset Bundle時,佔用了保留記憶體中的21.7MB,而使用Resources時稍多,大概在22.2MB。此外,在卸載時,這個記憶體數值分別下降為20.7MB和21.2MB。所以,很顯然Asset Bundles是記憶體利用效率方面的贏家。

你可能已經注意到,Asset Bundle的使用量在卸載後要比它啟動時更大(4.4MB)。這是因為前面提到的記憶體池的關係,因此如果您重新載入Asset Bundle與Asset,它將會回到21.7MB。而對於Resources來說,啟動與卸載時的記憶體差異來自四捨五入誤差。對於Asset Bundle的塊記憶體分配可以減少,但是要犧牲效能與向下相容性。正如上面提到的,為了滿足載入物件所需的記憶體量,所以必須要分配4MB大小的區塊。剩下的6MB中,2MB用於了非同步載入API。

因此,為了防止區塊記憶體分配,只要犧牲FPS使用同步API即可。最後的4MB是源於類型樹的分配。這個系統會在Asset Bundle裡儲存Resources系統中沒有的額外資料,這些資料使Asset Bundle可以相容更多版本的Unity,並使諸如FormerlySerializedAs這樣的序列化特性正常工作。這使您可以在升級到更新的Unity版本後依然能使用相同的Asset Bundle,或只需修改少量程式,而不是重新打包,導致開發者因為更新必須重新下載整個Asset Bundle。向BuildPipeline.BuildAssetBundlesAPI傳入BuildAssetBundleOptions.DisableWriteTypeTree選項,可以禁止寫入這個額外資料。

無類型樹Asset Bundle同步載入



您可以用Asset Bundles 1, Resources 0試試。如果您想產生自己的資料,這個範例專案已經上傳到了 Github 上。目前它設定為亂數取樣每種建立100個:Textures、Monobehavior、Prefabs,所以在自己機器上每次執行都會產生同樣的輸出(但會和別人不一樣)。請確保你的Asset Bundle專案沒有意外包含了一個有內容的Resources資料夾,否則您的記憶體將會比預期高出兩倍。試試將每類資源的數量提高至300或甚至500個測試。

沒有留言:

張貼留言

關於我自己

我的相片
Unity台灣官方部落格 請上Facebook搜尋Unity Taiwan取得Unity中文的最新資訊