【Unity】開發WebGL內存概念詳解和遇到的問題

自加入unity WebGL平臺以來,Unity的開發團隊就一直致力於優化WebGL的內存消耗。我們已經在Unity使用手冊上有對於WebGL內存管理的詳盡分析,甚至在Unite Europe 2015與Unite Boston 2015兩屆大會上,也有專題對其進行深入的講解。然而,這方面的內容依舊是用戶討論的熱門話題,因此我們意識到應當分享更多。希望本文能回答一些被頻繁諮詢的問題。
 
Unity WebGL與其它平臺有何不同?
一些用戶已經熟悉了部分內存有所限制的的平臺。而對於其它如桌面和WebPlayer平臺,到目前爲止內存還不是問題。

在內存方面,主機平臺相對其它平臺較爲簡單,因爲您可以準確的知道內存是如何使用的。這允許您可以很好的管理內存,並保證您的遊戲內容完美運行。在移動平臺,內存管理變的有些複雜,因爲設備種類繁多,但至少您可以選擇最低標準的設備,並根據市場情況忽視那些相較於該標準更爲的低端設備。

在網頁平臺,就沒有那麼輕鬆了。理想情況下,所有終端用戶都擁有64位瀏覽器和大量內存,但事實卻相距甚遠。首先,您無法通過任何方法知道,正運行您的內容的硬件規格。其次,除了用戶的操作系統和瀏覽器外,您並不知道其它信息。最後,終端用戶可能像運行其它網頁一樣運行您的WebGL內容。因此這是一個非常複雜的問題。

概覽

下圖是在瀏覽器上運行Unity WebGL內容時的內存概覽:



上圖展示了Unity 堆,Unity WebGL內容將需要向瀏覽器請求額外分配的內存。這是理解WebGL內存管理的重點,從而讓您優化項目得以將用戶流失率降至最低。

正如上圖所示,存在幾組內存分配:DOM,Unity堆,資源數據和代碼,這些內容都會在網頁加載時持久存在於內存中。而其它諸如 Asset Bundles, WebAudio 和 Memory FS 何時加載則取決於您的內容運行情況。(例如:Asset Bundle下載,音頻播放等等)

在加載期間, 一些瀏覽器在asm.js解析和編譯時會產生臨時內存分配,這偶爾也會導致部分使用32位瀏覽器的用戶出現內存溢出的問題。

Unity堆

通常來說,Unity堆是指包含了所有Unity特有的遊戲對象、組件、紋理、着色器 等等的內存塊。

在WebGL平臺,Unity堆的大小需要提前獲知,瀏覽器才能對此分配空間,並且內存空間一旦分配,就無法改變內存緩衝區大小。

負責Unity 堆內存分配的代碼如下:

1.buffer = new ARrayBuffer(TOTAL_MEMORY);

這段代碼可以在所生成的build.js中找到,並通過瀏覽器的JS虛擬機來執行。

TOTAL_MEMORY 是在Player Settings 中的WebGL Memory Size中設置的總內存。默認爲256MB,但這是我們隨意設定的值,事實上,一個空項目運行僅需16MB。

然而,真實世界中游戲內容可能會需要更多的內存空間,大部分情況下都需要256或者386MB。請記住,項目需要的內存越多,能夠運行它的終端用戶就越少。

源代碼/編譯代碼內存

在代碼可以被執行之前,它需要如下步驟:
  • 下載
  • 複製到一個文本域
  • 編譯

請慎重考慮,上述的每一個步驟都將請求大量內存。因爲:

  • 下載緩衝區是臨時的,但是源代碼和編譯代碼將持久存在於內存中。
  • 下載緩衝區和源代碼大小,都是Unity所生成的未壓縮的js大小。按照以下步驟,您可以估算它們需要多少內存:
    • 構建一個發佈版本。
    • 將jsgz 、datagz重命名爲*.gz文件,並通過壓縮工具對它們進行解包。
    • 解壓縮後的大小就是它們在瀏覽器內存中的大小。
  • 編譯代碼的大小取決於瀏覽器。

