【Unity优化】资源管理系列01:Assets, Objects and serialization

注意:

1、教程中的 Objects 和 Assets 仅是为了这篇教程而做的命名,与 Unity API 的同名概念没有关联。

2、在 Unity API 中,这篇教程中的 Objects 反而一般被命名为 Assets,比如 AssetBundle.LoadAsset 和 Resources.UnloadUnusedAssets 方法。

3、而这篇教程中的 Assets 一般对应 Unity API 中的 files,很少出现在 Unity 公开的 API 中,一般用于构建相关的代码,比如 AssetDatabase 和 BuildPipeline。

一、资源导入和引用流程

【非运行时】

1、资源文件导入Unity

① Importer 导入处理:转换成平台相关格式,纹理压缩,以及其他必要的处理。转换结果缓存在 Library 文件夹中,下次启动不需要再次导入处理 。导入处理是重度耗时操作。

② 生成 .meta 文件,生成 File GUID 并存入 .meta文件;生成GUID与文件路径之间的map映射。将该资源的GUID-路径映射加入map(资源被导入或运行时load时,都会生成并加入该GUID-路径映射;当资源在Editor中更改路径,GUID对应的路径也会更改)。

③ 生成多个 Objects,在 Editor 中表现为一个父资源和多个子资源,同时生成 Local ID。所有生成的 Objects 序列化到 Library 文件夹下一个与 GUID 同名的二进制文件中。

2、Objects 相互引用,表现在序列化文件中,存储的是被引用 Object 的 GUID 和 Local ID

【运行时】

1、当有新的 Object 生成时,Unity 将它的 GUID 和 Local ID 转换为一个整数,该整数叫 InstanceID。目的是为了提升运行时效率。

2、Unity 维护了一个名为 PersistentManager 的缓存,又叫 InstanceID 缓存。缓存维护了 InstanceID、GUID+LocalID,及内存中可能存在的 Object 实例,这三者之间的联系。当有新的 Object 被加载或创建时(Assetbundle.Load、new Texture2D),将会被注册进缓存,同时生成 InstanceID。(补充:按照03页面的说法,当AB被加载后,其包含的对象已经分配了 InstanceID。难道生成ID,和生成条目,不是同一个操作?)

3、当游戏启动时,将有三部分被自动注册进 InstanceID 缓存,分别是:游戏启动必要的 Objects、第一个场景中的 Objects,以及 Resources 文件夹中的 Objects(注意:Resources 文件夹比较特殊,这里仅注册 InstanceID 和 GUID+LocalID,真正的 Object 并没有被加载进内存)。

二、运行时资源生存周期,及 InstanceID 缓存的使用

1、在满足下面三个条件时,Object 将自动被加载:

① Object 的 InstanceID 被引用

② Object 当前没有被加载进内存

③ 可通过 GUID+LocalID 定位到 Object 的原数据

2、也可通过调用资源加载API来手动加载 Object(eg: AssetBundle.LoadAsset)。当该 Object 被加时,如果它引用了其他资源,Unity将尝试将每个引用的 GUID+LocalID 转换为 InstanceID,之后(实际上这些也都是自动处理):

① 如果InstanceID对应的 Object 存在于内存,就直接返回;

② 如果不存在,但是 InstanceID 缓存中存在有效的 GUID+LocalID,则 Object 自动被加载;

③ 如果不存在,且对应的 GUID+LocalID 无效,则 Object 不会被加载;

另外,如果 GUID+LocalID 无法转换为 InstanceID,则 Object 同样不会被加载。

Object 不被加载,将会导致Editor中引用部分显示Missing,对应的部分可能不显示,如果是纹理,则会显示为洋红色。

3、Obejct 会在下面三种情况下被卸载:

① Scene 被破坏性地改变时,比如非additively调用 SceneManager.LoadScene 时;另外当 Object 不被任何脚本字段引用,且不被其他 Object 引用时,调用 Resources.UnloadUnusedAssets 方法,Object 也会被卸载。注意,如果被标记为 HideFlags.DontUnladUnusedAsset 和 HideFlags.HideAndDontSave,则不会被卸载。

