2016年5月31日 星期二

Unity全新產品及定價6月出爐

今天在Unite Europe,我們宣佈了即將在6月推出的新產品。如果您現在是我們的客戶,我們將邀請您以優惠的方式轉到新機制。

為什麼我們要推出新的訂閱制產品?

此時此刻市場越來越大,客戶的選擇越來越多。移動設備已成主流,主機與PC需要大膽的創意,而VR/AR已奔上浪頭。面對這些,Unity過去的購買方式顯得不夠平易近人。以往必須以較高價格購買核心產品與加附加元件,每兩年左右再繳納升級費用的做法似乎已不合時宜。 因此我們想簡化,並且提供更多的價值。我們相信,新產品將為您帶來更大的收益。

您無需擔心會錯過最新的技術,同時也無需擔心無法負擔Unity。

新方案為您帶來的更多好處:
  • 全平臺支持,取消了原本Pro版還必須額外買iOS和Android擴充包的額外付費,現已包含在新Pro版授權。 
  • 不會再有不同大版本不同更新支援的問題,持續穩定的獲取最新的技術更新、性能改進以及新平臺資訊。無需額外費用。 
  • 更多新功能及工具:從性能報告到新版Analytics即時分析及原始資料匯出、資源商店工程包等等。 

瞭解新產品及定價

Unity Personal 個人版

Unity Personal 個人版沒有改變。還是一樣的免費且全編輯器功能。我們希望Unity Personal 個人版能夠更好地推動開發大眾化。使用條件必須年收入或資本額少於10萬美元。

Unity Pro 專業版

按年訂閱,每月125美金,沒有任何收入的限制。
新Unity Pro版,專為需要極度靈活發佈商業遊戲或互動內容的專業個人或團隊而設計。由於是全平台,iOS Pro和Android Pro等套件已經包含在內。

Unity Enterprise 企業版

為需要原始碼及企業支援的大型組織而準備。
Unity Enterprise企業版需要聯繫所在地區的Unity銷售代表諮詢。 此外,服務週期結束後我們還會提供付費降為Unity Pro版本服務。

新產品及訂閱對您有何意義?

我們將聯繫現有客戶,告知有關新訂閱的資訊。為了保證這個過程順利進行,我們將從6月15日接下來的幾個月對現有客戶進行分批聯絡。請查看轉移進度圖,瞭解何時可以轉到新方案。
客戶可以保留舊方案直到2018年6月。擁有永久授權的客戶可以保留他們的目前的授權,但在2017年5月3日起我們將停止對Unity 5 舊授權提供新功能及更新。

查看關於新版Unity訂閱的FAQ(英文),或上論壇討論
或者觀看影片瞭解更多資訊(英文):

2016年5月29日 星期日

中斷! 如何在Unity裡用C#停止一個無限迴圈

作者:PETER ANDREASEN 原文連結

(這篇心得是Peter在今年Unity內部活動HackWeek所做的內容。)

這篇文章將為大家介紹在Unity中如何中斷無限迴圈的小技巧。可用於64位元Windows系統的編輯模式,以及有開啟除錯模式的專案。稍微改一下就可以相容32位元環境,甚至不用開啟除錯模式也可以用。

無限迴圈看起來很容易避免。但偶爾就是會遇到它的變種。曾經有次因為有問題的亂數函式回傳了1.000001導致無限迴圈。也曾經一個壞掉的網格剛好幫沒做輸入檢查的while(1) { d += 1.0; if(d>10.0) break; /* .. */ } 迴圈發送了NaN這個資料。還曾發生壞掉的資料結構被執行一個規則current = current.next;並認定這個規則一定會有個終點等等...

如果在Unity中碰過這種無限迴圈,你應該理解這會讓人很不爽。Unity會沒回應,並且需要強行關閉整個程式來結束無限迴圈。如果運氣好的話在之前有打開除錯模式,那或許還能中斷。但你還是得猜哪裡是合適的中斷點。

加入Unity之前,我有找到一個解決方案解決這種問題,直到我參加了HackWeek並碰到了一些專家,我了解到根本原理與思考更好的解決方法。或許未來這個功能會加入Unity裡面,在那之前,你可以使用今天介紹的方法,或者享受反編譯程式的樂趣,沒問題的。

本測試別用真實專案!


受過良好訓練的專家都知道測試的重要性,因此正式用於專案之前,請先在測試專案中試一下這個小技巧。打開Unity並新建一個空專案,在空的場景中加一個Cube物件再建立一個C#腳本,命名為 “Quicksand”並放在Cube物件上。腳本內容如下:
using UnityEngine;
class Quicksand : public Monobehaviour
{
            public OnMouseDown()
            {
                    while(true)
                    {
                   // "Mind you, you'll keep sinking forever!!", -- My mom
                    }
            }
}


現在點Play後按一下Cube物件。你會發現Unity卡住,不要驚慌,這只是個測試不是實際項目。

腳本已經卡住,Unity似乎也當了。接下來開啟Visual Studio。

為了要保證這個方法有用,你會需要在安裝Visual Studio時勾選C++程式設計語言。在Debug功能表下選擇 Attach to Process(注意:這個選項並不是通常選用的Attaching to Unity)。找到Unity並綁定:



把除錯器加在卡住的Unity後,依次點“Debug > Break all”然後找到Disassembly介面,這裡顯示了主執行緒正在執行的程式。操作步驟見下圖。可能還需要點擊“Show Disassembly”或者一些其它按鈕,這取決於Visual Studio的設定。在我的測試機上,需要點F10進行一次單步除錯來打開Disassembly視窗:


眾所皆知,為了執行效率更高,編寫好的程式往往被編譯成機器語言來執行。這也稱為jit-compiling(即時編譯)。執行的結果可以在Disassembly視圖中查看。如下:



在這個例子中出現了無限迴圈(參考上圖的紅色尖頭)。這裡有一個mov,一個cmp和很多nop然後 jmp 迴圈回了開始的位置。沒有任何出路。

這只是測試專案,在實際情況中,專案的C#程式要更複雜,也更難判斷到底發生了什麼,還好開發者並不需要理解這些,因為技巧就是:不停按F10(只需一步)直到看到“cmp dword ptr [r11], 0″這條指令。它們應該不受限的分散在程式的各個位置,因為它們是除錯的基礎。再執行幾步之後,看到這樣的提示就可以結束了:



幸運的話這裡會出現“Autos”視窗(如果沒有,點Debug > Windows > Auto打開)。視窗裡面顯示了目前正在註冊執行中的值:


