Assets和Objects
Asset是存儲在硬盤上的文件,保存在Unity項目的Assets文件夾內。比如:紋理貼圖、材質和FBX都是Assets。一些Assets以Unity原生格式保存數據,例如材質。另一些Assets需要通過處理轉換到原生格式,例如FBX。
Object是一系列序列化數據,這些數據描述了具體的資源實例,這可以是Unity使用的任意類型的資源,例如mesh,sprite,audio clip或animation clip。所有的Objects都是UnityEngine.Object的子類。
大部分Object類型都是Unity內置的,但有兩個特殊類型:
1. ScriptableObject允許開發者定義他們自己的數據類型。這些類型能夠由Unity序列化和反序列化,並且在編輯器的Inspector窗口中進行操作。
2. MonoBehaviour提供了鏈接到MonoScript的封裝。MonoScript是Unity的內部數據類型,其中保存了指向在具體的程序集和命名空間中的具體腳本類的引用。MonoScripte不包含任何實際可執行的代碼。
Assets和Objects之間存在一對多的關系:也就是說,Asset文件內能夠包含一個或多個Objects。
內部對象引用
所有的UnityEngine.Objects都可以引用其他的UnityEngine.Objects,被引用的Objects可以和引用的Objects位於同一個Asset文件內,也可以是由其他Asset文件導入的。例如,材質對象通常有一個或多個紋理對象的引用,這些紋理對象通常都是從紋理資源文件導入的(例如PNG或JPG)。
當序列化的時候,這些對象由兩部分分離的數據組成:文件的GUID和Local ID。文件的GUID標記了存儲資源的Asset文件。Local ID是局部唯一的(也就是說,在每個Asset文件中,Local ID都是唯一的),標記了Asset文件中的每個Object。
文件的GUID存儲在.meta文件中。這些.meta文件是Unity第一次導入Assets時生成的,並且和Asset存儲在同一個目錄中。下圖展示了Diffuse材質及其.meta文件:
.meta文件中包含了GUID:
打開材質文件本身,可以看到Local ID:
如果在場景中有對象使用該材質進行渲染,那么打開場景文件后,就會發現該材質對象由GUID以及Local ID來標記:
為什么使用GUID和Local ID?
GUID的功能是提供文件路徑的抽象表示。只要使用GUID來關聯具體的文件,那么文件在磁盤上的位置就無關緊要了。因此可以隨意移動文件而不需要更新引用該文件的Objects(因為這些Objects存儲的都是文件的GUID)。
由於一個Asset文件可能包含多個UnityEngine.Object資源,因此需要用Local ID來明確的標記每個不同的Object。
如果一個Asset文件關聯的GUID丟失的話,那么所有對該Asset文件中的Objects的引用都將丟失。當.meta文件丟失時,Unity會重新生成。
Unity維護了具體文件路徑與GUID的映射關系。當一個Asset被加載或導入時,就會新增一個映射項,該映射項將Asset的文件路徑和Asset文件的GUID連接在一起。如果一個Asset的.meta文件丟失但其文件路徑沒有發生變化的話,Unity能確保重新生成的.meta中記錄的GUID是保持不變的。
如果.meta文件在Unity關閉時丟失,或者Asset文件的路徑發生了變化,但.meta文件沒有跟着一起移動的話,那么所有對該Asset文件中的Objects的引用都將丟失。舉個例子,場景中的Cube使用了我創建的材質Diffuse:
Diffuse材質及其.meta文件存儲在Assets目錄下,如果現在在外部移動Diffuse材質到Assets/Temp目錄下,由於沒有同時移動其.meta文件,因此Cube對其引用就會丟失:
資源及其導入
非Unity原生資源必須導入進Unity中才能使用,這是通過asset importer完成的。這些improter在資源導入時會被自動調用,同時你也可以用AssetImporter及其子類的API來通過代碼調整資源導入過程。
資源導入的結果是一個或多個UnityEngine.Objects。在Unity中你可以看到一個父對象包含多個子對象,例如sprite atlas:。這些對象都共享同一個GUID,因為他們的源數據來自於同一個Asset文件。Unity使用Local ID來區分他們:
資源導入過程包含了十分耗時的操作,例如紋理壓縮。所以如果每次打開Unity都需要執行一遍資源導入過程的話將會十分低效,因此,Unity將資源導入的結果緩存在Library文件夾中:。具體來說,存儲在以Asset文件的GUID前兩個數字命名的文件夾中,這些文件夾位於目錄Library/metadata:
實際上即使是Unity原生資源,也會將導入結果存儲在對應文件中。但是原生資源不需要很長的轉換時間或重新序列化時間。
實例ID
盡管GUID和Local ID健壯耐用,但是GUID的比較很耗時,而在運行時我們需要有個十分高效的系統。因此Unity在內部會維護一份緩存,這份緩存將GUID和Local ID轉換成獨一無二的整數,這些整數被稱為Instance ID,每當有新的Objects添加到緩存中時,Instance ID以簡單的單調遞增的方式進行賦值。緩存維護了Instance ID,GUID和Local ID(這兩個定義了Object的源數據在磁盤上的位置)以及Object在內存中的實例(如果Object已經被加載到內存中的話)之間的映射關系。這樣UnityEngine.Objects就可以維護相互之間的引用關系。通過Instance ID可以快速找到對應的已經加載的Object,如果對應的Object還沒有加載,那么就可以通過GUID和Local ID來找到Object的源數據,然后加載相應的Object。
應用程序啟動時,項目內置對象(比如場景中使用的對象)的數據以及在Resources文件夾中的對象的數據將被初始化到Instance ID緩存中。當運行時有新的資源被導入(比如通過腳本創建的Texture2D對象),以及當從AssetBundle中加載對象時,就會在緩存中添加Instance ID項。Instance ID只有在被認為已經過時的情況下才會從緩存中刪除,這種情況發生在一個AssetBundle被卸載時。當一個AssetBundle被卸載時,除了會導致對應的Instance ID被認為已經過時,Instance ID和GUID以及Local ID之間的映射數據也會被從內存中刪除。如果AssetBundle被重新加載的話,那么從該AssetBundle中加載的每一個對象都會創建一個新的Instance ID。
需要注意的是在具體平台上的一些特定事件會導致Objects從內存中被刪除。比如當iOS上的應用程序被掛起時,圖形資源可能會從顯存中被刪除,如果這些資源是來自一個已經被卸載的AssetBundle,那么Unity就無法重新加載這些資源了,任何對這些資源的引用也將變得無效(例如出現不可見的模型(missing)使用粉色的材質(missing)來渲染)。
MonoScript
一個MonoBehaviour包含了一個對MonoScript的引用,而MonoScript僅僅包含了用於定位到一個具體腳本類所需的信息,他們都不包含腳本類的可執行代碼。
一個MonoScript中包含了三個字符串:一個程序集名,一個類名以及一個命名空間名。
當Unity構建項目時,會將Assets文件夾下的所有腳本文件編譯到Mono程序集中。具體來說,Unity會為在Assets文件夾中使用的每種不同的編程語言編譯一個程序集,並且會將在Assets/Plugins文件夾中的腳本單獨編譯到一個程序集中。在Assets/Plugins文件夾外的C#腳本會被編譯到Assetmbly-CSharp.dll中,在Assets/Plugins文件夾外的Java腳本會被編譯到Assembly-UnityScript.dll中,Assets/Plugins中的腳本會被編譯到Assembly-CSharp-firstpass.dll中。
這些程序集(再加上預編譯的程序集)都會被包含在最終的應用程序中:
這些程序集就是MonoScript引用的程序集。和其他資源不同,所有程序集在應用程序第一次啟動時會被全部加載進來。這種方式也是為什么一個AssetBundle(或者一個Scene、一個Prefab)中不包含掛載的MonoBehaviour組件中的可執行代碼。這種方式使得不同的MonoBehaviour可以引用共同的具體類。
資源生命周期
有兩種加載UnityEngine.Objects的方式:自動加載和顯示的手動加載。當一個Instance ID被解引用,其對應的Object當前沒有加載到內存中,並且Object的源數據能夠被定位到時,Object會被自動加載。Objects還能夠顯示的在腳本中手動加載,例如新建一個Texture2D或通過AssetBundle.LoadAsset方式加載一個Object。
如果一個文件GUID和Local ID沒有對應的Instance ID,或者一個Instance ID對應的Object沒有被加載,並且其對應的GUID和Local ID是無效的話,那么Object就不會被加載,但是引用關系仍舊會被保留,此時在Unity編輯器中就會出現"(Missing)"。
Objects在下面三種具體的情況下會被卸載:
1. 當清理未被引用的Asset時,未被引用的Objects會被自動卸載。當場景切換時或當調用Resources.UnloadUnusedAssets函數時會觸發清理未被引用的Asset。
2. 來自Resources文件夾的Objects在調用Resources.UnloadAsset函數時會被銷毀。但是Instance ID會被保留,所以如果在Object被銷毀后,有任何先前對該對象的引用被解引用時,Unity會重新通過Instance ID找到GUID和Local ID,然后將該對象再次加載進來。
3. 來自AssetBundle的Objects在調用AssetBundle.Unload(true)函數時會被立即銷毀,同時也會使得Instance ID,GUID和Local ID變得無效,任何對該對象的引用也會變成"(Missing)"。之后在C#中任何對該對象的訪問都會引發"NullReferenceException"異常。如果調用AssetBundle.Unload(false),從AssetBundle加載的Objects不會被銷毀,但是Instance ID對應的GUID和Local ID會變得無效,因此如果這些對象被從內存中釋放的話,Unity將無法再次加載他們。
加載大層級對象
當序列化Unity GameObjects(例如Prefabs)時,要記住整個層級都會被序列化。也就是說,層級中每個GameObject及其組件在序列化數據中都會被獨立的表示。因此,加載和實例化具有大層級的GameObjects時會有性能影響。
當實例化GameObjects時,實例化一個具有大層級的GameObject和實例化多個小層級的GameObjects然后將這些GameObjects組合在一起相比,需要耗費更多的CPU時間。盡管實例化一個大層級的GameObject不需要組合GameObjects(不需要trampolining和SendTransformChanged回調)的CPU時間,但這些節約的CPU時間遠遠比不過讀取和反實例化大層級數據的時間。
之前提到,序列化GameObjects時,整個層級中的GameObject及其組件數據都會被序列化 --- 即使這些數據是重復的。比如一個UI中有30個一樣的Button,那么Button數據會被序列化30次。在加載時,這些數據都需要從磁盤上進行讀取,在加載大層級的GameObjects時,文件讀取時間會消耗大量CPU時間。因此,可以把重復對象從整個層級中移出來,再單獨實例化后再組合到整個層級中。