赞
踩
本章介绍Unity的序列化系统的深层内部,以及Unity如何在Unity Editor和运行时维护不同对象之间的健壮引用。它还讨论了对象和资产之间的技术区别。这里涉及的主题是理解如何在Unity中有效加载和卸载资产的基础。正确的资产管理对于缩短加载时间和降低内存使用率至关重要。
要了解如何在Unity中正确管理数据,了解Unity如何识别和序列化数据非常重要。第一个关键点是Assets和UnityEngine.Objects之间的区别。
一个资产是磁盘上的文件,存储在资产为Unity项目的文件夹中。纹理,3D模型或音频剪辑是常见的资产类型。某些资产包含Unity原生格式的数据,例如材料。其他资产需要处理为本机格式,例如FBX文件。
甲UnityEngine.Object,或对象以大写“O”,是一组序列化的数据统称为描述资源的特定实例的。这可以是Unity Engine使用的任何类型的资源,例如网格,精灵,AudioClip或AnimationClip。所有对象都是UnityEngine.Object基类的子类。
虽然大多数对象类型都是内置的,但有两种特殊类型。
一个个ScriptableObject提供了一个方便的系统开发人员可以定义自己的数据类型。Unity可以对这些类型进行本机序列化和反序列化,并在Unity Editor的Inspector窗口中进行操作。
一个MonoBehaviour提供链接到一个包装MonoScript。MonoScript是一种内部数据类型,Unity用于保存对特定程序集和命名空间内特定脚本类的引用。该MonoScript并没有包含任何实际的可执行代码。
资产和对象之间存在一对多的关系; 也就是说,任何给定的Asset文件都包含一个或多个Objects。
所有UnityEngine.Objects都可以引用其他UnityEngine.Objects。这些其他对象可以驻留在同一资产文件中,也可以从其他资产文件导入。例如,材质Object通常具有一个或多个对纹理对象的引用。这些纹理对象通常从一个或多个纹理资源文件(例如PNG或JPG)导入。
序列化时,这些引用由两个单独的数据组成:文件GUID和本地ID。文件GUID标识存储目标资源的资产文件。一个本地唯一(1)本地ID标识资产文件中的每个对象,因为资产文件可能包含多个对象。
文件GUID存储在.meta文件中。这些.meta文件是在Unity首次导入资产时生成的,并存储在与资产相同的目录中。
可以在文本编辑器中查看上述标识和引用系统:创建一个新的Unity项目并更改其编辑器设置以公开可见元文件并将资产序列化为文本。创建材质并将纹理导入到项目中。将材质指定给场景中的立方体并保存场景。
使用文本编辑器打开与材料关联的.meta文件。标有“guid”的行将出现在文件顶部附近。此行定义材料Asset的文件GUID。要查找本地ID,请在文本编辑器中打开材料文件。材质Object的定义如下所示:
---!u!21&2100000
材料:
serializedVersion:3
......更多数据......
在上面的示例中,以&符号开头的数字是素材的本地ID。如果此材料Object位于由文件GUID“abcdefg”标识的资产内,则可以将材料Object唯一标识为文件GUID“abcdefg”和本地ID“2100000”的组合。
为什么Unity的文件GUID和本地ID系统是必需的?答案是稳健性,并提供灵活的,独立于平台的工作流程。
文件GUID提供文件特定位置的抽象。只要特定的文件GUID可以与特定文件相关联,该文件在磁盘上的位置就变得无关紧要了。该文件可以自由移动,而无需更新引用该文件的所有对象。
由于任何给定的资产文件可能包含(或通过导入生成)多个UnityEngine.Object资源,因此需要使用本地ID来明确区分每个不同的对象。
如果与资产文件关联的文件GUID丢失,则对该资产文件中所有对象的引用也将丢失。这就是为什么重要的是.meta文件必须保持与相关文件名相同的文件名,并保存在与其关联的资产文件相同的文件夹中。请注意,Unity将重新生成已删除或放错位置的.meta文件。
Unity Editor具有已知文件GUID的特定文件路径的映射。只要加载或导入资产,就会记录映射条目。映射条目将Asset的特定路径链接到Asset的文件GUID。如果在.meta文件丢失且资产路径未更改时Unity编辑器处于打开状态,则编辑器可以确保资产保留相同的文件GUID。
如果在关闭Unity编辑器时丢失.meta文件,或者资产的路径发生更改而.meta文件不随资产一起移动,则对该资产中对象的所有引用都将被破坏。
如“ 内部资产和对象”部分所述,必须将非本机资产类型导入Unity。这是通过资产导入程序完成的。虽然这些导入器通常是自动调用的,但它们也通过AssetImporter API 暴露给脚本。例如,TextureImporter API提供对导入单个纹理资源(如PNG文件)时使用的设置的访问。
导入过程的结果是一个或多个UnityEngine.Objects。这些在Unity编辑器中可见为父资产中的多个子资产,例如嵌套在纹理资产下的多个精灵已导入为精灵图集。每个对象将共享一个文件GUID,因为它们的源数据存储在同一个Asset文件中。它们将通过本地ID在导入的纹理Asset中区分。
导入过程将源资源转换为适合在Unity Editor中选择的目标平台的格式。导入过程可以包括许多重量级操作,例如纹理压缩。由于这通常是一个耗时的过程,导入的资产会缓存在Library文件夹中,因此无需在下次编辑器启动时再次重新导入Assets。
具体来说,导入过程的结果存储在以Asset的文件GUID的前两位数命名的文件夹中。该文件夹存储在Library / metadata /文件夹中。资产中的各个对象被序列化为单个二进制文件,其名称与Asset的文件GUID相同。
此流程适用于所有资产,而不仅仅适用于非本地资产。本机资产不需要冗长的转换过程或重新序列化。
虽然文件GUID和本地ID是健壮的,但GUID比较很慢,并且在运行时需要更高性能的系统。 Unity内部维护一个缓存(2),它将文件GUID和本地ID转换为简单的会话唯一整数。这些被称为实例ID,并且在向缓存注册新对象时以简单,单调递增的顺序分配。
高速缓存维护给定实例ID,文件GUID和定义对象源数据位置的本地ID与内存中对象实例(如果有)之间的映射。这允许UnityEngine.Objects可以稳健地维护彼此的引用。解析实例ID引用可以快速返回由实例ID表示的已加载对象。如果尚未加载目标Object,则可以将File GUID和Local ID解析为Object的源数据,从而允许Unity及时加载对象。
在启动时,Instance ID缓存初始化为项目立即需要的所有对象的数据(即,在构建的场景中引用),以及Resources文件夹中包含的所有对象。 在运行时导入新资产(3)和从AssetBundles加载对象时,会向缓存添加其他条目。仅当卸载提供对特定文件GUID和本地ID的访问权限的AssetBundle时,才会从缓存中删除实例ID条目。发生这种情况时,将删除实例ID,其文件GUID和本地ID之间的映射以节省内存。如果重新加载AssetBundle,将为从重新加载的AssetBundle加载的每个Object创建一个新的 Instance ID。
有关卸载AssetBundle的含义的更深入讨论,请参阅AssetBundle Usage Patterns一文中的Managing Loaded Assets部分。
在特定平台上,某些事件可能会强制对象内存不足。例如,当应用程序挂起时,可以从iOS上的图形内存卸载图形资源。如果这些对象源自已卸载的AssetBundle,则Unity将无法重新加载对象的源数据。对这些对象的任何现有引用也将无效。在前面的示例中,场景可能看起来具有不可见的网格或洋红色纹理。
实施说明:在运行时,上述控制流程并不准确。在重负载操作期间,在运行时比较文件GUID和本地ID将不会充分发挥作用。构建Unity项目时,文件GUID和本地ID确定性地映射为更简单的格式。但是,这个概念仍然是相同的,并且在运行时期间考虑文件GUID和本地ID仍然是一个有用的类比。这也是在运行时无法查询资产文件GUID的原因。
重要的是要了解MonoBehaviour具有对MonoScript的引用,而MonoScripts只包含查找特定脚本类所需的信息。这两种类型的Object都不包含脚本类的可执行代码。
MonoScript包含三个字符串:程序集名称,类名称和命名空间。
构建项目时,Unity会将Assets文件夹中的所有松散脚本文件编译为Mono程序集。插件子文件夹之外的C#脚本放在Assembly-CSharp.dll中。Plugins子文件夹中的脚本放在Assembly-CSharp-firstpass.dll中,依此类推。此外,Unity 2017.3还引入了定义自定义托管程序集的功能。
这些程序集以及预构建的程序集DLL文件包含在Unity应用程序的最终版本中。它们也是MonoScript引用的程序集。与其他资源不同,Unity应用程序中包含的所有程序集都在应用程序启动时加载。
此MonoScript对象是AssetBundle(或场景或预制件)实际上不包含AssetBundle,Scene或预制件中的任何MonoBehaviour组件中的可执行代码的原因。这允许不同的MonoBehaviours引用特定的共享类,即使MonoBehaviours位于不同的AssetBundle中。
为了减少加载时间并管理应用程序的内存占用,理解UnityEngine.Objects的资源生命周期非常重要。在特定和定义的时间将对象加载到内存中或从内存中卸载。
在以下情况下自动加载对象:
映射到该Object的实例ID将被取消引用
Object当前未加载到内存中
可以定位Object的源数据。
也可以通过创建对象或通过调用资源加载API(例如,AssetBundle.LoadAsset)在脚本中显式加载对象。加载对象时,Unity会尝试通过将每个引用的文件GUID和本地ID转换为实例ID来解析任何引用。如果两个条件为真,则在第一次取消引用其实例ID时,将按需加载对象:
实例ID引用当前未加载的Object
实例ID具有在缓存中注册的有效文件GUID和本地ID
这通常在加载和解析引用本身后不久发生。
如果文件GUID和本地ID没有实例ID,或者具有卸载的对象的实例ID引用无效的文件GUID和本地ID,则会保留引用但不会加载实际的对象。这在Unity编辑器中显示为“(缺失)”参考。在正在运行的应用程序中,或在“场景视图”中,“(缺失)”对象将以不同的方式显示,具体取决于其类型。例如,网格看起来是不可见的,而纹理可能看起来是洋红色。
在三个特定场景中卸载对象:
发生未使用的资产清理时,会自动卸载对象。当破坏性地改变场景时(即,非加性地调用SceneManager.LoadScene),或者当脚本调用Resources.UnloadUnusedAssets API 时,将自动触发此过程。此过程仅卸载未引用的对象; 如果没有Mono变量保存对Object的引用,则只会卸载Object,并且没有其他实时对象持有对Object的引用。此外,请注意,不会卸载任何标有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的内容。
可以通过调用Resources.UnloadAsset API 显式卸载源自Resources文件夹的对象。这些对象的实例ID仍然有效,并且仍将包含有效的文件GUID和LocalID条目。如果任何Mono变量或其他Object包含对使用Resources.UnloadAsset卸载的Object的引用,则只要取消引用任何实时引用,就会重新加载该Object。
在调用AssetBundle.Unload(true)API 时,会自动立即卸载源自AssetBundles的对象。这使对象的实例ID的文件GUID和本地ID无效,并且对已卸载对象的任何实时引用将变为“(缺失)”引用。从C#脚本,尝试访问卸载对象上的方法或属性将产生NullReferenceException。
如果调用AssetBundle.Unload(false),则不会销毁从卸载的AssetBundle中获取的活动对象,但Unity将使其实例ID的文件GUID和本地ID引用无效。如果稍后从内存中卸载这些对象并且仍然保留对已卸载对象的实时引用,则Unity将无法重新加载这些对象。(4)
序列化Unity游戏对象的层次结构时,例如在预制序列化期间,重要的是要记住整个层次结构将完全序列化。也就是说,层次结构中的每个GameObject和Component将在序列化数据中单独表示。这对加载和实例化GameObjects层次结构所需的时间产生了有趣的影响。
在创建任何GameObject层次结构时,CPU时间以几种不同的方式使用:
读取源数据(来自存储,来自AssetBundle,来自另一个GameObject等)
设置新变换之间的父子关系
实例化新的GameObjects和组件
在主线程上唤醒新的GameObjects和Components
无论层次结构是从现有层次结构克隆还是从存储加载,后三种时间成本通常都是不变的。但是,读取源数据的时间随序列化到层次结构中的组件和游戏对象的数量线性增加,并且还乘以数据源的速度。
在所有当前平台上,从内存中的其他位置读取数据要快得多,而不是从存储设备加载数据。此外,可用存储介质的性能特征在不同平台之间变化很大。因此,在缓存存储的平台上加载预制件时,从存储器读取预制件序列化数据所花费的时间可能会快速超过实例化预制件所花费的时间。也就是说,加载操作的成本与存储I / O时间有关。
如前所述,在序列化单片预制件时,每个GameObject和组件的数据都是单独序列化的,这可能会复制数据。例如,具有30个相同元素的UI屏幕将具有序列化30次的相同元素,从而产生大量二进制数据。在加载时,必须在从传输到新实例化的对象之前从磁盘读取这30个重复元素中的每一个上的所有游戏对象和组件的数据。此文件读取时间是实例化大型预制件的总体成本的重要因素。大型层次结构应该在模块化块中实例化,然后在运行时拼接在一起。
Unity 5.4注意: Unity 5.4改变了内存中转换的表示。每个根变换的整个子层次结构都存储在紧凑的,连续的内存区域中。在实例化将立即重新定位到另一个层次结构的新GameObject时,请考虑使用新的GameObject.Instantiate接受父参数的重载变量。使用此重载可避免为新GameObject分配根变换层次结构。在测试中,这将实例化操作所需的时间加速约5-10%。
AA本地ID与同一资产文件的所有其他本地ID唯一。↩
在内部,此缓存称为PersistentManager。↩
在运行时创建的Asset的一个示例是在脚本中创建的Texture2D Object,如下所示:var myTexture = new Texture2D(1024, 768);
↩
当Unity失去对其图形上下文的控制时,最常见的情况是在运行时从内存中删除对象而不会被卸载。当移动应用程序被暂停并且应用程序被强制进入后台时,可能会发生这种情况。在这种情况下,移动OS通常会从GPU内存中清除所有图形资源。当应用程序返回到前台时,Unity必须将所有需要的纹理,着色器和网格重新加载到GPU,然后才能恢复场景渲染。↩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。