AssetBundle02

首先附上原文链接:https://unity3d.com/cn/learn/tutorials/topics/best-practices/assets-objects-and-serialization

                                            Assets, Objects和序列化

       这是关于Unity 5中的Asset,Resources和资源管理的系列文章的第二章。

        本章涵盖了Unity序列化系统的深层本质和Unity如何在编辑器和运行时在不同Objects之间维持稳固的关系。同时也讨论了Object和Asset之间的技术区别。文章的主题涵盖了基本概念,用于理解怎样有效的在Unity中加载和卸载资源。正确的Asset管理对于缩短加载时间和减少内存占用至关重要。


1.1内部的Asset和Object

        要想理解怎样的在Unity中正确的管理数据,重要的是去理解Unity是怎样识别和序列化数据的。第一个关键点是Asset和UnityEngine.Objects之间的区别。

        一个Asset是磁盘上的一个文件,存储在Unity项目的Assets文件夹中。纹理,3D模型,或是音频片段是Asset的常见类型。一些Asset包含了Unity原生格式的数据,比如说material。其他的Asset需要被处理成原生格式,比如FBX文件。

        一个UnityEngine.Object,或是有一个大写‘O’的Object,是用来统一的描述特定资源实例的一些序列化数据,它可以是Unity引擎使用的任何类型的资源,比如mesh,sprite,AudioClip或是AnimationClip。所有的Object是UnityEngine.Object基类的子类。

        虽然大多数Object类型都是内置的,但有两种特殊类型。

        1.一个ScriptableObject给开发人员可以定义自己的数据类型提供了一个方便的系统。这些类型可以通过Unity进行自然的序列化和反序列化,并在Unity编辑器的Inspector窗口中进行操作。

        2.一个MonoBehaviour提供了一个链接到MonoScript的包装。MonoScript是Unity用来在特定程序集和名称空间内保持对特定脚本类的引用的内部数据类型。MonoScript中并没有包含任何实际的可执行代码。

          Asset和Object之间有一对多的关系; 也就是说,任何给定的Asset文件都包含一个或多个Object。


1.2Object之间的引用

        所有UnityEngine.Object都可以引用其他UnityEngine.Object。这些其他的Object也许包含在同样的Asset文件中,或许从其他Asset文件中导入。比如说一个material的Object经常有一个或者更多个texture的Object的引用。这些texture的Object通常从一个或多个texture的Asset文件(如PNG或JPG)导入。

        在序列化时,这些引用由两个数据组成,一个File GUID和一个Local ID。File GUID标识了存储目标资源的Asset文件。一个局部与众不同的(AA Local ID在同一个Asset文件中与其他Local ID不同)Local ID标识了一个Asset文件中的每个Object,因为一个Asset文件可能包含多个Object。

        File GUID存储在.meta文件中。这些.meta文件通常在Unity第一次导入Asset时生成,并且和Asset存储在同一个文件目录下。

        上述的标识和引用系统可以在text编辑器中看到,创建一个新的Unity项目并更改其编辑器设置,显示可见meta文件并将Asset序列化为文本。创建一个material并且向项目中导入一个texture,将material指定给一个在场景中的cube并且保存场景。

        使用text编辑器打开与material关联的.meta文件。标有“guid”的行将出现在文件顶部附近,这一行定义了material的Asset的File GUID。要找到Local ID,使用text编辑器打开material文件,material的Object将会是这样的:

--- !u!21 &2100000

Material:

serializedVersion: 3

... more data …

       在上面的例子中,以&符号开头的数字是material的Local ID。如果此material的Object位于由File GUID“abcdefg”标识的Asset内,则material的Object可以被唯一标识为File GUID“abcdefg”和Local ID“2100000”的组合。


