天天看點

【Unity優化】資源管理系列01:Assets, Objects and serialization

注意:

1、教程中的 Objects 和 Assets 僅是為了這篇教程而做的命名,與 Unity API 的同名概念沒有關聯。

2、在 Unity API 中,這篇教程中的 Objects 反而一般被命名為 Assets,比如 AssetBundle.LoadAsset 和 Resources.UnloadUnusedAssets 方法。

3、而這篇教程中的 Assets 一般對應 Unity API 中的 files,很少出現在 Unity 公開的 API 中,一般用于建構相關的代碼,比如 AssetDatabase 和 BuildPipeline。

一、資源導入和引用流程

【非運作時】

1、資源檔案導入Unity

① Importer 導入處理:轉換成平台相關格式,紋理壓縮,以及其他必要的處理。轉換結果緩存在 Library 檔案夾中,下次啟動不需要再次導入處理 。導入處理是重度耗時操作。

② 生成 .meta 檔案,生成 File GUID 并存入 .meta檔案;生成GUID與檔案路徑之間的map映射。将該資源的GUID-路徑映射加入map(資源被導入或運作時load時,都會生成并加入該GUID-路徑映射;當資源在Editor中更改路徑,GUID對應的路徑也會更改)。

③ 生成多個 Objects,在 Editor 中表現為一個父資源和多個子資源,同時生成 Local ID。所有生成的 Objects 序列化到 Library 檔案夾下一個與 GUID 同名的二進制檔案中。

2、Objects 互相引用,表現在序列化檔案中,存儲的是被引用 Object 的 GUID 和 Local ID

【運作時】

1、當有新的 Object 生成時,Unity 将它的 GUID 和 Local ID 轉換為一個整數,該整數叫 InstanceID。目的是為了提升運作時效率。

2、Unity 維護了一個名為 PersistentManager 的緩存,又叫 InstanceID 緩存。緩存維護了 InstanceID、GUID+LocalID,及記憶體中可能存在的 Object 執行個體,這三者之間的聯系。當有新的 Object 被加載或建立時(Assetbundle.Load、new Texture2D),将會被注冊進緩存,同時生成 InstanceID。(補充:按照03頁面的說法,當AB被加載後,其包含的對象已經配置設定了 InstanceID。難道生成ID,和生成條目,不是同一個操作?)

3、當遊戲啟動時,将有三部分被自動注冊進 InstanceID 緩存,分别是:遊戲啟動必要的 Objects、第一個場景中的 Objects,以及 Resources 檔案夾中的 Objects(注意:Resources 檔案夾比較特殊,這裡僅注冊 InstanceID 和 GUID+LocalID,真正的 Object 并沒有被加載進記憶體)。

二、運作時資源生存周期,及 InstanceID 緩存的使用

1、在滿足下面三個條件時,Object 将自動被加載:

① Object 的 InstanceID 被引用

② Object 目前沒有被加載進記憶體

③ 可通過 GUID+LocalID 定位到 Object 的原資料

2、也可通過調用資源加載API來手動加載 Object(eg: AssetBundle.LoadAsset)。當該 Object 被加時,如果它引用了其他資源,Unity将嘗試将每個引用的 GUID+LocalID 轉換為 InstanceID,之後(實際上這些也都是自動處理):

① 如果InstanceID對應的 Object 存在于記憶體,就直接傳回;

② 如果不存在,但是 InstanceID 緩存中存在有效的 GUID+LocalID,則 Object 自動被加載;

③ 如果不存在,且對應的 GUID+LocalID 無效,則 Object 不會被加載;

另外,如果 GUID+LocalID 無法轉換為 InstanceID,則 Object 同樣不會被加載。

Object 不被加載,将會導緻Editor中引用部分顯示Missing,對應的部分可能不顯示,如果是紋理,則會顯示為洋紅色。

3、Obejct 會在下面三種情況下被解除安裝:

① Scene 被破壞性地改變時,比如非additively調用 SceneManager.LoadScene 時;另外當 Object 不被任何腳本字段引用,且不被其他 Object 引用時,調用 Resources.UnloadUnusedAssets 方法,Object 也會被解除安裝。注意,如果被标記為 HideFlags.DontUnladUnusedAsset 和 HideFlags.HideAndDontSave,則不會被解除安裝。