現在只需將R11的值設為0,如下:



現在執行cmp指令,它會嘗試讀取記憶體位址為0的資料,這將導致異常。這也正是我們想要的,所以接下來按F5鍵讓程式繼續執行,並在彈出對話方塊中點擊“Continue”繼續:



如果一切順利,此時Unity控制台會顯示(Mono)異常資訊,迴圈已被終止且Unity恢復正常。這時可以先保存專案再看看控制台顯示是哪裡的腳本程式導致的問題。



這樣就中斷了無限迴圈!到這裡建議是存檔並重啟整個Unity,因為我們已經跑到Unity很底層並幹了些壞事。雖然範例是一切正常但建議還是小心為妙。

為什麼可以這樣搞?


之所以可行的原因是因為Mono有內建的腳本除錯系統。它的工作原理是穿插一些即時編譯的程式(實際上是每句C#程式一次)到讀取指定記憶體位址的過程中。也就是上面的“cmp dword ptr [r11], 0”指令。當在除錯模式下對程式進行單步除錯時,系統會將持有該記憶體位址的頁設為唯讀,這將導致每句C#代碼產生一次異常。Mono框架可以從JIT代碼外部捕獲異常並暫停代碼執行。

我們在上面用到的技巧就是將註冊的r11設為0,由於記憶體位址0是不可讀的,如此就不會再產生同類的異常。此時調試器會認為正在進行類似單步除錯的行為,但實際上這裡並未進行除錯,所以這裡會拋出NullReferenceException的異常,我們也會看到很有用的堆疊資訊。非常方便!

這個技巧對於編譯出來的可執行程式同樣適用。將Unity連接到你的遊戲Exe執行檔,全部中斷,找到JIT代碼,強制記憶體讀取失敗即可。只是這裡需要在log檔中查看堆疊資訊。

特別案例


上面只是為了展示而展示的簡單範例。現實遠比展示複雜,可能會遇到各種異常。即使拆包可能存取的也不是“純”JIT的程式。如果C#程式呼叫了任意API,這段程式可能會跑進Unity核心代碼部分。如下:


這裡的程式呼叫了GetPosition。當Call Stack頂部包含真正的函數名稱而非一些天書般的記憶體位址時,這就表示已經脫離Mono或JIT代碼了。這時要點幾次Shift+F11跳出當下步驟直至回到純JIT代碼(大量的nop指令也是純JIT代碼的象徵)。

有時你可以設法在某個主執行緒不活躍的位置中斷Unity。最簡單的解決方式是點contunue(或按F5)然後中斷所有直至主執行緒啟動即可。可能還有更多怪異的情況,但除錯就是這樣,隨機應變吧!

32位元系統?


在32位元系統下也能使用該方式。只是JIT代碼看起來有些區別,如下:


這裡表示從0xB10000的位置讀取資料。為了引發系統頁出錯,就需要實際更改程式,因為這裡的位址是硬編碼到指令中的,不像64位元系統那樣位於註冊器中。打開記憶體視圖(點Debug > Windows > Memory > Memory1)找到指令位址(上圖黃箭頭的位址)0x65163DC。顯示如下:


可以找到位址,然後將從頭開始第四個位元組“b1”改為“00”後點繼續。這會有些作用,但與64位元系統不同,這裡每次跑到這個位置都會導致中斷。

如果是非除錯模式呢?


如果實在不幸,這狀況只會發生在不勾選程式除錯的情況下才會出現的Bug,這就真的要即興發揮了。你可以看看程式然後找到某種方法引起讀取出錯,可能會有新的進展,但這可能不太容易。最後的絕招是通過手動寫入程式,類似cmp eax, dword ptr ds:[0x0]指令,這樣就能像上面那樣知道位址是3b 05 00 00 00 00。可以試試看上面的腳本。先中斷:


最壞的情況出現了,編譯器優化導致只有jmp指令在自己迴圈。這樣就沒有空間加入cmp了(與jmp相關,最多占2個位元組)。這裡不要多想了直接通過記憶體讀取來破壞程式。在記憶體視圖找到位址4D34446,不管是什麼內容都往最上方填充3b 05 00 00 00 00。然後點繼續。本範例(單機遊戲)成功中斷了,可以在log檔中查看堆疊資訊:



這時應該立即關掉遊戲,因為你已經毀掉了腳本的一部分JIT產生的程式,可能遊戲無法正常執行了。但至少可以知道是哪裡出了問題。

有時可以在中斷位置附近發現一些讀取指令。這時可以按右鍵指令並選擇“Set next statement”然後將註冊器設為0,透過這種方式就可以正確產生異常。

結論


透過一點小技巧就能中斷本來無法中斷的無限迴圈。趕緊來試試看吧,這樣你就然後你就可以跟別人臭屁說“想當年我也是玩過反編譯的喔!”。未來正式版的Unity,我們會推出更好的正式方案,敬請期待哦!

2016年5月17日 星期二

Unity2016年度Q1手遊遊戲產業報告



作者:LEON CHEN 原文
Unity Analytics釋出了第二版的"遊戲大數據"報告,詳細的分析了透過我們角度所看到針對平台、裝置和地區所總結的一份資料。相信對於開發者會有實質的幫助。
以下是一個簡單的總結,你也可以下載這份報告:

注意:內文提到的遊戲皆指使用Unity製作的iOS或Android平臺的遊戲。


總覽:

僅在2016第一季的手遊安裝量上,中國以31%的占比高居榜首,更勝美國一籌,甚至比第二至第五名的國家加起來還多。然而針對Android與iOS的安裝量則不同地區差別明顯。
在遊戲安裝總量排行榜前10位的國家中,韓國和巴西在Android平台的遊戲安裝量上占92%並列第一,而iOS平台百分比最高的則是日本占53%。

Android平台持續保持領先地位。在2016年第一季,Android平台佔據全球手機遊戲下載安裝量的81% ,而iOS僅為17%。 

Android持續保持在手遊安裝量上遙遙領先,但iOS使用者升級新系統的頻率更高。90%的iOS用戶都使用8.0(2014年9月發佈)以上的版本,最流行的Android系統則依舊是Android 4.4 和Android 5.0及5.1。

另外,蘋果玩家似乎更願意用iPhone來玩遊戲而非iPad。與iPhone用戶完全相反的是,iPad用戶似乎更願意使用舊的系統版本。

排除平台,各種類型設備的遊戲安裝量又是如何呢?三星以壓倒性的優勢完勝全球其它設備。但中國是個例外,小米在眾多Android品牌中脫穎而出,市場占比21.3%。
每天,越來越多使用Unity製作的遊戲被下載安裝。從2016年1月1日開始到3月31 日,共計219,693(約22萬)款Unity製作的遊戲產生了4,167,678,801(約42億)次下載安裝,並在1,655,093,544 (17億)個設備中被執行。

這些數字表明每天平均在1820萬設備上有4580萬次安裝發生。相比於2015年第四季,安裝使用Unity製作的遊戲的設備量增長了21%,遊戲下載安裝量增加了30%,並且遊戲數量增長了10%。 
這是一個非常巨量的資料蒐集工作。Unity的一貫目標是實現開發大眾化,讓更多開發者使用Unity引擎開發出 高品質的遊戲,幫助開發者解決研發過程中遇到的實際問題,引領開發者獲得商業成功。希望這份報告所帶來的資料能實際幫助到開發者!

2016年5月11日 星期三

Unity對IPv6協定的支援

作者:MANTAS PUIDA 原文連結

Apple最近宣佈從2016年6月1日起“所有送到App Store的APP必須支援IPv6-only的網路。” 由於我們很多開發者都會發佈Unity製作的遊戲到App Store,所以我們花點時間來講解一下Unity對於IPv6-only網路的支援。注意,Apple這項規定針對不管新APP或已在架上的APP。

什麼是IPv6?


維基百科上寫道:“網際網路通訊協定第六版(IPv6)是最新版的網際網路通訊協定,這個通訊協定為網路上的電腦和路由通訊設備提供了身份識別和定位系統。”設備可以在純IPv4網路(前一個標準版本),和純IPv6網路或者兩個協定的混合網路上通行。蘋果的新要求代表所有的APP都必須可以在只使用IPv6協定的網路上運行。

重點是協定上IPv4 和 IPv6兩者之間是不能進行互通的,雖然可以使用其他通道技術在兩種類型的網路間進行資訊傳輸。

Unity支持IPv6嗎?


在iOS平臺上WWW和UnityWebRequest已經完美相容IPv6了,它們基於Apple高層的網路API。過去幾個月我們也在努力讓Unity的.NET/IL2CPP可以支援IPv6-only的網路。我們已知的最新.NET/IL2CPP支援IPv6相關的Bug已於補丁版5.3.4p4中修復。還有一個UNET相關的Bug會在後面幾週發佈的補丁版本修復。在此要感謝Unity社群熱心成員(如 Exit Games)的支持與回饋,我們得以解決了Unity引擎核心部分的一些Bug,並針對以下平臺完成了對IPv6的支援:
  • 編輯器:全平臺(Windows、Mac、Linux) 
  • PC端:全平臺(Windows、Mac、Linux) 
  • iOS 
  • Android 
我們後續還會繼續增加其它平臺的IPv6網路支援。

舊版Unity怎麼辦?


如果你的專案使用WWW或UnityWebRequest API從網路獲取資源,就不用擔心這個問題。但如果是用其他網路方式存取,就要小心測試了。

我們會將.NET/IL2CPP修復更新到一些舊版Unity系列中,計畫將以4.7.2,5.1.5,5.2.5的版本發佈。

現在該做什麼?


首先,你應該將你的遊戲設定為僅使用IPv6網路的模式,然後測試遊戲的所有功能是否正常。Apple已經提供了如何在OS X 10.11(或以上版本)的Mac電腦上設定IPv6網路的指南。這些操作步驟與App Store審查員使用的步驟是一樣的,所以測試IPv6網路時有很好的參考依據。由於IPv4與IPv6網路是互不相容的,請確保只使用IPv6網路來進行測試。如果你的設備同時擁有IPv6和IPv4位址,則socket會使用IPv4來連接網路。

然後,審查你的程式碼,看看是否可能存在下面的問題:

  • 注意IPv4格式位址的使用,應移除程式中所有寫死的IP位址(例如:8.8.4.4)。建議優先使用主機名稱(host name),設備可以根據當時正在使用的網路類型獲得對應的IPv4或IPv6地址。 
  • 注意IPAddress.AddressFamily屬性的用法。如果程式分支是根據該屬性值建立的,是否有專門處理IPv6分支的程式碼呢? 
  • 注意IPAddress.AnyIPAddress.Loopback屬性,這些欄位是為IPv4準備的。如需使用IPv6版本,請使用IPAddress.IPv6AnyIPAddress.IPv6Loopback欄位。 
如果遇到上述問題,請確保更新到已經修復該問題的Unity版本。如果你所使用的系列(4.7.x, 5.1.x, 5.2.x)暫未有修復,請待我們發佈補丁版本後再升級。

一些協力廠商的原生或託管的第三方程式可能也與IPv6網路不相容,這種情況下請聯繫第三方程式廠商諮詢相關的相容資訊。

最後,如果遇到任何與IPv6相關的Unity Bug請及時向我們回報,我們會儘快進行修復。針對App Store規定相關的問題,請訪問論壇的iOS和tvOS開發版塊,Unity官方工程師會及時解答大家的問題。

2016年5月4日 星期三

記憶體除錯經驗:到底是誰在我的堆疊裡寫了個“2”?!

作者:TAUTVYDAS ZILYS 原文連結

Hi! 我叫Tautvydas,是Unity官方Windows團隊的一名軟體工程師。今天這篇文章將為大家分享一個難以循跡的記憶體除錯Bug故事。


幾個星期之前我們收到了客戶的一個Bug回報,說他們的遊戲在使用IL2CPP時當機了。QA確認了這個Bug並將這Bug分配給我來修。這個專案很大(當然還不是最大的),在我的機器上打包這個專案就花了40分鐘。Bug報告上的操作指示是這麼寫的“玩這個遊戲5-10分鐘直到它當機”。果然在遵照指示玩了一段時間後遊戲當了。我打開WinDbg準備定位Bug位置。不幸的是堆疊追蹤是有問題的:

0:049> k
# Child-SP RetAddr Call Site
00 00000022`e25feb10 00000000`00000010 0x00007ffa`00000102
0:050> u 0x00007ffa`00000102 L10
00007ffa`00000102 ?? ???
^ Memory access error in ‘u 0x00007ffa`00000102 l10’


顯然系統在嘗試執行一個無效的記憶體位址。儘管堆疊回溯(stacktrace)已毀損,我希望只是整個堆疊裡的部分毀損了,這樣我還可以重過去的記憶體堆疊指標來重建它。這讓我意識到下一步應該查看哪裡:

0:049> dps @rsp L200
……………
00000022`e25febd8 00007ffa`b1fdc65c ucrtbased!heap_alloc_dbg+0x1c [d:\th\minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp @ 447]
00000022`e25febe0 00000000`00000004
00000022`e25febe8 00000022`00000001
00000022`e25febf0 00000022`00000000
00000022`e25febf8 00000000`00000000
00000022`e25fec00 00000022`e25fec30
00000022`e25fec08 00007ffa`99b3d3ab UnityPlayer!std::_Vector_alloc<std::_Vec_base_types<il2cpp::os::PollRequest,std::allocator<il2cpp::os::PollRequest> > >::_Get_data+0x2b [ c:\program files (x86)\microsoft visual studio 14.0\vc\include\vector @ 642]
00000022`e25fec10 00000022`e25ff458
00000022`e25fec18 cccccccc`cccccccc
00000022`e25fec20 cccccccc`cccccccc
00000022`e25fec28 00007ffa`b1fdf54c ucrtbased!_calloc_dbg+0x6c [d:\th\minkernel\crts\ucrt\src\appcrt\heap\debug_heap.cpp @ 511]
00000022`e25fec30 00000000`00000010
00000022`e25fec38 00007ffa`00000001
……………
00000022`e25fec58 00000000`00000010
00000022`e25fec60 00000022`e25feca0
00000022`e25fec68 00007ffa`b1fdb69e ucrtbased!calloc+0x2e [d:\th\minkernel\crts\ucrt\src\appcrt\heap\calloc.cpp @ 25]
00000022`e25fec70 00000000`00000001
00000022`e25fec78 00000000`00000010
00000022`e25fec80 cccccccc`00000001
00000022`e25fec88 00000000`00000000
00000022`e25fec90 00000022`00000000
00000022`e25fec98 cccccccc`cccccccc
00000022`e25feca0 00000022`e25ff3f0
00000022`e25feca8 00007ffa`99b3b646 UnityPlayer!il2cpp::os::SocketImpl::Poll+0x66 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\socketimpl.cpp @ 1429]
00000022`e25fecb0 00000000`00000001
00000022`e25fecb8 00000000`00000010
……………
00000022`e25ff3f0 00000022`e25ff420
00000022`e25ff3f8 00007ffa`99c1caf4 UnityPlayer!il2cpp::os::Socket::Poll+0x44 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\socket.cpp @ 324]
00000022`e25ff400 00000022`e25ff458
00000022`e25ff408 cccccccc`ffffffff
00000022`e25ff410 00000022`e25ff5b4
00000022`e25ff418 00000022`e25ff594
00000022`e25ff420 00000022`e25ff7e0
00000022`e25ff428 00007ffa`99b585f8 UnityPlayer!il2cpp::vm::SocketPollingThread::RunLoop+0x268 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 452]
00000022`e25ff430 00000022`e25ff458
00000022`e25ff438 00000000`ffffffff
……………
00000022`e25ff7d8 00000022`e25ff6b8
00000022`e25ff7e0 00000022`e25ff870
00000022`e25ff7e8 00007ffa`99b58d2c UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint+0xec [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 524]
00000022`e25ff7f0 00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff7f8 00007ffa`99b57700 UnityPlayer!il2cpp::vm::FreeThreadHandle [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 488]
00000022`e25ff800 00000000`0000106c
00000022`e25ff808 cccccccc`cccccccc
00000022`e25ff810 00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff818 000001c4`1705f5c0
00000022`e25ff820 cccccccc`0000106c
……………
00000022`e25ff860 00005eaa`e9a6af86
00000022`e25ff868 cccccccc`cccccccc
00000022`e25ff870 00000022`e25ff8d0
00000022`e25ff878 00007ffa`99c63b52 UnityPlayer!il2cpp::os::Thread::RunWrapper+0xd2 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\thread.cpp @ 106]
00000022`e25ff880 00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff888 00000000`00000018
00000022`e25ff890 cccccccc`cccccccc
……………
00000022`e25ff8a8 000001c4`15508c90
00000022`e25ff8b0 cccccccc`00000002
00000022`e25ff8b8 00007ffa`99b58c40 UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\vm\threadpool.cpp @ 494]
00000022`e25ff8c0 00007ffa`9da83610 UnityPlayer!il2cpp::vm::g_SocketPollingThread
00000022`e25ff8c8 000001c4`155a5890
00000022`e25ff8d0 00000022`e25ff920
00000022`e25ff8d8 00007ffa`99c19a14 UnityPlayer!il2cpp::os::ThreadStartWrapper+0x54 [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\threadimpl.cpp @ 31]
00000022`e25ff8e0 000001c4`155a5890
……………
00000022`e25ff900 cccccccc`cccccccc
00000022`e25ff908 00007ffa`99c63a80 UnityPlayer!il2cpp::os::Thread::RunWrapper [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\thread.cpp @ 80]
00000022`e25ff910 000001c4`155a5890
……………
00000022`e25ff940 000001c4`1e0801b0
00000022`e25ff948 00007ffa`e6858102 KERNEL32!BaseThreadInitThunk+0x22
00000022`e25ff950 000001c4`1e0801b0
00000022`e25ff958 00000000`00000000
00000022`e25ff960 00000000`00000000
00000022`e25ff968 00000000`00000000
00000022`e25ff970 00007ffa`99c199c0 UnityPlayer!il2cpp::os::ThreadStartWrapper [ c:\users\tautvydas\builds\bin2\il2cppoutputproject\il2cpp\libil2cpp\os\win32\threadimpl.cpp @ 26]
00000022`e25ff978 00007ffa`e926c5b4 ntdll!RtlUserThreadStart+0x34
00000022`e25ff980 00007ffa`e68580e0 KERNEL32!BaseThreadInitThunk
這是對追蹤堆疊的粗略重建:
00000022`e25febd8 00007ffa`b1fdc65c ucrtbased!heap_alloc_dbg+0x1c […\appcrt\heap\debug_heap.cpp @ 447]
00000022`e25fec28 00007ffa`b1fdf54c ucrtbased!_calloc_dbg+0x6c […\appcrt\heap\debug_heap.cpp @ 511]
00000022`e25fec68 00007ffa`b1fdb69e ucrtbased!calloc+0x2e […\appcrt\heap\calloc.cpp @ 25]
00000022`e25feca8 00007ffa`99b3b646 UnityPlayer!il2cpp::os::SocketImpl::Poll+0x66 […\libil2cpp\os\win32\socketimpl.cpp @ 1429]
00000022`e25ff3f8 00007ffa`99c1caf4 UnityPlayer!il2cpp::os::Socket::Poll+0x44 […\libil2cpp\os\socket.cpp @ 324]
00000022`e25ff428 00007ffa`99b585f8 UnityPlayer!il2cpp::vm::SocketPollingThread::RunLoop+0x268 […\libil2cpp\vm\threadpool.cpp @ 452]
00000022`e25ff7e8 00007ffa`99b58d2c UnityPlayer!il2cpp::vm::SocketPollingThreadEntryPoint+0xec […\libil2cpp\vm\threadpool.cpp @ 524]
00000022`e25ff878 00007ffa`99c63b52 UnityPlayer!il2cpp::os::Thread::RunWrapper+0xd2 […\libil2cpp\os\thread.cpp @ 106]
00000022`e25ff8d8 00007ffa`99c19a14 UnityPlayer!il2cpp::os::ThreadStartWrapper+0x54 […\libil2cpp\os\win32\threadimpl.cpp @ 31]
00000022`e25ff948 00007ffa`e6858102 KERNEL32!BaseThreadInitThunk+0x22
00000022`e25ff978 00007ffa`e926c5b4 ntdll!RtlUserThreadStart+0x34 
很了,現在我知道哪個執行緒崩潰了,就是IL2CPP執行時runtime socket輪詢處理執行緒。它的工作是當其它執行緒的socket準備好發送或接收資料時通知這些執行緒。運行機制是這樣的:有一個先進先出佇列來放置其它執行緒的socket輪詢請求。Socket輪詢處理執行緒會一個接一個從佇列中取出這些請求,呼叫select() 函數,當select()函數返回一個結果時,它會將執行緒池中原始請求的callback函數放入佇列。 