優化內存的一個簡單方法是啓用Strip Engine Code,這樣您發佈的版本將不包含那些不必需的原生引擎代碼(例如:如果不需要2D物理模塊,它將被剝離)。請注意:託管代碼一定會被剝離。

千萬要記住,異常捕捉和第三方插件也將增加代碼大小。正如之前所說,我們已經注意到用戶需要添加空值檢查和數組邊界檢測的代碼,但不希望完整的異常檢測支持會帶來過多的內存(及性能)消耗。要實現這點,您可以通過編輯器腳本傳遞 
–emit-null-checks 和 –enable-array-bounds-check 到il2cpp,例如

PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");

最後請記住,構建開發版本產生的代碼尺寸更大,因爲它不曾縮減。這不是問題,畢竟最終交給用戶的會是發佈版。

資源數據

在其它平臺上,一個應用可以簡單地訪問位於固定存儲空間(硬盤,閃存等等)的文件。而在網頁平臺上這是不可能的,因爲出於安全考慮,網頁平臺無法訪問真正的文件系統。因此,Unity WebGL 數據(.data文件)一旦被下載,就會永遠存儲在內存中。這樣做的缺點就是它相對其它平臺將需要更多的內存(例如5.3中.data文件以lz4壓縮的形式存儲在內存中)。例如,下圖是分析器顯示的一個項目生成了約40MB的數據文件(在256MB Unity堆的設置下):



.data 文件中包含了什麼?它是Unity所生成的文件集合,包含以下內容:data.unity3d (所有的場景,它們依賴於Resources文件夾中的資源和所有內容),unity_default_resources和少量引擎所需的小文件。

爲了知曉資源的準確總大小,您需要在發佈至WebGL平臺後查看Temp\StagingArea\Data目錄下的data.unity3d (Temp文件夾將會在Unity編輯器關閉時被刪除)。另外,您也可以通過查看UnityLoader.js 中的DataRequest差值得知素材資源的準確大小。

new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(這段代碼根據Unity版本不同,寫法可能有些區別——示例是Unity 5.4)

內存文件系統
雖然不存在真實的文件系統,正如前文所述,您的Unity WebGL內容仍然可以讀寫文件。相對於其它平臺的主要區別在於,WebGL平臺的文件輸入/輸出操作實際上都是對內存的讀/寫操作。很重要一點是,這個內存文件系統並不存在於Unity 堆中。因此,它將需要額外的內存。例如,下面這個輸出數組到文件的示例:

var buffer = new byte [10*1014*1024];

File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);

這個文件將會被寫入到內存中,並且在瀏覽器的分析器也可以查看到。


請注意:Unity堆的大小爲256MB。

同樣,Unity的緩存系統依賴於文件系統,所以WebGL平臺整個緩存存儲也是在內存中進行的。這意味着像PlayerPrefs和緩存的Asset Bundles也會被持久化到內存中,而不存在於Unity堆中。

Asset Bundles

減少WebGL平臺內存消耗的最佳方法之一是使用Asset Bundles (如果您對Asset Bundles不熟悉,請查閱Unity使用手冊或通過教程學習)。然而,根據使用方式不同,它們將會對內存消耗帶來巨大影響(Unity堆中和堆外都會受此影響),這將有可能導致您的內容無法運行在32位瀏覽器上。

如果真的需要使用Asset Bundle,您會將所有資源打包到一個單獨的Asset Bundle嗎?

千萬別這麼做!即使那樣可能會減少網頁加載期間的壓力,您仍然需要下載(極可能無比巨大的)Asset Bundle,從而導致內存使用高峯。來看看下載AB前的內存使用情況。


如您所見,256MB被分配給Unity堆。下圖是沒有經過緩存的Asset Bundle下載:


現在看到的是額外的緩存,大約與硬盤中的Asset Bundle(約65mb)大小相同,它是通過XHR分配的。這只是一個臨時緩存,但它將導致連續幾幀的內存高峯,直至垃圾收集器啓動。

如何最小化內存高峯?爲每個資源創建一個Asset Bundle?想法不錯,但明顯不合實際。