1.3为什么需要File GUID和Local ID?

       为什么Unity的File GUID和Local ID系统是必需的?答案是健壮性,以及提供一个灵活的且独立于平台的工作流程。

        File GUID提供文件特定位置的抽象。只要特定的File GUID可以与特定的文件相关联,那么该文件在磁盘上的位置就变得无关紧要。该文件可以自由移动,而无需更新引用该文件的所有Object。

        由于任何给定的Asset文件可能包含(或通过导入生成)多个UnityEngine.Object资源,因此需要Local ID来明确区分每个不同的对象。

        如果与Asset文件关联的File GUID丢失,那么所有引用该Asset文件Object的引用也将丢失。这就是为什么.meta文件必须与相关的Asset文件存储在相同的文件名和相同的文件夹中是非常重要的。请注意,Unity会重新生成已删除或错位的.meta文件。

        Unity编辑器具有到已知File GUID的特定文件路径的映射。无论加载或导入Asset时都会记录映射条目。映射条目将Asset的特定路径与Asset的File GUID相连。如果Unity编辑器在打开时,一个.meta文件丢失但是Asset路径没有改变,编辑器便可以确定该Asset保持有相同的File GUID。

        如果Unity编辑器是关闭的,这时如果.meta文件丢失或是移动Asset文件路径时没有将.meta文件跟随一起移动,那么所有Object对于该Asset的引用关系都将被打破。


1.4复合Asset和导入器

        正如“内部的Asset和Object”部分提到的,非原生的Asset类型必须被导入进Unity,这是通过一个Asset导入器完成的。虽然这些导入器是自动执行的,但是其也会通过AssetImporter这个API暴露给脚本。比如,TextureImporter这个API可以在导入单独的Texture的Asset时(比如说PNG文件),给文件提供设置。

        导入过程的结果是一个或多个UnityEngine.Objects。这些在Unity编辑器下可见的在一个父Asset下的多个子Asset,比如说多个Sprite嵌套在一个作为一个sprite图集导入的texture的Asset之下。所有这些Object将会分享一个File GUID,因为它们的源数据存储在相同的资产文件中。它们将通过Local ID在导入的texture Asset中进行区分。

        导入过程将源Asset转换为适合在Unity编辑器中选择的目标平台的格式。导入过程可以包括许多重量级操作,例如纹理压缩。由于这通常是一个耗时的过程,因此导入的Asset会缓存在Library文件夹中,无需在下次编辑器启动时再次重新导入Asset。

        具体来说,导入过程的结果存储在一个名为该Asset的File GUID的前两位的文件夹中。该文件夹存储在Library/metadata/文件夹内。Asset中的单个Object被序列化为一个与Asset的File GUID名称相同的二进制文件。

        此流程适用于所有Asset,而不仅仅是非本地Asset。本地Asset不需要冗长的转换过程或重新序列化。


1.5序列化和实例

        虽然File GUID和Local ID是健壮的,但GUID相对速度较慢,运行时需要更高性能的系统。Unity内部持有一个缓存(在内部,这个缓存被称为PersistentManager),将File GUID和Local ID转换为简单的、期间唯一的整数。它们被称为Instance ID,当新Object被注册进缓存的时候,它们被分配为一个简单的单向递增的排序。

        缓存持有给定的Instance ID、File GUID和Local ID之间的映射关系来区分Object的本地元数据,以及Object在内存中的实例(如果有)。这保证了UnityEngine.Object之间互相持有引用。解析Instance ID引用可以快速得到由Instance ID表示的加载Object。如果目标Object尚未加载,则File GUID和Local ID被解析为Object的源数据,从而使Unity能够及时加载对象(JIT)。

        在启动时,工程(也就是构建场景所引用的)会立即将包含所有Object数据的Instance ID迅速实例化,也包含Resources文件夹中的所有Object。当新的Asset在运行时导入的时候(在运行时Asset被创建的例子是一个Texture2D类型的Object在脚本中被创建,比如:var myTexture = new Texture2D(1024, 768); ),或是Object从AssetBundle中加载的时候,这些额外的条目会被添加进缓存。Instance ID条目唯一从缓存中移除的情况是当一个提供对指定的File GUID和Local ID的AssetBundle卸载的时候。发生这种情况时,Instance ID、File GUID和Local ID之间的映射关系将被删除来节省内存。如果重新加载这个AssetBundle时,则将为从重新加载的AssetBundle中加载的每个Object创建一个新的Instance ID。

        有关卸载AssetBundles的含义的更深入讨论,请参阅AssetBundle使用模式文章中的管理已加载Asset部分。

        在特定的平台上,某些事件可能会强制Object从内存中删除内存。比如说IOS平台上的图形Asset当App暂停时会从图形内存中卸载,如果这些Object来自一个被卸载的AssetBundle,Unity将无法重新加载这些Object的源数据。这些Object任何现存的引用也会变得无效。在刚才的例子中,场景可能看起来有不可见的网格或是洋红色的texture。

        实现注意事项:在运行时,上述控制流程不是字面上的精确的。当有比较重的加载任务时,比较File GUID和Local ID将会性能不足。在构建Unity项目时,File GUID和Local ID会被确切的映射成一个简单的格式。然而,这个概念仍然是完全相同的,并且在运行时考虑用File GUID和Local ID保持一个有用的类比。这也是Asset的File GUID在运行时无法查询的原因。


