天天看點

玩轉Unity資源,對象和序列化(下)

本文将從Unity編輯器和運作時兩個角度出發,主要探讨以下兩方面内容:Unity序列化系統内部細節以及Unity如何維護不同對象之間的強引用。另外還會讨論對象與資源的技術實作差别。

譯注:除非特别說明,下文中所有的“資源”均指代“Asset”。

本文内容是了解在Unity中如何高效加載和解除安裝資源的基礎。正确的資源管理對縮短加載時間并減少記憶體占用來說至關重要。之前已經介紹了 上半部分,今天繼續為大家分享下半部分内容。

盡管檔案GUID和本地ID已準備妥當,它們可以強有力地維護資源之間的關系,但還有一個問題,GUID比較效率低下,我們需要為運作時準備一個效率更高的解決方案。Unity在内部維護着一個緩存表(2),負責将檔案GUID和本地ID轉換成為整數數值,這個數值在本次會話中是唯一的,稱作執行個體ID。執行個體ID會簡單地以單調遞增的方式配置設定給緩存中新注冊的對象。

緩存負責維護執行個體ID與檔案GUID和本地ID定義的對象源資料位置以及對象在記憶體中的位址(如果存在)的映射。 這樣Unity就能夠強而有力地保證它們互相之間的引用關系。通過解析執行個體ID,我們能夠快速找到并傳回ID對應的已載入執行個體。如果目标沒有被加載,則Unity會通過檔案GUID和本地ID解析獲得對象的源資料,實時載入對象。

啟動時,執行個體ID緩存與所有工程内建的對象(例如在場景中被引用),以及Resources檔案夾下的所有對象,都會一起被初始化。如果在運作時(3)導入了新的資源,或從AssetBundle中載入了新的對象,緩存會被更新并為這些對象添加相應條目。執行個體ID僅在失效時才會被從緩存中移除,當提供了指定檔案GUID和本地ID的AssetBundle被解除安裝時就會産生移除操作。

解除安裝AssetBundle會使執行個體ID失效,執行個體ID與其檔案GUID和本地ID之間的映射會被删除以便節省記憶體。重新載入AssetBundle後,載入的每個對象都會獲得一個新的執行個體ID。

關于AssetBundle隐式解除安裝的更深層次讨論,參考AssetBundle Usage Patterns中的管理已加載資源章節。

注意在某些平台上,有一些系統事件會使得對象在記憶體中被強行解除安裝。例如iOS平台上,當一個app處于挂起狀态時,圖形資源就會從顯存中解除安裝。如果這些對象都來自于一個已被卸除的AssetBundle時,Unity将無法再次從源資料處加載這些對象。任何已有的對這些對象的引用都會失效。這個例子中,導緻的後果就是出現網格不可見(丢失),或模型的紋理和材質呈現為洋紅色(Shader丢失)。

提示:在運作時,對上述控制流程的描述并非完全準确。檔案GUID和本地ID的比較操作在載入負擔較重時的效率也會下降。建構Unity工程時,檔案GUID和本地ID,确切地說會被映射到一種更簡單的格式中。不過概念還是大緻相同的,在考慮“運作時”的時候以檔案GUID和本地ID的工作方式為思路還是有一些參考意義的。

這也是在運作時資源的檔案GUID無法被調取的原因。

了解MonoBehaviour很重要的一點是知道它有一個對MonoScript的引用。MonoScript的用途非常簡單,裡面包括了定位某個具體的程式設計類所需要的資訊。這兩種對象都沒有程式類的可執行代碼。

一個MonoScript含有三個字元串:程式庫名稱,類名稱,命名空間。

建構工程時,Unity會收集Assets檔案夾中獨立的腳本檔案并将它們編譯,組成一個Mono程式庫。要特别說明的是,Unity會将Assets目錄中的語言分開編譯,Assets/Plugins目錄中的腳本同理。Plugins子目錄之外的C#腳本會放在Assembly-CSharp.dll中。而Plugins及其子目錄中的腳本則放置在Assembly-CSharp-firstpass.dll中,以此類推。

這些程式庫(加上預編譯好的DLL程式庫)将被包含在最終建構的Unity應用程式中。這些程式庫都會被MonoScript所引用。與其他類型的資源不同,Unity應用程式中的所有應用程式都會在程式第一次啟動時被加載。

MonoScript就是為什麼AssetBundle(或一個場景,一個Prefab)中的MonoBehaviour元件不包含有任何實際可執行代碼的原因。這樣就可以讓不同的MonoBehaviour引用某個共享類,即便這些MonoBehaviour不在同一個AssetBundle中。

UnityEngine.Objects從記憶體中加載或解除安裝的時間點是定義好的。為了縮短程式載入時間,管理應用程式的記憶體足迹,了解UnityEinge.Object的資源生命周期是很重要的。

有兩種加載UnityEngine.Object的方式:自動加載或外部加載。

當對象的執行個體ID與對象本身解引用,對象目前未被加載到記憶體中,而且可以定位到對象的源資料,此時對象會被自動加載。對象也可以外部加載,通過在腳本中建立對象或調用資源加載API來載入對象(例如AssetBundle.LoadAsset)。