② 來自 Resources 檔案夾的 Objects,可通過 Resources.UnloadAsset 方法被解除安裝。但是要注意,雖然 Object 被解除安裝了,但是它的 InstanceID 和 GUID+LocalID 都會被保留且有效。如果之後有腳本字段或者其他 Object 再次引用了已被解除安裝的 Object,則該 Object 将被自動加載。

(這種描述是很奇怪的。前面提到的遊戲啟動時 Resources 中所有 Objects 都會被注冊進 InstanceID 緩存,但是想要使用,仍然需要調用 Resources.Load 方法加載,這也很奇怪。目前沒看到官方的詳細解釋。但是可以推測,Resources 檔案夾中的所有 Objects 在遊戲啟動時就被加載進記憶體了,Load方法要麼是複制一份,要麼是文法糖,直接傳回已加載 Object;Unload要麼解除安裝的是複制,要麼同樣是文法糖。還有一種可能,一開時注冊 InstanceID 時,并沒有加載 Resouces 檔案夾内資源,僅是注冊 InstanceID 與 GUID+LocalID。目前來看,後一種可能性更大。)

③ 來自 AssetBundle 的 Objects,可通過 AssetBundle.Unload(true) 方法被解除安裝,并且 InstanceID 和 GUID+LocalID 變為無效。此時如果有其他 Object 引用該 Object,将會顯示 Missing;如果是腳本試圖引用該 Object,則會報 NullReferenceException 錯誤。

(注意:如果使用 AssetBundle.Unload(false) 方法,則僅解除安裝 AssetBundle,而不解除安裝已加載進記憶體的 Objects。但是 InstanceID 和 GUID+LocalID 仍然會被無效化。)

三、Assembly 與 MonoScripts

1、當打包項目的時候,所有 Plugins 檔案夾以外的 C# 腳本都會被編譯進 Assembly-CSharp.dll 程式集;所有 Plugins 檔案夾内的腳本都會被編譯進 Assembly-CSharp-firstpass.dll 程式集(正如其名,該程式集會被提前編譯);所有 Editor 檔案夾内的腳本将被編譯進 Assembly-CSharp-Editor.dll 程式集。在 Editor 中選中腳本,Inspector 視窗會顯示該腳本最終會被編譯進的程式集。

2、每個 Monobehaviour 都會引用一個 MonoScript(後面簡稱MS)。MS僅包含三個字元串:assembly名、class名,以及 namespace;用于反過來定位 Monobehaviour 腳本。是以 AssetBundle、Scene、Prefab 等不需要包含腳本代碼,隻包含 MS 即可,之後在需要時再根據 MS 提供的資訊找到對應的腳本。

四、多層級對象的序列化與加載

1、多層級 Unity GameObject(prefab、Scene)在序列化的時候,每個 GameObject(後面簡稱 GO)群組件,都會被單獨的序列化為一塊資料,通過 LocalID 引用來表述組合、層級等關系。而将這些資料加載并執行個體化為多層級 GO,是項比較耗時的工作。

2、加載并執行個體化 GO 時,CPU耗時主要在下面四個步驟:

① 讀取源資料:從硬碟存儲中、AssetBundle中,甚至是其他 GO 中;

② 設定 Transforms 的層級關系;

③ 執行個體化新的 GO 群組件;

④ 在主線程喚醒上一步執行個體化的 GO 群組件。

其中後三步,無論是從已有層級克隆,還是從存儲中加載,耗時基本固定不變。而第一步的耗時,随着 GO 群組件的增多呈線性增加;并且如果是從存儲中讀取,則還要考慮到源資料的讀取時間,讀取時間是以可能會超過執行個體化時間。

3、正如第1點提到的,GO 群組件,都會被單獨的序列化為一塊資料。比如一個UI界面中有30個相同的元素,那麼序列化的時候就會産生30份資料,導緻二進制序列化檔案增大,相應的從硬碟讀取的時間也會增加。應避免這種情況,建議采用子產品化設計,在執行個體化的時候再克隆組裝在一起。并且執行個體化的時候,使用 GameObject.Instantiate 帶父物體參數的重載方法,避免層次的再次配置設定,這會節省5%~10%的執行個體化時間。

繼續閱讀