所以這裡一定有什麼東西在作怪。為了縮小範圍,我決定在這個執行緒裡大部分堆疊中放入“堆疊定位”。這是它的結構:

當這個結構體被建立時,它會向暫存中寫入 “0xDD”。被釋放時會檢查這些值有沒有變化。這個方法OK,遊戲不再當了!但是會出現斷言(asserting):


有甚麼東西觸發了堆疊定位的私有變數 - 這傢伙絕對不是甚麼好東西。我跑了好幾次結果都是一樣,它每次都會先在暫存中寫入一個“2”。查看記憶體圖,我發現看到的內容其實很熟悉:



這些值與一開始堆疊被篡改時看到的值是一樣的。我意識到之前導致程式當掉東西也正想修改定位。我先想到的是有某種緩衝區溢位,對記憶體的寫入超出了變數範圍。所以我開始在這個執行緒中的每個函式呼叫之前放置更多的定位。但是這個破壞行為似乎是隨機出現的,這方法無法找出導致這些問題的原因。

我發現當執行到其中一個定位時總會產生記憶體篡改。我需要能夠當場抓到導致記憶體被篡改的原因。所以我讓定位中的記憶體在整個生命週期都是唯讀的,所以我在建構函式中呼叫VirtualProtect() 將記憶體頁標記唯讀,在釋放時標記為可寫:

讓我驚訝的是,記憶體仍然會被篡改!Log如下:

Memory was corrupted at 0xd046ffeea8. It was readonly when it got corrupted.
CrashingGame.exe has triggered a breakpoint.

對我來說這是一個危險信號。居然有人有更改唯讀記憶體的權利,或者是在我設定唯讀之前就篡改了記憶體。因為我的存取權限是正常的,我只能假設是後者,所以我修改了程式來檢查記憶體內容是否在設定了值之後被修改:


我的懷疑得到了證實:

Memory was corrupted at 0x79b3bfea78.
CrashingGame.exe has triggered a breakpoint.

這時我想:“好吧,肯定是另外一個執行緒改了我的堆疊。肯定是。對吧?”唯一的調查途徑是使用資料(記憶體)中斷點來找出犯人。不幸的是在x86下同時只能監控四個記憶體位元,也就是說我只能監控最多32個位元組,而被篡改的記憶體的範圍有16KB。所以我需要找出在哪裡設置中斷點最好。我觀察篡改模式。首先,篡改似乎是隨機的,但這僅僅只是一個錯覺,這是由於ASLR(英文)的特性:每次我啟動遊戲,堆疊會被放置到記憶體的隨機位置,所以篡改的位置肯定是不同的。當我意識到這點之後,我就沒有在每次發現記憶體篡改後重啟遊戲,而是繼續執行。我發現在每次除錯過程中被篡改的記憶體位置是固定的。這就是說,一旦一次篡改過後,只要程式沒有中止,篡改都會發生在相同的記憶體位址:

Memory was corrupted at 0x90445febd8.
CrashingGame.exe has triggered a breakpoint.
Memory was corrupted at 0x90445febd8.
CrashingGame.exe has triggered a breakpoint.

我在這個記憶體位址加了中斷點不斷的觀察每次進入時的值0xDD。Visual Studio可以設置中斷點條件:所以我設定只在值是2的時候才中斷:


一分鐘之後,終於到了中斷點。我在除錯了3天之後才到達這一步。我終究取得了勝利,宣佈“終於逮到你了!”,我很樂觀的這麼認為:


接下來我充滿懷疑的看著除錯器,心裡不但沒有答案反而湧現更多疑問:“什麼?這怎麼可能?我要瘋掉了。。。”,我決定看一下反組譯的碼:



果然,它在修改這段記憶體的內容。但是它是寫入0xDD,不是0x02!通過查看記憶體視窗,發現整個區域都被篡改了:


我都快要用頭去撞牆了!我叫我的同事過來看看是不是漏掉了什麼。我們一起檢查了程式,卻找不出一點點可能導致這個奇怪問題的原因。然後我退一步嘗試想像可能會導致除錯器認為那段程式不是寫入“2”的原因。然後我得到了這麼一個假設的流程:
  1. mov byte ptr [rax], 寫入0DDh 到記憶體,CPU中斷執行來讓除錯工具檢視程式狀態 
  2. 記憶體被什麼東西篡改 
  3. 除錯工具檢視記憶體位址,發現裡面的值是“2”並且認為這個就是修改後的值 
那麼,當整個程式被除錯器凍結的時候誰可以修改記憶體內容呢?就我所知有兩種可能:另外一個緒或是作業系統核心幹的。對於這兩種情況傳統除錯工具無法查出,我們要使用Kernel除錯工具。

