最新文章
泰课在线 | 微信拼团成功后如何获取课程?
08-09 17:57
Unity教程 | 使用ARKit为iOS开发AR应用
07-31 17:23
Unity Pro专业版7折订阅四选一工具包之VR开发与艺术设计
07-28 11:47
网友使用虚幻UE4实现CAVE 多通道立体渲染的沉浸式环境
07-27 11:57
VR晕动症调查:未来5年内大部分VR晕动症将得到解决
07-27 11:26
AMD CEO:未来3-5年最重要 希望5年达1亿VR用户
07-27 10:44
细说Assets Objects和 Serialization
一、Assets和Objects的基本概念
1. Assets
Asset是存储在硬盘上的文件,保存在Unity项目的Assets文件夹内。例如,纹理贴图、材质和FBX文件都属于Assets。部分Assets以Unity原生格式保存数据,像材质;而另一些Assets则需要经过处理转换为原生格式,例如FBX文件。
2. Objects
Object是一系列序列化数据,用于描述具体的资源实例,这些资源可以是Unity使用的任意类型,如mesh、sprite、audio clip或animation clip等。所有的Objects都是UnityEngine.Object的子类。
大部分Object类型是Unity内置的,但有两个特殊类型:
- ScriptableObject:允许开发者自定义数据类型。这些类型可由Unity进行序列化和反序列化,并且能在编辑器的Inspector窗口中操作。
- MonoBehaviour:提供了与MonoScript的封装链接。MonoScript是Unity的内部数据类型,保存了指向具体程序集和命名空间中具体脚本类的引用,但不包含实际可执行代码。
3. Assets和Objects的关系
Assets和Objects之间存在一对多的关系,即一个Asset文件内可以包含一个或多个Objects。
二、内部对象引用
1. 对象引用方式
所有的UnityEngine.Objects都能引用其他的UnityEngine.Objects,被引用的Objects可以和引用的Objects位于同一个Asset文件内,也可以是由其他Asset文件导入的。例如,材质对象通常会引用一个或多个纹理对象,这些纹理对象一般从纹理资源文件(如PNG或JPG)导入。
2. 序列化数据组成
当进行序列化时,这些对象由两部分分离的数据组成:文件的GUID和Local ID。文件的GUID用于标记存储资源的Asset文件,而Local ID在每个Asset文件中是局部唯一的,用于标记Asset文件中的每个Object。
3. GUID和Local ID的存储
文件的GUID存储在.meta文件中,这些.meta文件是Unity第一次导入Assets时生成的,且与Asset存储在同一目录。例如,打开Diffuse材质的.meta文件,可看到其中包含的GUID;打开材质文件本身,则能看到Local ID。如果场景中有对象使用该材质进行渲染,打开场景文件后,会发现该材质对象由GUID和Local ID来标记。
4. 使用GUID和Local ID的原因
- GUID:提供文件路径的抽象表示。只要使用GUID关联具体文件,文件在磁盘上的位置就无关紧要,可随意移动文件而无需更新引用该文件的Objects,因为这些Objects存储的是文件的GUID。
- Local ID:由于一个Asset文件可能包含多个UnityEngine.Object资源,所以需要用Local ID来明确标记每个不同的Object。
5. GUID丢失的影响
如果一个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材质移到Assets/Temp目录下而未移动其.meta文件,Cube对其的引用就会丢失。
三、资源及其导入
1. 资源导入方式
非Unity原生资源必须通过asset importer导入到Unity中才能使用。这些importer在资源导入时会自动调用,也可以使用AssetImporter及其子类的API通过代码调整资源导入过程。
2. 资源导入结果
资源导入的结果是一个或多个UnityEngine.Objects。在Unity中可以看到一个父对象包含多个子对象,如sprite atlas。这些对象共享同一个GUID,因为它们的源数据来自同一个Asset文件,Unity使用Local ID来区分它们。
3. 资源导入缓存
资源导入过程包含一些耗时操作,如纹理压缩。为避免每次打开Unity都执行资源导入过程导致低效,Unity将资源导入的结果缓存在Library文件夹中,具体存储在以Asset文件的GUID前两个数字命名的文件夹中,这些文件夹位于目录Library/metadata。实际上,即使是Unity原生资源,也会将导入结果存储在对应文件中,但原生资源不需要很长的转换时间或重新序列化时间。
四、实例ID
1. 引入原因
尽管GUID和Local ID健壮耐用,但GUID的比较耗时,而在运行时需要高效的系统。因此,Unity在内部维护一份缓存,将GUID和Local ID转换为独一无二的整数,即Instance ID。每当有新的Objects添加到缓存中时,Instance ID以简单的单调递增方式赋值。
2. 缓存映射关系
缓存维护了Instance ID、GUID和Local ID(定义了Object的源数据在磁盘上的位置)以及Object在内存中的实例(如果Object已加载到内存中)之间的映射关系。这样,UnityEngine.Objects就可以维护相互之间的引用关系。通过Instance ID可以快速找到对应的已加载Object,如果对应的Object未加载,可通过GUID和Local ID找到Object的源数据并加载相应的Object。
3. Instance ID的管理
应用程序启动时,项目内置对象(如场景中使用的对象)的数据以及在Resources文件夹中的对象的数据将被初始化到Instance ID缓存中。当运行时有新的资源被导入(如通过脚本创建的Texture2D对象),以及从AssetBundle中加载对象时,会在缓存中添加Instance ID项。Instance ID只有在被认为已经过时的情况下才会从缓存中删除,这种情况发生在一个AssetBundle被卸载时。当一个AssetBundle被卸载时,除了对应的Instance ID被认为过时,Instance ID和GUID以及Local ID之间的映射数据也会从内存中删除。如果AssetBundle被重新加载,从该AssetBundle中加载的每个对象都会创建一个新的Instance ID。
4. 特定事件影响
在具体平台上的一些特定事件会导致Objects从内存中被删除。例如,当iOS上的应用程序被挂起时,图形资源可能会从显存中被删除,如果这些资源来自已卸载的AssetBundle,Unity将无法重新加载这些资源,任何对这些资源的引用也将变得无效(如出现不可见的模型(missing)使用粉色的材质(missing)来渲染)。
五、MonoScript
1. 基本信息
一个MonoBehaviour包含对MonoScript的引用,而MonoScript仅包含用于定位到一个具体脚本类所需的信息,不包含脚本类的可执行代码。一个MonoScript包含三个字符串:程序集名、类名和命名空间名。
2. 脚本编译
当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中。
3. 程序集加载
这些程序集(加上预编译的程序集)都会包含在最终的应用程序中。这些程序集就是MonoScript引用的程序集。与其他资源不同,所有程序集在应用程序第一次启动时会全部加载进来。这也是为什么一个AssetBundle(或一个Scene、一个Prefab)中不包含挂载的MonoBehaviour组件中的可执行代码,这种方式使得不同的MonoBehaviour可以引用共同的具体类。
六、资源生命周期
1. 加载方式
有两种加载UnityEngine.Objects的方式:自动加载和显式的手动加载。当一个Instance ID被解引用,其对应的Object当前未加载到内存中,且Object的源数据能够被定位到时,Object会被自动加载。Objects也可以在脚本中显式地手动加载,例如新建一个Texture2D或通过AssetBundle.LoadAsset方式加载一个Object。
2. 加载失败情况
如果一个文件GUID和Local ID没有对应的Instance ID,或者一个Instance ID对应的Object没有被加载,且其对应的GUID和Local ID无效,那么Object就不会被加载,但引用关系仍会保留,此时在Unity编辑器中会出现"(Missing)"。
3. 卸载情况
Objects在以下三种情况下会被卸载:
- 清理未引用资源:当清理未被引用的Asset时,未被引用的Objects会被自动卸载。场景切换或调用Resources.UnloadUnusedAssets函数时会触发清理未被引用的Asset。
- 销毁Resources文件夹资源:来自Resources文件夹的Objects在调用Resources.UnloadAsset函数时会被销毁,但Instance ID会被保留。所以如果在Object被销毁后,有任何先前对该对象的引用被解引用,Unity会重新通过Instance ID找到GUID和Local ID,然后将该对象再次加载进来。
- 卸载AssetBundle资源:来自AssetBundle的Objects在调用AssetBundle.Unload(true)函数时会被立即销毁,同时Instance ID、GUID和Local ID会变得无效,任何对该对象的引用会变成"(Missing)"。之后在C#中任何对该对象的访问都会引发"NullReferenceException"异常。如果调用AssetBundle.Unload(false),从AssetBundle加载的Objects不会被销毁,但Instance ID对应的GUID和Local ID会变得无效,因此如果这些对象从内存中释放,Unity将无法再次加载它们。
七、加载大层级对象
1. 序列化影响
当序列化Unity GameObjects(如Prefabs)时,整个层级都会被序列化,即层级中每个GameObject及其组件在序列化数据中都会独立表示。因此,加载和实例化具有大层级的GameObjects会有性能影响。
2. 实例化性能比较
实例化一个具有大层级的GameObject和实例化多个小层级的GameObjects然后组合在一起相比,需要耗费更多的CPU时间。尽管实例化一个大层级的GameObject不需要组合GameObjects(不需要trampolining和SendTransformChanged回调)的CPU时间,但这些节约的CPU时间远远比不上读取和反实例化大层级数据的时间。
3. 重复数据问题
序列化GameObjects时,整个层级中的GameObject及其组件数据都会被序列化,即使这些数据是重复的。例如,一个UI中有30个相同的Button,Button数据会被序列化30次。在加载时,这些数据都需要从磁盘上读取,加载大层级的GameObjects时,文件读取时间会消耗大量CPU时间。因此,可以把重复对象从整个层级中移出来,单独实例化后再组合到整个层级中。