1.6 MonoScripts

        理解一个MonoBehaviour有一个MonoScript引用是重要的,MonoScript仅仅包含了用来定位特定脚本类的信息。任何类型的Object都不包含脚本类的可执行代码。

        MonoScript包含三个字符串:程序集名称,类名称和命名空间。

        当构建一个项目的时候,Unity会将Assets文件夹中的所有零散脚本文件编译为Mono程序集。Plugins子文件夹外的C#脚本被放置到Assembly-CSharp.dll中。Plugins子文件夹中的脚本放置在Assembly-CSharp-firstpass.dll中,诸如此类。此外,Unity 2017.3还引入了定义用户自定义托管程序集的功能。

        这些程序集以及预先构建好的的程序集DLL文件都包含在最终的Unity应用程序中。它们也是MonoScript引用的程序集。与其他资源不同,包含在Unity应用程序中的所有程序集都在应用程序启动时加载。

        MonoScript的Object就是为什么一个AssetBundle(或是一个场景或是一个prefab)并不实际包含可执行代码在任何AssetBundle、场景或是prefab的MonoBehaviour组件的原因。这就允许了不同的MonoBehaviour来引用特定的共享类,甚至这些MonoBehaviour在不同的AssetBundle中。


1.7  资源生命周期

        为了减少加载时间并且管理一个应用程序的内存占用空间,了解UnityEngine.Object的资源生命周期非常重要。Object在特定和定义的时间从内存中加载/卸载。

        在以下情况下会自动加载Object:

        1.映射到该Object的Instance ID被间接引用

        2.该Object目前没有加载到内存中

        3.Object的源数据可以被定位

        Object也可以在脚本中创建它们时或者调用资源加载API时(比如AssetBundle.LoadAsset)被明确的加载。加载Object后,Unity会尝试通过将每个引用的File GUID和Local ID转换为Instance ID来解析任何引用。如果下面两个条件为真,Object将会在其Instance ID被间接引用时第一时间加载:

        1.该Instance ID引用了一个当前未加载的Object

        2.该Instance ID有一个有效的File GUID和Local ID注册在缓存中

        这通常在其自身的引用被加载并解析后很快发生。

        如果一个File GUID和Local ID没有Instance ID,或是带有未加载Object的Instance ID引用了无效的File GUID和Local ID,则将保留该引用,但实际的对象不会被加载。这在Unity编辑器中显示为“(Missing)”。在正在运行的应用程序中或在场景视图中,“(Missing)”的Object将以不同的方式显示,具体取决于它们的类型。例如,mesh看起来是不可见的,而texture可能看起来是品红色的。

        对象在三种特定情况下卸载:

        ·当未使用的Asset清理发生时,Object将会被自动卸载。这个进程将会在场景以破坏性方式切换时(即SceneManager.LoadScene被非附加调用时)或是当脚本执行Resources.UnloadUnusedAssets这个API时自动触发。该进程仅卸载未引用的Object,比如说没有Mono变量引用此Object,并且没有其他激活的Object引用到此变量。此外,请注意,任何标有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的Object都不会被卸载。

        ·可以通过调用Resources.UnloadAsset这个API显式地卸Resources文件夹中的对象。这些对象的Instabce ID保持有效,并且仍将包含有效的File GUID和Local ID条目。如果任何Mono变量或其他Object持有对使用Resources.UnloadAsset卸载的Object的引用,那么只要任何活动引用被间接引用,就会重新加载该Object。

        ·源自AssetBundles的Object在调用AssetBundle.Unload(true)这个API时会自动并立即卸载。这将使对象的Instance ID的File GUID和Local ID无效,并且对卸载的Object的任何实时引用都将变成“(Missing)”引用。C#脚本尝试访问卸载对象上的方法或属性将产生NullReferenceException。

        如果调用了AssetBundle.Unload(false),则来自未加载的AssetBundle的实时Object不会被销毁,但Unity会使其Instance ID的File GUID和Local ID引用无效。如果这些Object被从内存中卸载并且有对这些Object的实时引用保留,Unity将不会重新加载这些Object。(最常见的情况是,在运行时将Object没有卸载就从内存中移除时,Unity会失去对其图形上下文的控制权。当移动应用程序被暂停并且该应用程序被强制置于后台时,可能会发生这种情况。在这种情况下,移动操作系统通常会从GPU内存中清除所有图形资源。当应用程序返回到激活时,Unity必须在场景渲染恢复之前将所有需要的纹理,着色器和网格重新加载到GPU。)