還好在Windows中設定
Kernel除錯非常容易。你需要兩台機器:一台執行除錯工具,另外一台被除錯的電腦,打開你要工作列視窗,然後輸入:


Host IP是執行除錯工具的機器的IP位址。它會使用指定埠來連接。埠可以選擇49152和65535之間的任意值。在輸入第二個命令並按下Enter,視窗會顯示金鑰(圖片已修改),這個金鑰會作為連接到除錯工具的密碼。在完成上述步驟後,重啟機器。
在另外一台機器上,打開WinDbg,點功能表中的File -> Kernel Debug,然後輸入埠和金鑰。


如果一切正常,當你按下Debug -> Break後會中斷執行。這時被除錯的機器會凍結,按下”g”會繼續執行。

然後我啟動了遊戲並且等著被中斷,這樣就可以找到被篡改的記憶體位址。

Memory was corrupted at 0x49d05fedd8.
CrashingGame.exe has triggered a breakpoint.

很好,現在我知道該在哪裡設定中斷點了,我從Kernel除錯工具來設定:

kd> !process 0 0
PROCESS ffffe00167228080
SessionId: 1 Cid: 26b8 Peb: 49cceca000 ParentCid: 03d8
DirBase: 1ae5e3000 ObjectTable: ffffc00186220d80 HandleCount:
Image: CrashingGame.exe
kd> .process /i ffffe00167228080
You need to continue execution (press ‘g’ ) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> g
Break instruction exception – code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff801`7534beb0 cc int 3
kd> .process
Implicit process is now ffffe001`66e9e080
kd> .reload /f
kd> ba w 1 0x00000049D05FEDD8 “.if (@@c++(*(char*)0x00000049D05FEDD8 == 2)) { k } .else { gc }”


一段時間過後,終於進入中斷點了。

# Child-SP RetAddr Call Site
00 ffffd000`23c1e980 fffff801`7527dc64 nt!IopCompleteRequest+0xef
01 ffffd000`23c1ea70 fffff801`75349953 nt!KiDeliverApc+0x134
02 ffffd000`23c1eb00 00007ffd`7e08b4bd nt!KiApcInterrupt+0xc3
03 00000049`d05fad50 cccccccc`cccccccc UnityPlayer!StackSentinel::StackSentinel+0x4d […\libil2cpp\utils\memory.cpp @ 21]

OK,這裡發生什麼事呢?堆疊定位在很開心的設定數值之後就出現了一個硬體中斷,然後它會做一些例行公事,並且將“2”寫入到我的堆疊中。挖喔! 好吧,因為某種原因Windows在篡改我的記憶體,但為什麼

一開始,我以為是因為我們呼叫了一些Windows API並且傳遞了非法的參數。所以我又掃過了整個socket輪詢執行緒程式,發現唯一的系統呼叫就是select()函數。我查閱MSDN,花了一個小時閱讀select()函數的文檔並且重新檢查確認所有的東西我們都做對了。確實沒有什麼問題,文檔中沒有任何地方說:“如果你傳遞了錯誤的參數,我們就會往你的堆疊裡面寫入2”。似乎所有的東西都是對的。

在做完所有的嘗試之後,我決定透過除錯工具進到select()函數內部,單步執行程式瞭解它是如何運作的。這個動作花了我幾個小時,但我終究完成了。看起來select()函數包裝了WSPSelect(),WSPSelect()函數大概是這個樣子的:

auto completionEvent = TlsGetValue(MSAFD_SockTlsSlot);
/* setting up some state

*/
IO_STATUS_BLOCK statusBlock;
auto result = NtDeviceIoControlFile(networkDeviceHandle, completionEvent, nullptr, nullptr, &statusBlock, 0x12024,
buffer, bufferLength, buffer, bufferLength);
if (result == STATUS_PENDING)
WaitForSingleObjectEx(completionEvent, INFINITE, TRUE);
/* convert result and return it

*/

這裡最重要的部分是對NtDeviceIoControlFile()的呼叫,事實上呼叫它會傳遞本地變數statusBlock作為一個傳出參數,最後它使用一個alertable wait來等待事件信號。到目前為止都沒問題:它呼叫了內核函數,該函數在當它無法立即完成請求時返回STATUS_PENDING。在這種情況下,WSPSelect()會等待直到事件被設定。一旦NtDeviceIoControlFile()完成,它會將結果寫入到statusBlock變數中並且設定事件。等待結束然後WSPSelect()返回。
IO_STATUS_BLOCK結構體是這樣的:


IO_STATUS_BLOCK struct looks like this:
typedef struct _IO_STATUS_BLOCK
{
union
{
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

在64位元環境下面,該結構體長度為16位元組。這引起了我的注意,因為這個結構體符合我的記憶體篡改模式:頭4個位元組被篡改(NTSTATUS長度為4位元組),接著4個位元組被跳過(PVOID所佔據的空間)然後最後的8個或更多位元組被篡改。如果這就是寫入記憶體中內容,那麼頭四個位元組應該包含結果狀態的值。頭4個被篡改的位元組的內容總是0x00000102。這個似乎是...STATUS_TIMEOUT的錯誤碼。如果WSPSelect()沒有等待NtDeviceIOControlFile()結束,這個就說的通。但它等待了。

在瞭解了select()函數的工作機制後,我決定全面瞭解下socket輪詢執行緒是如何工作的。結果讓我像被成噸的磚頭砸中了一樣!

當另外一個執行緒向socket輪詢執行緒推送一個需要處理的socket時,socket輪詢執行緒會呼叫select()函數,因為select()是一個阻擋呼叫(Blocking call),當另外一個socket推送到socket輪詢執行緒佇列中以後,就需要以某種方式中斷select()執行,這樣新的socket可以被儘快處理。如何中斷select()函數呢?似乎我們在select()被阻塞時使用了QueueUserAPC() 來執行非同步過程...然後就會異常!然後會打開堆疊,執行更多的程式碼,然後未來在某個點上核心完成它的工作並且將結果寫入本地變數statusBlock中(但在這個點上變數實際上已經不存在了)。如果正好命中堆疊中的一個返回位址,就會發生當機。

修復方法很簡單:不使用QueueUserAPC(),我們建立了一個loopback socket,在需要中斷select()時會向它發送一個位元組。這種方法在POSIX上已經使用了相當長一段時間,現在也應用到了Windows中。這個Bug會在Unity 5.3.4p1中修復。

這是讓人夜不能寐的Bug之一 。它花費了我5天時間來解決,或許是我碰到過最困難的Bug之一。而帶來的深刻教訓就是:鄉親們! 
系統呼叫絕對不要在非同步情況下拋出異常,可能會干涉到其他執行緒!

2016年5月3日 星期二

Unite 2016 - Unity 5.4 GPU Instancing 功能簡介

作者:蔡元星 原文連結

Unity在5.4 Beta版本中引進了一種新的Draw Call Batching方式-GPU Instancing。當場景中有大量使用相同材質和網格的物體時,通過GPU Instancing可以大幅降低Draw Call數量。本文將為大家簡單介紹GPU Instancing的原理並介紹修改現有Shader來啟用Instancing。

什麼是GPU Instancing?

GPU Instancing是指由GPU和圖形API支援,用一個Draw Call同時繪製多個Geometry相同的物體的技術。


上圖中的場景有數千塊隕石,但只有三種隕石模型,這種情況下使用Instancing之後只需要幾十個Draw Call。

在D3D11中,Instanced Draw Call API如下所示:



注意前兩個參數:IndexCountPerInstance和InstanceCount,這是不同於一般Draw Call API的地方。你需要告訴D3D每個Instance用到多少個頂點索引以及這個Draw Call要畫多少個Instances。

那麼如何做到像上圖中那樣每塊石頭都有不同的位置、旋轉和大小呢?在使用Instancing時,我們一般會把世界矩陣這種每個Instance獨有的資料放到一個額外的Buffer中以供Shader呼叫,可以是第二個Vertex Buffer,也可以是Constant Buffer。

Instancing的應用場景


Instancing技術並不代表一定能提高性能,所以必需明白Instancing技術可以和不能做什麼。

Instancing能做什麼:
  • 通過減少Draw Call數量來降低CPU開銷。 
Instancing不能做什麼:
  • 減少GPU的負載。實際上,Instancing還會在GPU上帶來一些額外的開銷。
具體來說,如果你的場景具備以下條件,使用Instancing可能會給你帶來效能提升:
  • 有大量使用相同材質和相同網格的物體 
  • 效能受制於過多的Draw Call (圖形驅動在CPU上負載過大) 
在實際的遊戲專案中,最合適使用Instancing來優化的是植物植被。因為通常這系統需要繪製大量相同的樹木和草,使用Instancing之後Draw Call的消耗會大幅降低。

在Unity 5.4中使用Instancing


在Unity 5.4中使用Instancing需要注意:
  • 類似於Static / Dynamic Batching,Instancing是一種新的合併Draw Call的方式 
  • 適用於MeshRenderer組件和Graphics.DrawMesh() 
  • 需要使用相同的Material和Mesh 
  • 需要把Shader改成Instanced的版本 
  • 當所有前提條件都滿足時,Instancing是自動進行的,並且比Static/Dynamic Batching有更高的優先順序 

Instancing的實現

Instancing的實現步驟如下:
  • 將Per-Instance Data(世界矩陣、顏色等自訂屬性)打包成Uniform Array,存儲在Instance Constant Buffers中
  • 對於可以使用Instancing的Batch,呼叫各平臺圖形API的Instanced Draw Call,這樣會為每一個Instance產生一個不同的SV_InstanceID 
  • 在Shader中使用SV_InstanceID作為Uniform Array的索引獲取當下的Instance的Per-Instance Data 

如何修改Shader以支持Instancing

1.自訂Vertex / Fragment Shader

下面的程式碼展示如何把一個簡單的Unlit Shader修改為支援Instancing的版本。紅色字體的部分是在已有Shader基礎上需要增加或修改的地方。




下面我們來逐一解釋每一處的修改是什麼意思。 

#pragma multi_compile_instancing

“multi_compile_instancing”會使你的Shader產生兩個Variant,其中一個定義了Shader關鍵字INSTANCING_ON,另外一個沒有定義此關鍵字。 

除了這個#pragma指令,下面所列其他的修改都是使用了在UnityInstancing.cginc裡定義的巨集(此cginc檔位於Unity_Install_Dir\Editor\Data\CGIncludes)。取決於關鍵字INSTANCING_ON是否被定義,這些巨集將展開為不同的代碼。 

UNITY_INSTANCE_ID

用於在Vertex Shader輸入 / 輸出結構中定義一個語義為SV_InstanceID的元素。

UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END
每個Instance獨有的屬性必須定義在一個遵循特殊命名規則的Constant Buffer中。使用這對巨集來定義這些Constant Buffer。“name”參數可以是任意字串。

UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

定義一個具有特定類型和名字的每個Instance獨有的Shader屬性。這個巨集實際會定義一個Uniform陣列。 

UNITY_SETUP_INSTANCE_ID(v)

這個巨集必須在Vertex Shader的最開始呼叫,如果你需要在Fragment Shader裡存取Instanced屬性,則需要在Fragment Shader的開始也用一下。這個巨集的目的在於讓Instance ID在Shader函數裡也能夠被訪問到。

UNITY_TRANSFER_INSTANCE_ID(v, o)

在Vertex Shader中把Instance ID從輸入結構拷貝至輸出結構中。只有當你需要在Fragment Shader中訪問每個Instance獨有的屬性時才需要寫這個巨集。 

UNITY_ACCESS_INSTANCED_PROP(_Color)

存取每個Instance獨有的屬性。這個巨集會使用Instance ID作為索引到Uniform陣列中去取當下Instance對應的資料。 

最後我們需要提一下UnityObjectToClipPos

在寫Instanced Shader時,通常情況下你並不用在意頂點空間轉換,因為所有內建的矩陣名字在Instanced Shader中都是被重定義過的。比如unity_ObjectToWorld實際上會變成unity_ObjectToWorldArray[unity_InstanceID];UNITY_MATRIX_MVP會變成mul(UNITY_MATRIX_VP, unity_ObjectToWorldArray[unity_InstanceID])。注意到如果直接使用UNITY_MATRIX_MVP,我們會引入一個額外的矩陣乘法運算,所以推薦使用UnityObjectToClipPos / UnityObjectToViewPos函數,它們會把這一次額外的矩陣乘法優化為向量-矩陣乘法。 

2.Surface Shader


如果想把一個Surface Shader改寫成支持Instancing的版本,你只需要加上“#pragma multi_compile_instancing” 就可以了。設定Instance ID的程式會自動產生。定義或存取每個Instance獨有屬性的方法同Custom Vertex / Fragment Shader。

另外,你可以在Project視窗按右鍵,選擇Create->Shader->Standard Surface Shader (Instanced)來建立一個範例Shader。

使用Instancing的限制

下列情況不能使用Instancing:
  • 使用Lightmap的物體 
  • 受不同Light Probe / Reflection Probe影響的物體 
  • 使用包含多個Pass的Shader的物體,只有第一個Pass可以Instancing 
  • 前向渲染時,受多個光源影響的物體只有Base Pass可以instancing,Add Passes不行
另外,由於Constant Buffer的尺寸限制,一個Instanced Draw Call能畫的物體數量是有上限的(參見UnityInstancing.cginc中的UNITY_MAX_INSTANCE_COUNT)

最後需要再次強調的是,Instancing在Shader上有額外的開銷,並不代表一定能提高FPS。永遠要以實際Profiling結果為準

5.4 Beta目前支持Instancing的平臺

  • Windows: DX11 / DX12 with SM 4.0以及更高 
  • OS X & Linux: OpenGL 4.1以及更高
  • PlayStation 4 
  • 手機移動平臺和其他主機平臺會在後續版本支持 

立即下載Unity 5.4 Beta 試用Instancing

這裡下載:所有人都可以使用(包括Personal Edition用戶)
可以到這裡反映意見,以及更多關於Instancing的文件

2016年5月2日 星期一

淺談Unity Profiler記憶體分析

作者:柳振東 原文

 使用Unity開發遊戲的過程中,借助Profiler來分析記憶體使用狀況是非常重要的。但許多開發者還對Profiler裡各項資料表示含義不甚明確,今天我將針對Profiler記憶體分析相關的問題與大家討論分享。 要想完全發揮Profiler分析的威力,首先要做的就是了解Profiler資料所表達的含義,以及到底哪些模組所使用的記憶體才會被統計到Unity的Profiler中。Profiler涉及到的知識有很多,今天先挑選一些大家常有的疑問來作解答。

Q. 在Unity的Profiler中看到的總記憶體使用和我使用其它工具看到的系統記憶體佔用不太一樣,這是為什麼呢?

Profiler中看到的記憶體是通過Unity引擎的記憶體分配,凡是引擎所分配和釋放的記憶體均會記錄,可以給出明確的引擎記憶體佔用資訊。但仍有其他記憶體資訊我們是無法獲知的。

比如,如果引擎使用了第三方廠商的庫,那麼庫分配的記憶體我們是無法進行統計的。另外,在手機設備上大家看到的記憶體,其實都要比Profiler大很多,這是因為不管是通過Xcode的Instrument還是通過Android的USS,記錄實體記憶體都包括兩部分,一個是使用(Used)的實體記憶體,另一個則是緩存的(Cached)的實體記憶體。




這是系統決定的,Android和iOS系統在資源不使用時當下均不會將它回收,而是放在緩存的實體記憶體中,以便下次再用時,可以快速地載入。當系統發現App的記憶體不夠用時,才會在底層呼叫一個Memory Killer執行緒來檢查緩存,為App清理記憶體。
而Unity Profiler記錄的則是目前真實使用的實體記憶體,即上述所說的第一部分。因此,當遊戲執行時間越長,Profiler分配記憶體和通過其他軟體獲得的系統記憶體差距會越大。
因此,只要所使用的第三方的庫不存在記憶體洩露問題,我們一般都建議只需要查看Profiler即可,只要Profiler中的記憶體可以保證正常讀取和釋放,那就代表引擎分配的記憶體是沒有問題的。

Q. 我們做項目的時候發現有時Profiler中System.ExecutableAndDlls這項佔用很多,有什麼解決辦法嗎?

“System.ExecutableAndDlls”顯示的是執行檔和所呼叫的庫(物理、渲染、IO等系統庫)總和。請不用擔心它的數值,因為很多Application均在共用這些庫,並且它對於真實遊戲的記憶體壓力非常小,而且也不會導致系統因為該記憶體來移除掉遊戲。

Q. 為什麼在Profiler中的Simple模式下,Used Total的數值不等於右邊各項Unity, Mono, GfxDriver, FMOD與Profiler總和呢?

其實在Unity中,Used Total的計算公式為Used Total = Unity + Mono + GfxDriver + Profiler + additionalUsedMemory。公式中的additionalUsedMemory項在Profiler中並沒有顯示,因為這一項一般情況下都為0,只在某些特殊發佈平臺下才會有數值(一般Android,PC和IOS都為0)。因此一般情況下Used Total的值就是除FMOD之外各項的總和了。當然,這個規則對於Reserved Total是同樣適用的。

Q. 我們專案的資源主要使用AssetBundle動態載入資源,發現Profiler中Detailed模式下PersistentManager.Remapper一項佔用時多時少,這一項主要是做什麼的呢?

Remapper主要提供檔的持久化存儲,包括各種序列化的Asset,項目的設置檔等,維護檔案系統的中的檔與記憶體中資料的對應關係。

那麼如果專案大量使用AssetBundle的話,在對AssetBundle Unload之前都會需要佔用Remapper記憶體。而Remapper本身的實現使用記憶體池,數值只會變大,為了使Remapper佔用的記憶體保持在一個穩定的數值上,我們需要每次在載入一定數量的AssetBundle之後進行Unload操作,而不要一次把所有AssetBundle都載入後才呼叫Unload。(這樣的操作對維持整個Mono Heap的大小也是至關重要的,因為Mono Heap本身也是只增不減的。)

Q. 我們在Editor中除錯專案的時候發現貼圖的記憶體大小是其本身大小的兩倍,是因為Unity把記憶體和顯存的大小都計算進去了嗎?


其實並不是這個原因,因為Editor本身會保有貼圖的一份記憶體,在Editor裡Profiler會把Editor本身所使用的紋理大小也計算進去,因此會有記憶體變為兩倍的情況。我們並不建議在Editor下對專案進行效能調試,而是在真的機器上跑編譯好的專案,然後連接Profiler進行除錯,只有這樣才能得到真正精確的測試資料。

總結:一般情況下只需Unity Profiler在手,就能窺測到遊戲中各項記憶體使用狀況,當然前提是需要對Profiler各項資料所表達的含義了然於心。並且儘量使用真的機器結合Profiler除錯,這樣才能瞭解到遊戲執行的真實記憶體佔用資料。

更多相關的內容在Unity官方中文社區(forum.china.unity3d.com),敬請保持關注!

著作人