對象加載後,Unity會嘗試修複任何可能存在的引用關系,通過将每個引用的檔案GUID和本地ID轉化成為執行個體ID的方式。

一旦對象的執行個體ID被解引用且滿足以下兩個标準時,對象會被強制加載:

執行個體ID引用了一個沒有被加載的對象。

執行個體ID在緩存中存在對應的有效GUID和本地ID。

這種情況通常會在引用被加載并解析後很短的一段時間内發生。

如果檔案GUID和本地ID沒有執行個體ID,或一個已解除安裝對象的執行個體ID引用了非法的檔案GUID和本地ID,則引用本身會被保留,但實際對象不會被加載。在Unity編輯器中表現為“(空)”引用。在運作的應用程式中,或場景視圖裡,“(空)”對象通常以多種方式表示,這取決于丢失對象的類型:網格會變得不可見,紋理呈現為洋紅色等等。

對象被解除安裝有以下三種情況:

閑置資源清理程序開始後,一些對象會被自動解除安裝。該過程通常會在切換場景切不保留原場景(例如調用了非疊加的場景切換API Application.LoadLevel),或者腳本中調用了Resources.UnloadUnusedAssets時自動觸發。該程序僅解除安裝沒有被引用的對象:對象僅在Mono變量不存在對其的引用,且不存在引用該對象的其他活動對象時被解除安裝。

Resources目錄中的對象可以通過調用Resources.UnloadAsset API主動解除安裝。解除安裝後對象的執行個體ID會保持可用狀态,對檔案GUID和本地ID的條目會被保留且仍然有效。如果有Mono變量或其他有指向該對象的活動對象引用了被Resources.UnloadAsset解除安裝的對象,則該對象會在任意有效的引用被解引用時立刻重新加載。

調用AssetBundle.Unload(true) API時,加載自AssetBundle的對象會被立刻自動解除安裝。該操作會釋放對象執行個體ID的檔案GUID和本地ID引用,任何對解除安裝對象的引用都會變成“(空)”引用。C#腳本中,任何試圖通路已解除安裝對象上的方法和屬性都會導緻抛出空引用異常(NullReferenceException)。

如果調用了AssetBundle.Unload(false),被解除安裝的AssetBundle中仍然處于激活狀态的對象不會被回收,但Unity會釋放其執行個體ID的檔案GUID和本地GUID引用。之後假如它們被從記憶體中解除安裝,隻剩下對這些被解除安裝對象額引用,Unity無法再次重新加載這些對象(4)。

當序列化含有大量Unity遊戲對象的結構樹時(例如序列化Prefab),要記住一點,即整個結構樹都會被完全序列化。這就是說,結構樹中的每一個遊戲對象群組件都會在序列化資料中單獨表示。這會對遊戲對象結構的載入與執行個體化的耗時帶來有趣的影響。

假設一個代碼塊執行個體化了一定數量的遊戲對象,結構樹非常龐大的單個Prefab的執行個體化,會比分别執行個體化結構樹的多個子產品然後運作時組合花費更多的CPU時間。

深度分析底層資料後發現,執行個體化和喚醒遊戲對象所花費的CPU時間在各種情況下大緻是相同的,單個獨立的Prefab的執行個體化和喚醒僅需非常少量的CPU時間(因為不需要tranmpolining和SendTransformChanged回調)。但是,這些細小的時間節省相對于花在讀取和序列化資料上的時間是很不值的。

正如之前提到的,序列化單獨的Prefab時,每個遊戲對象和其元件都會被分開序列化——即便資料是重複的。一個有30個獨立元素的UI界面中,Unity會為這些元素序列化30次,這就會産生大量的序列化資料。載入時,所有30個重複元素的遊戲對象群組件在被轉化為新執行個體對象之前都需要從硬碟中進行讀取。正是這裡讨論的讀取時間決定了執行個體化大型Prefab的性能花銷。

Unity支援嵌套Prefab之後,對于有載入大型結構體遊戲對象需求的工程而言,要想減少這類工程的載入時間,可以考慮将大型Prefab中的可重用元素分開存儲到不同的Prefab中,并在運作時對它們進行執行個體化,而不是完全依靠Unity的序列化和Prefab系統。

在檔案中,本地ID是唯一的。即在一個資源檔案中,裡面包含的本地ID都是不重複的。

在内部,這種緩存被稱為PersistentManager。實際的轉換工作在在Unity的C++ Remapper類中進行,Remapper類沒有提供任何C# API調用接口。

運作時建立資源的示例是在腳本中建立Texture2D對象: var myTexture = new Texture2D(1027, 768);

程式運作時對象并沒有被解除安裝卻被從記憶體中移除的情況通常會發生在Unity失去了對圖形内容的控制的時候。例如,當手機應用被挂起并被強制在背景運作。這種情況下,手機作業系統通常會将所有的圖形資源從GPU顯存中強行解除安裝。之後APP再回到前台運作時,Unity不得不重新向GPU上傳需要的材質、着色器和網格資料,以便恢複場景的正常渲染。

到此整個Unity内部資源管理與對象引用及序列化的内容就結束了,希望看完本文的你對如何合理配置設定Unity項目結構都有了比較清晰的概念。

繼續閱讀