事實上,對於如何做能夠減少內存高峯並沒有普遍的標準,這取決於您項目的實際需求。

最後,在資源使用完畢後記得通過AssetBundle.Unload卸載Asset Bundle。

Asset Bundle緩存

Asset Bundle緩存與其它平臺一樣,您只需要使用WWW.LoadFromCacheOrDownload。它們最大的區別就是內存消耗。在Unity WebGL中,AB緩存依賴於IndexedDB,IndexedDB是由目前內存文件系統所支持的emscripten編譯器實現。

下圖使用LoadFromCacheOrDownload下載Asset Bundle的內存使用情況:


如您所見,Unity堆使用了512MB,並額外分配了約4MB的內存。

下圖是加載Asset Bundle後的內存情況:


額外需要的內存跳到了約167mb。這是該Asset Bundle所需的額外內存(壓縮包約爲64mb)。下圖是js虛擬機垃圾收集器啓動後的內存情況:


可以看到現在有了一些改善,但仍需約85mb的內存,其中大部分內存用於將Asset Bundle緩存到內存文件系統。這些內存即使卸載了Asset Bundle也不會回收。還有一點很重要,當玩家第二次在瀏覽器中運行遊戲時,這些內存會被立即加載,甚至在加載Asset Bundle之前。

下圖是Chrome的內存截圖以供參考:


同樣,在Unity堆外還有其它緩存相關的臨時內存分配,以供Asset Bundle系統使用。壞消息是最近我們發現它比預想的更大。好消息是它將在未來的Unity 5.5 Beta 4,5.3.6 Patch 6和5.4.1 Patch 2中得以修復。

對於更早的Unity版本,萬一您的Unity WebGL內容已經上線或即將發佈,而您又不想升級項目,一個快速的變通方法是通過編輯器腳本的設置以下屬性:

PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);

最小化Asset Bundle緩存內存消耗的長遠解決方案是,使用WWW構造器替代LoadFromCacheOrDownload(),或者您使用新的UnityWebRequest API 時,調用UnityWebRequest.GetAssetBundle()不要帶有哈希或版本參數。

其次是在XMLHttpRequest層使用替代的緩存機制,繞過內存文件系統,將下載的文件直接存儲到indexedDB中。我們已經開發了這樣的工具併發布在Asset Store中。您可以免費將它用於您的項目,也可以自定義以滿足特殊需求。

Asset Bundle壓縮

Unity 5.3和5.4均支持LZMA和LZ4兩種壓縮方式。然而,即使使用LZMA(默認)壓縮相對於LZ4或未壓縮下載的包更小,但它在WebGL平臺上還是有些缺點:它會導致明顯的運行延遲,並且需要更多的內存。因此強烈建議使用LZ4或者未壓縮的格式(實際上,Unity 5.5的WebGL平臺將不再支持對Asset Bundle的LZMA壓縮),爲了彌補相比LZMA壓縮的下載尺寸過大,您可能希望使用gzip/brotli來壓縮Asset Bundle,並配置到您的服務端。

查閱Unity使用手冊以獲得更多關於Asset Bundle壓縮的信息

網頁音頻

音頻在Unity WebGL上的實現方式有所不同。這對內存意味着什麼?

Unity將會在JavaScript中創建特定的AudioBuffer的對象,以便它們可以通過WebAudio進行播放。

由於WebAudio緩存位於Unity堆外,因此無法通過Unity 分析器進行跟蹤分析,您需要使用瀏覽器專用的工具,來查看音頻使用了多少內存。示例如下(火狐瀏覽器, about:memory page):


考慮到那些Audio Buffers保存的是未解壓的數據,其可能不適用於大型音頻片段資源(例如:背景音樂)。對於那些資源,你可能希望自己編寫js插件,以便使用<audio>標籤。這種方式下音頻文件會保持壓縮,因此需要的內存更少。

FAQ