② 来自 Resources 文件夹的 Objects,可通过 Resources.UnloadAsset 方法被卸载。但是要注意,虽然 Object 被卸载了,但是它的 InstanceID 和 GUID+LocalID 都会被保留且有效。如果之后有脚本字段或者其他 Object 再次引用了已被卸载的 Object,则该 Object 将被自动加载。

(这种描述是很奇怪的。前面提到的游戏启动时 Resources 中所有 Objects 都会被注册进 InstanceID 缓存,但是想要使用,仍然需要调用 Resources.Load 方法加载,这也很奇怪。目前没看到官方的详细解释。但是可以推测,Resources 文件夹中的所有 Objects 在游戏启动时就被加载进内存了,Load方法要么是复制一份,要么是语法糖,直接返回已加载 Object;Unload要么卸载的是复制,要么同样是语法糖。还有一种可能,一开时注册 InstanceID 时,并没有加载 Resouces 文件夹内资源,仅是注册 InstanceID 与 GUID+LocalID。目前来看,后一种可能性更大。)

③ 来自 AssetBundle 的 Objects,可通过 AssetBundle.Unload(true) 方法被卸载,并且 InstanceID 和 GUID+LocalID 变为无效。此时如果有其他 Object 引用该 Object,将会显示 Missing;如果是脚本试图引用该 Object,则会报 NullReferenceException 错误。

(注意:如果使用 AssetBundle.Unload(false) 方法,则仅卸载 AssetBundle,而不卸载已加载进内存的 Objects。但是 InstanceID 和 GUID+LocalID 仍然会被无效化。)

三、Assembly 与 MonoScripts

1、当打包项目的时候,所有 Plugins 文件夹以外的 C# 脚本都会被编译进 Assembly-CSharp.dll 程序集;所有 Plugins 文件夹内的脚本都会被编译进 Assembly-CSharp-firstpass.dll 程序集(正如其名,该程序集会被提前编译);所有 Editor 文件夹内的脚本将被编译进 Assembly-CSharp-Editor.dll 程序集。在 Editor 中选中脚本,Inspector 窗口会显示该脚本最终会被编译进的程序集。

2、每个 Monobehaviour 都会引用一个 MonoScript(后面简称MS)。MS仅包含三个字符串:assembly名、class名,以及 namespace;用于反过来定位 Monobehaviour 脚本。因此 AssetBundle、Scene、Prefab 等不需要包含脚本代码,只包含 MS 即可,之后在需要时再根据 MS 提供的信息找到对应的脚本。

四、多层级对象的序列化与加载

1、多层级 Unity GameObject(prefab、Scene)在序列化的时候,每个 GameObject(后面简称 GO)和组件,都会被单独的序列化为一块数据,通过 LocalID 引用来表述组合、层级等关系。而将这些数据加载并实例化为多层级 GO,是项比较耗时的工作。

2、加载并实例化 GO 时,CPU耗时主要在下面四个步骤:

① 读取源数据:从硬盘存储中、AssetBundle中,甚至是其他 GO 中;

② 设置 Transforms 的层级关系;

③ 实例化新的 GO 和组件;

④ 在主线程唤醒上一步实例化的 GO 和组件。

其中后三步,无论是从已有层级克隆,还是从存储中加载,耗时基本固定不变。而第一步的耗时,随着 GO 和组件的增多呈线性增加;并且如果是从存储中读取,则还要考虑到源数据的读取时间,读取时间因此可能会超过实例化时间。

3、正如第1点提到的,GO 和组件,都会被单独的序列化为一块数据。比如一个UI界面中有30个相同的元素,那么序列化的时候就会产生30份数据,导致二进制序列化文件增大,相应的从硬盘读取的时间也会增加。应避免这种情况,建议采用模块化设计,在实例化的时候再克隆组装在一起。并且实例化的时候,使用 GameObject.Instantiate 带父物体参数的重载方法,避免层次的再次分配,这会节省5%~10%的实例化时间。

原文地址:https://www.cnblogs.com/hearthstone/p/13357841.html