2.Assets, Objects and serialization
这一章涵盖了Unity序列化系统的深层次内容,以及Unity如何在编辑模式和运行模式维护不同对象之间的健壮引用。它还讨论了对象和Asset之间的技术区别。这里的内容对于理解如何在Unity中有效地加载和卸载资产是非常重要的。正确的资源管理对于减少加载时间和降低内存至关重要。
2.1. Inside Assets and Objects
为了理解如何正确地管理Unity中的数据,很重要的一步是理解Unity是如何识别和序列化数据的。第一个关键点是Asset和UnityEngine.Objects之间的区别。
Asset :它是硬盘上存在的文件,存储在unity 工程下的 Assets 文件夹下. Textures, 3D 模型, 或者 audio clips 都是 Assets类型. 一些 Assets 包含Unity原生格式的数据, 比如 materials. 还有其他一些 Assets 需要处理为原生格式,比如FBX文件
UnityEngine.Object或者 Object:它是一组序列化数据,表示的是特定资源的一个实例,这个资源可以是unity中的任意类型,比如贴图,音频文件等,它继承自 UnityEngine.Object
在众多内置的对象类型中,有两种特殊类型:
-
一: ScriptableObject 为开发人员定义自己的数据类型提供了一个方便的系统. 这种类型可以被unity 序列和反序列化,并且可以在属性窗口编辑,就相当于可编辑的配置表
-
二: MonoBehaviour 提供了一个指向 一个MonoScript 的封装器,MonoScript是Unity 的一个内置类型, 用来对特定脚本中的类,命名空间,程序及保持引用 ,它不包含任何实际可操作的代码.
Asset和Object是一对多的关系,也就是任何asset都至少包含一个object,因为object只是asset的一个实例,是给unity用的,Asset是在硬盘上的
内存时运行内存,我们说的卸载这上面的东西,Asset是存储内存上的东西,我们说的删除东西,就是删除该上面的东西,也就是内存卸载了,只要硬盘上还在,就还可以加载,如果源文件不在了,就加载不回来了
2.2. Inter-Object references
所有 UnityEngine.Objects 都可以引用其它的 UnityEngine.Objects. 这些其它的 Objects 可能属于同一个asset文件,或者从其它 Asset文件中导入. 比如,一个材质球引用其他的贴图,这些贴图一般是通过一个或多个 texture Asset 文件导入进来的(such as PNGs or JPGs).
当序列化的时候,这些引用分成两部分独立的数据: 一个 GUID 文件和一个 Local ID文件. GUID 表示该资源在哪个位置存储着.(独一无二的)Local ID 表示该资源引用了那些资源,因为一个Asset 文件可能包含多个 Objects,同一个asset文件中的local id 是唯一的.
GUIDs 保存在 .meta 文件中. 这些.meta文件是在Unity第一次导入Asset时生成,并且存储在与Asset相同的目录中。
上面说的GUID,也就是.meta文件可以通过记事本打开: 首先在 Editor Settings 中设置资源序列化类型
把一个材质球拖到记事本中,该材质球引用一张贴图,如下如所示
&后面的就是该材质球在其它资源中的Local ID
如果该材质球的 GUID "abcdefg",Local ID "2100000".它就可以在一个Asset中被唯一标识
2.3. Why File GUIDs and Local IDs?
为什么非得用GUIDS和Local IDS呢?
File GUID提供了该资源在硬盘上存储位置的抽象,也就是GUID指向了该资源的位置,只要GUID和位置是相关联的,那么它实际在硬盘上的位置就可以随意挪动,因为都可以通过Guid找到
Local ID用来区分Asset当中引用的多个UnityEngine.Object对象
如果与Asset文件关联的GUID丢失了,那么该Asset中引用的所有对象的也将丢失。这就是.meta文件必须以相同的文件名存储在与其关联的资产文件相同的文件夹中的原因。注意Unity会重新生成被删除或放错位置的.meta文件。
Unity 编辑器拥有一个资源路径到GUIDs的映射,每当加载资源或者导入资源时,就会记录一个map映射. 这个映射连接Guid和资源路径.如果当unity编辑器的时候, .meta 文件丢失了,这个 Asset的路径不会改变,编辑器会确保该asset拥有和之前相同的 File GUID.也就是重新生成了一个.meta文件
2.4. Composite Assets and importers
导入资源是通过一个 asset importer导入的,这些 importers 会在导入资源时自动调用,他们可以在脚本中通过 AssetImporter 访问到,比如, TextureImporter提供了导入 texture Assets, such as PNG files.的设置
如果一个资源下包含多个子对象,比如一个图集下包含多张图片,则这些图片会共享同一个GUID,因为它们在一个相同的asset中,所以资源路径是一致的,那么他们将会根据各自的local ID来区分
导入过程将Asset转换为适合于在Unity编辑器中选择的目标平台的格式。导入过程可以包括一些重量级的操作,比如纹理压缩. 由于这通常是一个耗时的过程,所以导入的Asset会缓存在 Library 文件夹中,从而减少下次打开编辑器的时候,导入资源的时间
,资源对象序列化的二进制文件以该Asset的GUID命名,存在以GUID的前两位命名的文件夹中. 这些文件夹放在 Library/metadata/ folder中. The individual Objects from the Asset are serialized into a single binary file that has a name identical to the Asset's File GUID.
这个importer过程适用于所有的Assets,并不仅仅是原始资源,只不过原始资源不需要长时间的转换过程或重新序列化
2.5. Serialization and instances
虽然GUID和Local ID连接是健壮的,但是根据GUID解析路径比较慢, Unity内部维护了一个把GUID和Local ID转换成更简单的整数缓存下来,这个缓存机制一般叫 PersistentManager. 缓存的这个整数叫 Instance IDs, 当有新的Objects被注册到缓存当中的时候,它会自动递增.
这个缓存维持了GUID,Local ID,与Instance ID之间的映射,定义了对象资源的位置,也就是通过instance id 就能获取到源资源的位置和资源实例在内存中的位置,这样 UnityEngine.Objects 就可以相互引用,解析一个 Instance ID 引用,可以快速加载该Instance ID所指向的对象. 如果目标对象还没有被加载, GUID and Local ID 就会被解析为该对象的源资源,以便Unity及时加载对象。
在开始运行的时候,这个Instance ID 初始化项目中所有用到的对象,比如,场景中引用的对象 (i.e., referenced in built Scenes), 以及 Resources 文件夹下的对象.在游戏运行时,如果有新的assets导入进来,或者从AssetBundle中加载新的对象的时候,就会在该缓存中添加新的cache对象 (比如在运行时通过脚本创建一个 Texture2D 对象: var myTexture = new Texture2D(1024, 768);)。 Instance ID 只有当 AssetBundle 中 File GUID and Local ID指向的资源被卸载的时候,才会被从cache中移除,这时, Instance ID, its File GUID and Local ID之间的映射也会从内存中清除,如果重新加载 AssetBundle, 将会创建一个 新的Instance ID .
在特定的平台上,某些事件可以迫使内存不足. 例如, 在iOS系统上,当应用被挂起的时候,graphical Assets会从 graphics memory被卸载,如果这些在AssetBundle 中的对象被卸载了, Unity 就不能重新加载这些对象的源资源. 任何对这些对象的引用都是无效的. 在前面的例子中,场景可能看起来有不可见的网格或洋红色纹理。
Comparing File GUIDs and Local IDs at runtime would not be sufficiently performant during heavy loading operations. When building a Unity project, the File GUIDs and Local IDs are deterministically mapped into a simpler format. However, the concept remains identical, and thinking in terms of File GUIDs and Local IDs remains a useful analogy during runtime. This is also the reason why Asset File GUIDs cannot be queried at runtime.
2.6. MonoScripts
它表示的是一个脚本Asset,可以以scriptObject 或者Monobehavior
一个MonoBehaviour 引用了一个 MonoScript, MonoScripts 只包含脚本中的类所需的信息. 这两种类型的对象都不包含script类中的可执行代码。
一个MonoScript 包含三个字符串: assembly name, class name, and namespace.
在发布一个项目时,Unity会将Assets文件夹中的所有松散脚本编译成Mono程序集.除了 Plugins 文件夹下的脚本,被编译进 Assembly-CSharp.dll中. Plugins 文件夹下的脚本编辑到Assembly-CSharp-firstpass.dll,In addition, Unity 2017.3 also introduces the ability to define custom managed assemblies.
这些程序集,以及预构建的程序集DLL文件, 包含在Unity最终发布的应用程序中.它们也是MonoScript引用的程序集.与其他资源不同,包含在Unity应用程序中的所有程序集都是在应用程序启动时加载的.这也是为什么c#不能热更
This MonoScript Object is the reason why an AssetBundle (or a Scene or a prefab) does not actually contain executable code in any of the MonoBehaviour Components in the AssetBundle, Scene or prefab. This allows different MonoBehaviours to refer to specific shared classes, even if the MonoBehaviours are in different AssetBundles.
2.7. Resource lifecycle
为了减少资源的加载时间和管理应用程序的内存占用,理解UnityEngine.Objects资源的生命周期是很重要的. 对象在特定的时间从内存中卸载/加载。
对象被自动加载 ,当出现下面三种情况时:
-
映射到该对象的 Instance ID 被解除引用了,当再使用该对象时,会重新加载
-
对象当前没有加载到内存中,使用该对象的时候,会重新加载
-
能够在内存中找到该对象的源资源
也可以在脚本中通过创建对象或调用资源加载的API加载对象 (例如, AssetBundle.LoadAsset). 当一个对象被加载的时候, Unity 会解析每一个引用该对象的 File GUID and Local ID ,从而转换成Instance ID. 当出现下面两种情况时,该对象的Instance ID 会被解除引用,这时这个对象会被按需加载(用到的时候才加载):
-
The Instance ID 引用当前未加载的对象
-
The Instance ID在缓存中保有一个有效的 GUID and Local ID,以便能够找到源资源
这通常发生在引用本身被加载和解析之后不久。
如果一个 GUID and Local ID 没有映射到一个 Instance ID, 或者 Instance ID 映射到了一个已经被卸载的对象的 GUID and Local ID, 引用虽然还在,但是资源已经被卸载了.unity编辑器中就会出现 "(Missing)" reference . 如果是在运行的场景中,“(缺少)”对象将以不同的方式可见,这取决于它们的类型。例如,网格看起来是不可见的,而纹理可能是洋红色的。
:对象在三种特定场景下被卸载:
-
当清理未被使用的Asset时候,对象会自动卸载. 当改变场景时,这个过程会自动进行, (比如当 SceneManager.LoadScene 的时候), 或者使用 Resources.UnloadUnusedAssets API. 这个过程只卸载未引用的对象; 只有当所有的Mono变量都没有引用该对象时,且没有显示的对象保有对该对象的引用时,才会被卸载,如果一个对象被标记称 HideFlags.DontUnloadUnusedAsset and HideFlags.HideAndDontSave ,则它不会被卸载
-
Resources 文件夹下的资源可以通过 Resources.UnloadAsset 卸载, 这些对象的 Instance ID仍然有效,且映射的 GUID and LocalID 也依然有效. 如果任何Mono变量或其他对象持有对已卸载对象的引用,当目前任何引用被解除引用时,该对象将被重新加载。
-
AssetBundle中对象会立即卸载 当调用 AssetBundle.Unload(true) 时. 这将使GUID,Local ID与Instance Id 之间的映射无效,任何对卸载对象的引用都会变成 "(Missing)" references.因为通过instance id ,找不到对应的资源了, C# 脚本中, 试图访问已卸载对象上的方法或属性将产生NullReferenceException异常,这个时候是可以重新被加载的,也就是重新从assetbundle中加载到内存当中,重新生成一个instance ID,重新产生一个引用,因为之前的引用已经销毁了。
当调用AssetBundle.Unload(false) 时,当前对AssetBundle中已卸载对象的引用不会被销毁, 但是Unity 依然会使GUID,Local ID与Instance Id 之间的映射无效. 如果这些对象后来被从内存中卸载,但是对卸载对象的引用仍然存在,那么Unity将不能重新加载这些对象,因为还存在引用。
(Note: 最常见的情况是,对象在运行时从内存中移除了,但是没有被卸载(映射还在),这种情况出现在unity书去了对其图形文本的控制. 当一个移动应用程序被挂起并被迫进入后台时,可能会发生这种情况. 在这种情况下,移动操作系统通常会从GPU内存中移除所有图形资源. 当应用程序返回前台时, 在恢复场景渲染之前,Unity必须重新加载所有需要的纹理、着色器和网格到GPU,造成大量耗时.)
2.8. Loading large hierarchies
当序列化Unity GameObjects的层次结构时,比如在序列化prefabs时,需要记住该prefab的层次结构 ,层级中的每一个 GameObject 和Component 都会被单独序列化成一个文件. 这会对加载和生成prefab的时间造成影响
当创建GameObject时,CPU主要花时间在下面几种方式上:
-
读取源资源数据 (从内存中,从 AssetBundle,或者从其他GameObject上.)
-
为新创建的GmaeObject创建新的父子级关系
-
实例化 new GameObjects and Components
-
唤醒 new GameObjects and Components on the main thread
后三种时间开销通常是不变的,不管层次结构是从现有层次结构克隆出来的还是从存储中加载出来的.然而,读取源数据的时间会随着序列化中的层次结构中的GameObjects and Components的数量线性增加。还要乘以数据源的速度
在所有当前平台上,从内存中读取数据要比从其他存储设备加载数据快得多. 然后不同的平台,它的存储能力可能不同,因此当低内存中加载对象时,花费在读取prefab的时间比实例化对象的时间还要长,所以结论就是加载资源的时间和内存读取时间有关。
前面提到的,序列化prefab时,每一个 GameObject and component的数据都会被单独序列化,可能会有重复数据. 例如,具有30个相同元素的UI屏幕将会将相同元素序列化30次,产生大量二进制数据.在加载的时候,这30个重复元素中的每一个元素中的组件和对象数据在赋给新实例化对象之前,都会从磁盘上读取,这会造成大量的时间耗费,所以大型层级的游戏物体,应该一块一块的实例化,然后在运行时组装到一起.
Unity 5.4 note: Unity 5.4 改变了transform(游戏物体)在内存中的表示,每个根转换的整个子层次结构存储在紧凑的、连续的内存区域中. 当实例化新的游戏对象,将立即返回到另一个层次,在使用 GameObject.Instantiate 传递一个parent参数,会避免为新的GameObject分配一个层级结构,因为这个层级结构已经分配好了 ,这会减短实例化的时间, this speeds up the time required for an instantiate operation by about 5-10%.
3.The Resources folder
这一节讨论 Resources system.它允许在一个或者多个名字为 Resources 的文件夹中存储资源,并在运行中通过 Resources API.加载或者卸载资源对象
3.1. Best Practices for the Resources System
最好不要用它
提出这一强烈建议有以下几个原因:
-
使用Resources文件夹会增加细粒度内存管理的难度,
-
资源文件夹的不当使用将增加应用程序的启动时间和构建的长度
-
随着资源文件夹数量的增加,管理这些文件夹中的资产变得非常困难
-
资源系统降低了项目向特定平台交付定制内容的能力,并且不能更新资源
-
AssetBundle Variants是unity最基本的资源定制工具
3.2. Proper uses of the Resources system
有两个特定的用例,资源系统可以在不妨碍良好开发实践的情况下提供帮助:
-
资源文件夹的易用性使其成为快速原型的优秀系统。但是,当项目进入完全生产状态时,应该取消对Resources文件夹的使用。
-
资源文件夹可能在一些琐碎的情况下有用,如果资源是:
-
通常在项目的整个生命周期中都需要
-
不是内存密集型,不会消耗太多内存
-
不需要打补丁,或者不因平台或设备而不同
-
Used for minimal bootstrapping,用于最小引导
Examples of this second case include MonoBehaviour singletons used to host prefabs, or ScriptableObjects containing third-party configuration data, such as a Facebook App ID.
3.3. Serialization of Resources
在构建项目时,所有名为“Resources”的文件夹中的Asset和Object被合并到单个序列化文件中。 该文件还包含元数据和索引信息, 就像 AssetBundle. 该索引包括序列化查找树,用于把给定对象名称解析成 GUID and Local ID.它也可以用来定位对象的位置
在大多数平台上,查找数据结构是一个平衡二叉树, 构造时间以O(n log(n))的速度增长. 索引的加载时间随着资源文件夹中对象数量的增加而超过线性地增长
这个操作是不可跳过的,他会在应用成语开始的时候初始化. 据观察,初始化包含10,000个资产的资源系统会在低端移动设备上花费数秒,尽管资源文件夹中包含的大多数对象在第一个场景中很少需要加载。
4.AssetBundle fundamentals
这一章讨论 AssetBundles. 讨论 AssetBundles 是如何创建的,和主要的API, 同时介绍了如何加载/卸载 AssetBundles ,以及从AssetBundle中加载特定的资源
4.1. Overview
AssetBundle 系统提供了一个Unity 可以索引和序列化的存储有一个或多个文件的结构方法. AssetBundles是Unity在应用程序安装后更新非代码内容的主要工具.这允许开发者发布一个更小的应用程序包, 最小化运行时内存的压力,并根据玩家最终的设备类型加载不同的内容
更多信息 AssetBundle documentation.
4.2. AssetBundle layout
总而言之, AssetBundle 由两部分组成: a header and data segment.
The header包含了 AssetBundle的信息,比如identifier, compression type, and a manifest. The manifest一个以对象名称为键的查找表. 每一条都提供了一个字节索引,用来查找在AssetBundle data segment中的对象的位置. 在大多数平台上,这个查询表被实现为一个平衡的二叉树.但是 Windows and OSX-derived platforms (including iOS) 是一个红黑树. 因此搜索的时间会根据AssetBundle中Asset的数量的增加而非线性增加.
data segment 包含了AssetBundle中Asset序列化的原始数据, 如果使用 LZMA 压缩格式,整个Asset序列化文件都会被压缩,也就是压缩成一整个文件,如果是 LZ4 则每一个Asset都会被单独压缩成一个序列化文件,如果原始数据没有被压缩,则 data segment 将保持为原始字节流 byte streaming
在Unity 5.3之前,对象不能被单独压缩在一个 AssetBundle 中. 因此,如果Unity 5.3之前的版本从压缩的AssetBundle中读取一个或多个对象, Unity必须对整个AssetBundle包进行解压缩 ,通常,Unity缓存了一个资源包的解压缩副本,以提高后续在同一资源包上的加载请求的加载性能。
4.3. Loading AssetBundles
AssetBundles 可以通过四个不同的api加载.这四个api取决于两个标准:
-
AssetBundle是否是 LZMA 压缩, LZ4 压缩or 未压缩
-
AssetBundle 加载的平台
These APIs are:
-
AssetBundle.LoadFromMemory(Async optional)
-
AssetBundle.LoadFromFile(Async optional)
-
UnityWebRequest's DownloadHandlerAssetBundle
-
WWW.LoadFromCacheOrDownload (on Unity 5.6 or older)
4.3.1 AssetBundle.LoadFromMemory(Async)
Unity's不推荐使用这个 API.
AssetBundle.LoadFromMemoryAsync 从一个托管的字节数组中加载 AssetBundle from a managed-code byte array (byte[] in C#).这会从托管字节数组中复制一份数据,重新分配 , 如果 AssetBundle是LZMA 压缩格式的, 会在复制的时候,对整个AssetBundle解压缩. 如果是未压缩或者是 LZ4-格式压缩的,会被原始的复制下来
这个API消耗的最大内存量将至少是资源包大小的两倍: API在本机内存中创建的一个副本,托管字节数组中保存一个副本,传递给API. 通过这个 API从AssetBundle中加载asset 又会复制一分,所以在内存中复制了三分,: once in the managed-code byte array, once in the native-memory copy of the AssetBundle and a third time in GPU or system memory for the asset itself.
4.3.2. AssetBundle.LoadFromFile(Async)
AssetBundle.LoadFromFile 是一个高效的API,用来从本地内存中比如硬盘或者SD卡加载未压缩的或者是 LZ4-压缩的AssetBundle
在独立桌面、控制台和移动平台上, 这个API仅仅加载 AssetBundle的 header,并将剩余数据留在磁盘上. AssetBundle's 对象或被按需加载,当调用加载方法时,比如 AssetBundle.Load 或者 当InstanceIDs 被消除引用时. 不会消耗多余的内存. 在Unity编辑器中,API会将整个资源包加载到内存中, 就像从磁盘读取字节一样 and并且使用 AssetBundle.LoadFromMemoryAsync 方法. 如果在编辑器下加载AssetBundle,会造成内存的峰值,. 这应该不会影响设备上的性能,在采取补救措施之前,应该在设备上重新测试这些峰值
Note: 在使用Unity 5.3或更老版本的Android设备上, 如果用这个API从Streaming Assets path中加载 AssetBundles 会失败,Unity 5.4解决了
4.3.3. AssetBundleDownloadHandler
The UnityWebRequest API允许开发者准确地指定Unity应该如何处理下载的数据,并允许开发者消除不必要的内存使用. 使用UnityWebRequest下载一个资产包最简单的方法是调用 UnityWebRequestAssetBundle.GetAssetBundle.
然后使用 DownloadHandlerAssetBundle 处理下载下来的AssetBundle. 使用工作线程,不是主线程,他把下载下来的流文件存储进一个固定缓存里面,然后把缓存数据发送给临时存储器或者AssetBundle缓存,取决于Download Handler的配置, 如果缓存里面已经有了,且不需要更新,就会从缓存里面下载,而不是从硬盘上再加载一遍. 所有这些操作都在本机代码中进行,从而消除了扩展托管堆的风险.除此之外, Download Handler 在本机代码中没有保留下载下来的资源的副本,进一步降低了内存的消耗
LZMA压缩的AssetBundle会在加载的时候,解压缩,然后采用 LZ4 压缩,是否采用LZ4压缩,取决于Caching.CompressionEnabled的值,默认为true
当下载结束后,通过Download Handler的 assetBundle 属性,访问下载下来的AssetBundle, 就和调用AssetBundle.LoadFromFile 下载AssetBundle一样.
如果缓存当中已经存在,要下载的AssetBundle,那么这个操作就和 AssetBundle.LoadFromFile.一样
在Unity 5.6之前,UnityWebRequest系统使用一个固定的工作线程池和一个防止过量同时下载的内部作业系统. 线程池的大小不可配置.在Unity 5.6中,这些保护措施已经被移除,以适应更现代的硬件, and allow for faster access to HTTP response codes and headers.
4.3.3.1UnityWebRequestAssetBundle.GetAssetBundle
https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequestAssetBundle.GetAssetBundle.html
4.3.4. WWW.LoadFromCacheOrDownload
Note:从Unity 2017.1开始, WWW.LoadFromCacheOrDownload 封装在了 UnityWebRequest.
WWW.LoadFromCacheOrDownload 是允许从远程服务器和本地存储加载对象的API. 文件可以从本地存储中通过 file:// URL下载,如果缓存当中已经存在,要下载的AssetBundle,那么这个操作就和 AssetBundle.LoadFromFile.一样
如果一个资产包还没有被缓存,那么WWW.LoadFromCacheOrDownload将从源文件中读取AssetBundle.如果资产包被压缩,那么它将使用一个工作线程解压并写入缓存中. 如果没被压缩,它将通过工作线程直接写入缓存. 一旦资产包被缓存,WWW.LoadFromCacheOrDownload将从缓存中解压的资源包中加载头信息. 这个缓存在 WWW.LoadFromCacheOrDownload 和UnityWebRequest是共享的,这两个API都可以访问到
当数据将被解压并通过一个固定大小的缓冲区写入缓存时,WWW对象将在本机内存中保留一个AssetBundle字节的完整副本。这个额外的资产包副本通过WWW.bytes 访问到
由于在WWW对象中缓存一个资产包字节的内存开销,资产包应该尽量保持较小——几兆字节
与UnityWebRequest不同,对这个API的每次调用都会产生一个新的工作线程.因此,在内存有限的平台上,比如移动设备,一次只能使用这个API下载一个资产包, 从而避免内存峰值. 在多次调用此API时,要小心创建过多的线程. 如果需要下载超过5个资产包,创建并管理一个载队列,以确保只有少数资产包下载同时运行。
4.3.5. Recommendations
一般而言,应该尽可能使用 AssetBundle.LoadFromFile. 这个API在速度、磁盘使用和运行时内存使用方面是最有效的。
用于必须下载或修补AssetBundles的项目 , 强烈推荐使用 UnityWebRequest . 可以使用包含在项目安装程序中的包来启动资产包缓存。
调用UnityWebRequest 或者WWW.LoadFromCacheOrDownload 方法, 要确保 downloader 在加载资产包之后调用 Dispose , 或者使用C#的 using 声明,自动调用
对于有大量工程团队的项目,需要独特的、特定的缓存或下载要求,可以考虑自定义custom loader.编写一个自定义下载程序是一项重要的工程任务,并且任何自定义下载程序都应该与AssetBundle.LoadFromFile兼容。有关更多细节,请参阅下一步的分发部分。
4.4. Loading Assets From AssetBundles
UnityEngine.Objects 可以通过三个API从 AssetBundles 中加载, 它们同时都具有同步和异步的方法:
这些api的同步版本总是比异步版本快至少一帧.
异步加载将在每帧加载多个对象,直到它们的时间限制.
LoadAllAssets 用来加载多个独立的UnityEngine.Objects.只有当一个 AssetBundle 中有大量的资源需要被加载的时候才用,与其它两个方法相比,LoadAllAssets 比单独调用 LoadAssets 要快一点点. 因此,如果要加载的资产数量很大,但是只有不到66%的资产包需要一次加载, 考虑将资产包拆分为多个更小的包 ,然后使用 LoadAllAssets.
LoadAssetWithSubAssets当加载包含多个嵌入对象的复合资产时使用, 例如一个带有嵌入动画的FBX模型或一个内嵌多个精灵的精灵图集. 如果需要加载的对象都来自相同的资产,但与许多其他不相关的对象存储在一个AssetBundle中,使用它
4.4.1. Low-level loading details
加载UnityEngine.Object在主线程之外执行: an Object's 数据在工作线程中从内存中读取, 任何不触及Unity系统的线程敏感部分(脚本、图形)都将在工作线程上转换. 例如,VBOs将从网格创建,纹理将被解压, etc.
从Unity 5.3开始,对象加载就可以并行处理了. 多个对象在工作线程上被反序列化、处理和集成. 当一个对象完成加载时,会调用Awake方法,并且在下一帧中该对象将对Unity的其余部分可用,也就是加载完的下一帧,才可以被引用
AssetBundle的同步加载方法将暂停主线程,直到对象加载完成,它们还将对对象加载进行时间切片,以便对象集成占用的帧时间不会超过一定的毫秒数. 毫秒数的上限是通过 Application.backgroundLoadingPriority属性设置的:
-
ThreadPriority.High: Maximum 50 milliseconds per frame
-
ThreadPriority.Normal: Maximum 10 milliseconds per frame
-
ThreadPriority.BelowNormal: Maximum 4 milliseconds per frame
-
ThreadPriority.Low: Maximum 2 milliseconds per frame.
从Unity 5.2开始,多个对象被加载,直到达到对象加载的帧时间限制. 假设所有其他因素都一样,异步方法总是比同步方法至少慢一帧,因为在调用异步方法与对象加载完成之间至少隔一帧,而同步方法是在同一帧完成的
4.4.2. AssetBundle dependencies
AssetBundles 之间的依赖关系,根据运行时环境,通过两个不同的 APIs获取到,在编辑器下, AssetBundle dependencies 可以通过 AssetDatabase 获取到, AssetBundle 的分配和依赖关系可以通过 AssetImporter 访问或者更改. 在运行时,可以通过基于 scriptableObject的AssetBundleManifest 访问
As described in the Serialization and instances section of that step, AssetBundles 充当源文件,它包含了多个asset,这些asset通过 FileGUID & LocalID 识别
因为对象在它的实例ID第一次被解除引用时被加载,并且因为在AssetBundle被加载时被分配了一个有效的实例ID, AssetBundles 加载的顺序并不重要,但是.需要在加载父物体Asset时,先加载子Asset,这个的意思是从assetBundle里加载asset的顺序,而不是加载AssetBundle的顺序,加载AssetBundle的顺序可以任意
4.4.3. AssetBundle manifests
首先manifest 也是一个assetBundle,它会自动构建,包含manifest 的 AssetBundle 也可以被loaded, cached and unloaded
当使用BuildPipeline.BuildAssetBundles API构建assetBundle时, Unity 序列化成一个对象,它包含每一个AssetBundle的依赖关系的数据,这个数据保存在一个独立的AssetBundle中,它包含一个单独的 AssetBundleManifest 类型的对象.
它和assetBundle的位置一样,名字也一样,只不过加了manifest后缀,如果 AssetBundles 生成在/build/Client/文件夹,则manifest为/build/Client/Client.manifest.
AssetBundleManifest 对象通过GetAllAssetBundles API 列举所有的构建的 AssetBundles ,通过下面两个方法获取特定AssetBundle的以来关系:
-
AssetBundleManifest.GetAllDependencies 获取一个 AssetBundle的所有层级的依赖,包括它的子层级,以及它子层级的层级等;
-
AssetBundleManifest.GetDirectDependencies 只返回AssetBundle的直接子层级;
这两个都返回依赖AssetBundle的路径,不要在性能吃紧的地方调用它
4.4.4. Recommendations
在许多情况下,最好在玩家进入应用程序性能关键区域之前加载尽可能多的所需对象, 比如游戏关卡或世界. 这在移动平台上尤为重要,在这种情况下,因为这对本地存储的访问很慢,并且在运行时加载和卸载对象的内存会触发垃圾收集器。
5.AssetBundle usage patterns
. 本章讨论了在实践中使用AssetBundle的各个方面的问题和可能的解决方案。
5.1 Managing loaded Assets
在内存敏感的环境中,谨慎地控制加载的对象的大小和数量是非常重要的.Unity不会自动卸载从当前场景中移除的对象. Asset 清理会在特定的时间触发,但是也可以手动触发.
AssetBundles 它们自己必须小心管理. 一个AssetBundle在内存上很小,一般几KB,但是多了就影响性能了
由于大多数项目允许玩家重新体验内容(比如重进一个关卡), 知道何时加载或卸载一个资产包是很重要的. .
所以理解 AssetBundle.Unload 是很重要的,对于它的参数
这个 API 会写在AssetBundle的头文件信息( header information). 传递的unloadAllLoadedObjects 参数决定了是否卸载已经从该AssetBundle中实例化的对象,如果true,则所有的从该AssetBundle中实例化的对象都会被销毁,即使当前场景还在活跃状态
比如在当前场景卸载了一个材质球M。
如果调用AssetBundle.Unload(true) , M 会从当前场景移除,销毁,卸载,如果调用 AssetBundle.Unload(false), AB的 header information 会被卸载,但是 M 还在,AssetBundle.Unload(false) 切断了M 和AB之间的联系,如果重新加载AB包,则AB中包含的对象的新拷贝将被加载到内存中。
如果AB稍后被再次加载,那么资产包头信息的一个新的副本将被重新加载.但是,M不是从这个新的AB拷贝中加载的. Unity不能在新的AB和M之间建立任何联系。
如果调用了AssetBundle.LoadAsset()来重新加载M,Unity不会把之前的M作为AB数据的一个实例. 因此,Unity会加载一个新的M副本,在场景中会有两个相同的M副本。
为true时,从运行内存中消除assetbundle 及其所有的instance
为false时,消除assetbundle,保留instance,但是之前加载的instance,已经不属于assetbundle了
这里的instance 就是从assetbundle 中加载的资源
对于大多数项目来说,这种行为是不可取的。大多数项目应该使用AssetBundle.Unload(true)并采用一种方法来确保对象不被复制. 两种常见的方法是:
-
在应用程序的生命周期中,在特定的时间点,卸载Assetbundle,比如在关卡加载期间.这是最简单和最常见的选择.
-
对从AssetBundle中的对象,进行引用计数,当应用为0时,卸载无用的AssetBundle,这让应用程序卸载和重新加载对象更灵活,而不用复制内存
如果程序必须要用 AssetBundle.Unload(false),那么单个对象只能通过两种方式卸载:
-
消除对不需要的对象的所有引用, 无论是在场景中还是在代码中. 完成之后, 调用 Resources.UnloadUnusedAssets.
-
非附加加载一个场景. 这将破坏当前场景中的所有对象,自动调用Resources.UnloadUnusedAssets .
如果一个项目有明确定义的点,用来等待对象加载和卸载,例如在游戏模式或关卡之间,这些点应该用来卸载尽可能多的对象和加载新对象.
要做到这一点,最简单的方法是将项目的离散块打包到场景中,然后将这些场景连同它们的所有依赖项一起构建到资产包中,然后,应用程序可以进入一个“加载”场景,完全卸载包含旧场景的资产包,然后加载包含新场景的资产包。
虽然这是最简单的流程,但有些项目需要更复杂的资产包管理。由于每个项目的不同,没有通用的资产包设计模式。
5.2. Distribution
有两种基本的方法可以将项目的AssetBundle包分发给客户端:与项目同时安装或安装后下载
决定是在安装时还是在安装后更新资产包是由运行项目的平台的能力决定的. 移动端通常选择安装后下载来减少初始安装大小,保持低于无线下载大小限制. 控制台和PC项目通常在初始安装时附带资产包。
适当的架构允许在安装后将新的或修改过的内容打补丁到项目中,而不管最初的资产包是如何交付的. For more information on this, see the Patching with AssetBundles section of the Unity Manual.
5.2. 0 Patching with AssetBundles
5.2.1. Shipped with project
把AssetBundle包和工程打包在一起是最简单的,下面有两种原因,解释了为什么能够在应用程序安装时,把AssetBundle包构建到一起:
-
减少项目构建时间并允许更简单的迭代开发. 如果这些资产包不需要从应用程序本身单独更新,可以把 AssetBundles放在 Streaming 文件夹下.
-
发布可更新内容的初始版本.这样做通常是为了在初始安装后为最终用户节省时间,或者作为以后打补丁的基础.这种情况下,放在streaming文件夹并不好, However, if writing a custom downloading and caching system is not an option, then an initial revision 然后,可更新内容的初始版本可以从 Streaming Assets加载到unity缓存中
5.2.1.1. Streaming Assets
这是包含任何类型内容的最简单方法,包括 AssetBundles, /Assets/StreamingAssets/ 文件夹下的内容,会在发布应用程序时,复制到应用程序中
StreamingAssets 文件夹的路径通过Application.streamingAssetsPath 属性访问. 然后可以通过 AssetBundle.LoadFromFile加载AssetBundle
Android 开发者: 在安卓上,在 StreamingAssets 文件夹下的资源是存储在APK 中,并且,如果它们被压缩了,将会花费更多时间加载,因为文件存储在一个APK中,可以使用不同的存储算法. 使用的算法可能因Unity版本的不同而有所不同. 您可以使用7-zip之类的归档器打开APK,以确定文件是否被压缩. 如果压缩了,AssetBundle.LoadFromFile() 会执行得更慢。在这种情况下, 可以使用 UnityWebRequest.GetAssetBundle 方法,它会在第一次运行时,解压缩 AssetBundle ,然后缓存下来,以便后续会更快,但是会需要等量的存储空间, 因为资产包将被复制到缓存中.或者,您可以导出您的Gradle项目,并在构建时向您的资产包添加一个后缀,. 然后编辑 build.gradle文件,把该后缀添加到不压缩的部分,然后你就可以使用AssetBundle.LoadFromFile() 加载,而不用解压缩
Note: Streaming Assets在一些平台是不可写入的,如果一个项目的资产包需要更新后安装, 则要么使用 WWW.LoadFromCacheOrDownload 或者写一个 custom downloader.
5.2.2. Downloaded post-install
在移动设备上,最经常用的就是安装之后下载,在许多平台上,应用程序二进制文件必须经历昂贵而漫长的重新认证过程,因此,为安装后的下载开发一个好的系统是至关重要的。
传递资产包最简单的方式是将它们放在web服务器上,然后通过UnityWebRequest传递.Unity会自动将下载的资源包缓存到本地存储中.如果下载的资源包是LZMA压缩的,资源包将以未压缩或重新压缩的LZ4的形式存储在缓存中(取决于Caching.compressionEnabled 设置,默认为true), 如果下载的包是LZ4压缩的,那么资产包将以LZ4被压缩存储. 如果缓存满了,Unity将会移除最近使用次数最少的那个AssetBundle.
一般建议在可能的情况下使用UnityWebRequest, 如果内置api的内存消耗、缓存行为或性能对于特定项目来说是不可接受的,或者项目必须运行特定于平台的代码来满足需求,那么就只能自定义Loader。
下面的情况不会使用 UnityWebRequest :
-
当需要对资产包缓存进行细粒度控制时
-
当项目需要实现自定义压缩策略时
-
当项目希望使用特定于平台的api来满足某些需求时,比如在非活动状态下需要流数据。
-
例如:在后台使用iOS的后台任务API下载数据。
-
当资产包必须在Unity没有适当SSL支持的平台(如PC)上通过SSL交付时。
5.2.3. Built-in caching
Unity 有一个内置的AssetBundle缓存系统,通过 UnityWebRequest 可以从缓存中下载AssetBundle,这个方法可以传递要下载AssetBundle的版本参数,这个数字存储在AssetB
缓存系统,它会记录上次传递给UnityWebRequest的版本号.当传递版本号调用此API时,缓存系统通过比较版本号来检查是否存在缓存的资产包. 如果这些数字匹配,系统将加载缓存的资产包.如果数字不匹配,或者没有缓存的资源包,那么Unity将下载一个新的拷贝。这个新的副本将与新的版本号相关联。
AssetBundles 在缓存系统中,只能通过它们的文件名来作为唯一标识,而不是通过下载它们时的完整URL. 这意味着具有相同文件名的资产包可以存储在多个不同的位置,比如内容分发网络. 只要文件名相同,缓存系统就会将它们识别为相同的资产包。
每个单独的应用程序都应该有一个为资产包分配版本号,并将这些版本号传递给UnityWebRequest的策略, 数字可能来自各种唯一标识符,比如CRC值. 注意,虽然AssetBundleManifest.GetAssetBundleHash()也可以用于此目的,但我们不推荐使用该函数进行版本控制,因为它仅提供估算,而不是真正的散列计算)。
See the Patching with AssetBundles section of the Unity Manual for more details.
从Unity 2017.1起, CachingAPI已被扩展,以提供更细粒度的控制,允许开发人员从多个缓存中选择一个活动缓存. 以前版本的Unity只能进行修改 Caching.expirationDelay and Caching.maximumAvailableDiskSpace 删除缓存项,来增加缓存空间 (这些属性保留在Unity 2017.1的Cache类中).
expirationDelay 就是延迟多长时间删除AssetBundle,如果在此期间没有访问一个资产包,它将被自动删除。
maximumAvailableDiskSpace 指定本地存储上的空间量,以字节为单位, 在缓存开始删除最近使用最少次数的资源包之前,缓存可能会使用 expirationDelay. 当达到缓存空间是,会删除最近使用次数最少的AssetBundle,(或者使用Caching.MarkAsUsed 标记的). Unity会删除缓存的资源包,直到有足够的空间来完成新的下载。
5.2.3.1. Cache Priming
因为资产包是通过文件名来标识的, 可以用应用程序附带的资源包来“prime”缓存. 为此,请将每个资产包的初始版本或基本版本存储在 /Assets/StreamingAssets/文件夹中.
当应用程序第一次运行时,可以从 Application.streamingAssetsPath 加载AssetBundle到缓存中,从那时起,就可以调用 UnityWebRequest 来加载AssetBundle了.
5.3. Asset Assignment Strategies
决定如何将一个项目的资源划分为AssetBundle并不简单.采用一种简单的策略是很诱人的,比如将所有对象放在它们自己的资产包中,或者只使用一个资产包,但是这些解决方案有明显的缺点:
-
Having too few AssetBundles...
-
增加运行时的内存
-
增加加载时间
-
需要更大的下载
-
Having too many AssetBundles...
-
构建时间增加
-
Can complicate development
-
Increases total download time
把 AssetBundles 分组的关键策略是:
-
Logical entities
-
Object Types
-
Concurrent content
More information about these grouping strategies can be found in the Manual.
5.4. Common pitfalls
本节描述在使用资产包的项目中经常出现的几个问题.
5.4.1. Asset duplication
当创建AssetBundle的时候,Unity会找出它所有的依赖关系,这个依赖关系就决定了该AssetBundle中包含哪些对象
那些显示声明为一个AssetBundle的对象,只会被打包进那一个AssetBundle,因为它的路径确定了,如果一个显示声明了,则它的 assetBundleName 属性就是不为空的
游戏物体也可以通过定义 AssetBundle building map,来让它们成为AssetBundle的一部分,这个映射关系可以传递给 BuildPipeline.BuildAssetBundles() 方法,也就是可以用代码设置打包进AssetBundle的对象.
所有未显示标记路径的对象,都会被打包进引用它的AssetBundle中
比如,这个不同的对象,构建进两个不同的AssetBundle中,但是它们共同引用了同一个物体,那个物体会复制进这两个AssetBundle中,复制依赖项也将被实例化,这意味着依赖项对象的两个副本将被视为具有不同标识符的不同对象.这会增加整个应用程序中AssetBundle的大小,如果同时加载这两个AssetBundle,它也会在内存中存在两份
这里有几种解决这个问题的方法:
-
确定打包进AssetBundle中的对象,没有共享依赖项,依赖项可以放进同一个AssetBundle中.
-
这种方法通常不适用于有许多共享依赖项的项目。因为它会产生很多单独的依赖项,加载起来比较繁琐
-
AssetBundles 分段加载,这样就不会有两个具有相同依赖项的AssetBundle同时加载, so that no two AssetBundles that share a dependency will be loaded at the same time.
-
这种方法可能适用于某些类型的项目,比如基于关卡的游戏. 然而,它仍然不必要地增加了项目资产包的大小,增加了构建时间和加载时间。
对象依赖关系通过位于UnityEditor命名空间中的AssetDatabase API来跟踪。正如名称空间所暗示的,这个API只能在Unity编辑器中使用,而不能在运行时使用. AssetDatabase.GetDependencies .可以用来定位特定对象或资产的所有直接依赖项 注意,这些依赖项可能有它们自己的依赖项此外,AssetImporter API可以用来查询分配特定AssetBundle中的对象,通过AssetDatabase and AssetImporter APIs, 通过这两个API,编写一个编辑器代码,确保所有的AssetBundle 的依赖项都显示声明AssetBundleName,或者没有AssetBundle共享依赖项
5.4.2. Sprite atlas duplication
任何自动生成的图集都会被声明为AssetBundle,它包含了该图集的所有Sprite对象,如果其中的Sprite被打包进其它的AssetBundle中,那么整个图集也会跟着复制进去
为了确定图集不会被复制,需要让所有图集中的所有sprite被打包进同一个AssetBundle中
:注意在Unity 5.2.2p3和更早的版本中,自动生成的精灵图集永远不会打包进AssetBundle中.因此,它们将被包含在任何包含其组成子元素的资产包中,以及任何引用其组成子元素的资产包中.,由于此原因,推荐升级unity
5.4.3. Android textures
在Android生态系统中,由于设备类型比较多,设备的性能好坏不一,经常需要将纹理压缩成几种不同的格式.虽然所有的Android设备都支持ETC1,但ETC1不支持带有alpha通道的纹理。如果一个应用程序不支持OpenGL ES 2,那么就使用ETC2,所有的Android OpenGL ES 3设备都支持它。
大多数应用程序需要装载在不支持ETC2旧设备上。解决这个问题的一种方法是使用Unity 5的AssetBundle变体(参考Unity的Android优化指南了解其他选项的详细信息)。
使用 AssetBundle Variants,所有不能使用ETC1压缩的texture,必须单独打包成一个AssetBundles.然后创建一个变体,用来支持不支持ETC2的设备,这些纹理使用特定于供应商的纹理压缩格式,如DXT5、PVRTC和ATITC. For each AssetBundle Variant, change the included textures' TextureImporter settings to the compression format appropriate to the Variant.
At runtime, support for the different texture compression formats can be detected using the SystemInfo.SupportsTextureFormat API. This information should be used to select and load the AssetBundle Variant containing textures compressed in a supported format.
More information on Android texture compression formats can be found here.
5.4.4. iOS file handle overuse
当前版本的Unity不受这个问题的影响
In versions prior to Unity 5.3.2p2, Unity would hold an open file handle to an AssetBundle the entire time that the AssetBundle is loaded. This is not a problem on most platforms. However, iOS limits the number of file handles a process may simultaneously have open to 255. If loading an AssetBundle causes this limit to be exceeded, the loading call will fail with a "Too Many Open File Handles" error.
This was a common problem for projects trying to divide their content across many hundreds or thousands of AssetBundles.
For projects unable to upgrade to a patched version of Unity, temporary solutions are:
-
Reducing the number of AssetBundles in use by merging related AssetBundles
-
Using AssetBundle.Unload(false) to close an AssetBundle's file handle, and managing the loaded Objects' lifecycles manually
5.5. AssetBundle Variants
Variants 的目的就是生成适合于当前运行环境的AssetBundle,Variants允许不同AssetBundle中的不同的 UnityEngine.Objects被加载时解析成同一个Instance ID 引用. 也就是说这两个 UnityEngine.Objects共享同一个 File GUID & Local ID.加载的时候也只会加载其中的一个
这个系统有两个主要用例:
-
Variants 简化了不同平台AssetBundle的加载,在不同的平台加载不同的AssetBundle.
-
比如:创建了一个包含 high-resolution textures and complex shaders适合于一个 standalone DirectX11 Windows 平台, 然后第二个 AssetBundle为Android设计的低保真内容. 在运行时,项目的资源加载代码可以为其平台加载适当的AssetBundle变体, AssetBundle.Load API不需要改变
-
变体允许应用程序在同一平台上根据硬件的不同加载不同的内容.
-
这是支持各种移动设备的关键. 在任何现实世界的应用程序中,iPhone 4都无法显示与最新款iPhone相同的内容保真度
-
在Android上,各种各样的资产包可以用来解决设备之间屏幕长宽比和DPIs的巨大差异.
5.6.1. Crunch Compression
主要使用 DXT-compressed 压缩的textures ,使用的是Crunch compression algorithm ,应该无压缩打包
5.5.1. Limitations
使用变体的限制是 必须创建不同的AssetBundle包.如果 Variant A and Variant B 仅仅是在图片的压缩格式上不同, Variant A and Variant B 仍然必须打包成单个独立的AssetBundle,也就是说 Variant A and Variant B 在硬盘上是独立存在的.
这种限制使大型项目的管理复杂化,因为必须在源代码控制中保存特定资产的多个副本. 当开发人员希望更改资产的内容时,必须更新资产的所有副本. 对于这个问题没有内置的解决方案.
大多数团队实现他们自己形式的资产包变体. 这是通过在文件名中添加定义良好的后缀来构建资产包来实现的,以便识别给定资产包所表示的特定变体. 然后在代码中设置导入设置.
5.6. Compressed or uncompressed?
是否压缩资产包需要考虑几个重要因素,其中包括:
-
加载时间:当从本地存储或本地缓存加载时,未压缩的资源包比压缩资源包加载要快得多。
-
构建时间:LZMA和LZ4在压缩文件时非常慢,并且Unity编辑器串行处理资产包. 拥有大量资产包的项目将花费大量时间压缩它们.
-
应用程序大小:如果资产包是在应用程序中提供的,压缩它们将减少应用程序的总大小。或者,资产包可以在安装后下载。
-
内存使用:在Unity 5.3之前,所有Unity的解压机制都要求在解压之前将整个压缩包加载到内存中.如果内存使用是重要的,使用未压缩或LZ4压缩资产包.
-
下载时间:只有当资源包很大,或者用户在带宽受限的环境下,例如在低速或计量连接上下载时,压缩才有必要.如果只有几十兆字节的数据正在被交付到pc高速连接,它可能会省略压缩。