1.8加载大的层级结构

        在序列化Unity GameObjects的层级结构时,例如在预制体序列化时,请务必记住整个层级结构将完全序列化。也就是说,层级结构中的每个GameObject和Component将分别在序列化数据中表示。这对加载和实例化GameObject的层级结构所需的时间有着奇妙的影响。

        在创建GameObject层级结构时,CPU时间花费在几个不同的方面:

        ·读取源数据(来自存储,来自AssetBundle,来自另一个GameObject等

        ·设置新的Transform的父子关系

        ·实例化新的GameObjects和组件

        ·在主线程中唤醒新的GameObjects和组件

        无论层级结构是从现有的层级结构中克隆还是从存储中加载,后三种时间成本通常是不变的。然而读取源数据的时间随着组件和GameObjects序列化到层次结构中的数量的增加而线性增加,并且也乘以数据源的速度。

        在目前所有的平台上,从内存中任意地方读取数据比从存储设备载入数据要快得多。此外,可用的存储介质的性能特征在不同平台之间差异很大。因此,在硬盘读取速度慢的平台上上加载预制体,从存储中读取预制体序列化数据的时间可能会大大超过实例化预制体的时间。也就是说,加载操作的性能成本必然与硬盘存储I/O时间有关。

        如前所述,当序列化一个整体预制体时,每个GameObject和组件的数据都会被单独序列化,这可能会导致数据重复。例如,具有30个相同元素的UI界面将具有相同元素序列化30次,产生大量的二进制数据。在加载时,这30个重复的元素身上的所有的GameObject和组件数据必须在被传送的到新Object实例化之前从硬盘中读取。这个文件读取时间是实例化大型预制体的总成本的重要组成部分。大的层级结构应该使用模块化方式实现实例化,然后在运行时缝合在一起。

Unity 5.4注意: Unity 5.4改变了内存中Transform的表示。每个根Transform的整个子层级结构都存储在紧凑,连续的内存区域中。在实例化新的GameObjects时,它们会立即重新排列到另一个层级结构中,请考虑使用接受父Transform参数的新GameObject.Instantiate()重载。使用此重载可以避免为新的GameObject分配根Transform层级结构。在测试中,这加快了实例化操作所需的时间约5-10%。

你可能感兴趣的:(AssetBundle02)