問:減少內存使用的最佳實踐是什麼?
答:概括如下:
  • 減少Unity堆的大小
  • 儘可能保持“WebGL Memory Size”足夠小
  • 減少代碼量
  • 啓用Strip Engine Code
  • 禁用異常檢測
  • 避免使用第三方插件
  • 減少數據大小
  • 使用Asset Bundles
  • 使用Crunch紋理壓縮

問:是否存在能夠決定最小WebGL Memory Size的策略?

答:有,最佳策略是使用內存分析器,分析您的內容實際所需的內存大小,然後據此改變WebGL Memory Size。

以空項目爲例,內存分析器告訴我們總的使用量僅爲16mb(這個值可能在不同Unity版本上有所不同):這意味着只須設置WebGL Memory Size大於16MB即可。當然,內存的總使用量將會依據您的內容而有所不同。

然而,如果因爲某些原因無法使用分析器,可以簡單地通過不斷地減少WebGL Memory Size 值,直到發現您的內容真正所需要的最小內存使用量爲止。

另外非常值得注意的是,任何不是16的倍數的值都將被自動的四捨五入(在運行時)爲下一個16的倍數,這是Emscripten編譯器所要求的。

WebGL Memory Size(MB)設置將決定生成的html中TOTAL_MEMORY(bytes)的值。


所以,爲了在不重新構建項目的前提下,反覆測試內存堆的值,推薦使用更改html的方式。一旦您通過此方式發現適合的值,只需在Unity項目設置中更改WebGL Memory Size即可。

最後,記住Unity的分析器將佔用一些來自Unity堆的內存,所以在使用分析器時可能需要增加WebGL內存大小。

問:運行時發生內存溢出,如何修復?

答:這取決於是Unity,還是瀏覽器的內存溢出。這個錯誤信息將會指出問題所在以及解決辦法:“如果您是該內容開發者,請在WebGL設置中爲您的應用分配更多(或更少)的內存。”此時您可以據此調整WebGL內存大小設置。然而還有很多可以解決內存溢出的方法。如果出現以下錯誤信息:


除了消息內容,您還可以嘗試減少代碼和數據的大小。這是因爲當瀏覽器加載網頁時,它將試圖爲一些內容尋找空餘的內存,其中最重要的是:代碼,數據,Unity堆和被編譯的asm.js。它們可能相當大,尤其是數據和Unity堆內存,這對32位瀏覽器來說可能是問題。

在一些例子中,儘管存在足夠多的空餘內存,瀏覽器仍將加載失敗,因爲內存是碎片化的。這就是爲什麼有時候您的內容可能在重啓瀏覽器之後,可以成功加載的原因。

另一種情況是,當Unity 內存溢出時提示以下信息:


在這種情況下,您需要優化您的Unity項目。

問:如何衡量內存消耗?

答:爲了分析內容所使用的瀏覽器內存,可以使用火狐瀏覽器的內存工具或Chrome堆快照。但它們不會顯示WebAudio內存使用情況,因此還可以獲取火狐瀏覽器的about:memory頁面快照,然後通過搜索“webaudio”找到。如果您需要通過JavaScript分析內存,請嘗試使用window.performance.memory(只支持Chrome)。

使用Unity分析器測量Unity堆內存使用。但請注意,您可能需要增加WebGL的內存大小,以便能夠使用分析器。

此外,我們一直在致力於開發一個新的工具,以便您能分析發佈版本:構建WebGL版本,然後訪問http://files.unity3d.com/build-report/即可使用該工具。雖然這在Unity5.4下已經可用,但請您注意,這還是正在開發中的功能,並且隨時會更改或被刪除。但至少現在可以使用它達到測試的目的。

問:WebGL Memory Size的最小值與最大值是多少?

答:16MB是最小的,最大是2032MB,然而我們通常建議保持在512MB以下。

是否可能出於開發目的而需要分配超過2032MB的內存?

這是一個技術上的限制:2048MB(或更多)將會超出TypeArray所用的32位有符號整型的最大值,而TypeArray被用於在JavaScript中實現Unity堆。

問:爲何Unity 堆大小不可改變?

答:我們一直在考慮使用Emscripten編譯器標誌ALLOW_MEMO
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章