Asset简介 agile Posted on Oct 2 2021 优秀博文 > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/411946807) 前言 -- 在开发 Unity 项目的时候,必然会经常和 Asset 打交道,例如我们工程的 Assets 文件夹下,基本都是各式各样的 Asset,接下来会从以下几个方面来聊聊它: 1. Asset 是如何产生的 2. Asset 的导入设置 3. Asset 和 Assetbundle 的一些关系 4. Asset 的加载与管理 5. Asset 的卸载 文章参考自: [[Unity 活动]- 官方直播详解 Unity Asset 的一生_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Wv41167i2)[https://docs.unity3d.com/Manual/AssetWorkflow.html](https://docs.unity3d.com/Manual/AssetWorkflow.html) Asset 是如何产生的 ------------ Asset 在我们平时使用的时候主要是由下面两种方法产生的: 1. 用第三方的工具来产生 Asset,这种情况最常见的有:FBX,Texture,Sound 等。它们经常是由第三方工具(例如 maya,3dmax,photoshop)等生成,然后放到 Unity 中来用。 2. Unity 自己产生的 Asset,比如说 Prefab,Scene,Animator 文件等。 注:其中 Script 从 Unity 引擎的角度来看也是一种 Asset,但是 Unity 在管理它以及进一步处理时,和其他的 Asset 有一定的区别。 不管哪种方式产生的 Asset,一般都是由下面两部分组成的: 1. 本身的数据内容,是文件的主旨,原始数据的所在。 2. meta 文件,主要是记录了一些额外的信息。 比如我们导入一张. png 图片,导进来的 png 文件本身是一个数据内容,同时 Unity 会为你产生一个同名的 meta 文件。以及我们生成一个 Prefab 同样也会生成一个同名的 meta 文件。 ![](https://pic4.zhimg.com/v2-7954b0a193c878c54f333dd2678d01bf_r.jpg) 对于第三方的 Asset,例如. png、.wav、.fbx 等文件,它们的文件内容和它们各自所对应的文件格式有关,不做过多介绍。我们来看看 Prefab 文件的内容,Sphere.prefab 其实就是 Unity 自带的球体,其在 Unity 中的 Inspector 界面如下图: ![](https://pic1.zhimg.com/v2-8077d114a02a87695a4377e9d34ea504_r.jpg) 接着我们使用文本编辑器来打开 Sphere.prefab 来一探究竟: ``` %YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!1 &2704994518885799472 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - component: {fileID: 3702433357692089522} - component: {fileID: 3757503778730075621} - component: {fileID: 7766305049339339301} - component: {fileID: 7900522011432960373} m_Layer: 0 m_Name: Sphere ...... --- !u!4 &3702433357692089522 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2704994518885799472} ...... --- !u!33 &3757503778730075621 MeshFilter: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2704994518885799472} m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} ...... ``` Unity 自己生成的 Asset 打开来看基本都是这个格式的文本,该文本格式我们称之为 [YAML](https://docs.unity3d.com/Manual/FormatDescription.html)。其中第一行第二行记录的类似于一个版本信息。 接着我们看后面的内容,可以发现这些信息被 --- 给切割成一块一块的,每一块代表的都是一个 Object 所对应的信息(在 Unity 中所有的类都是继承于 Object),并且每一块的开头的格式都是由 !u! 后面跟两串用 & 符合相连的数字,例如: ``` ---!u!1&2704994518885799472 ``` 其中第一串数字,我们称之为 [ClassID](https://docs.unity3d.com/Manual/ClassIDReference.html),是 Unity 内部为所有的 Object 做的一个类型枚举,例如 GameObject 的 ClassID=1,Transform 的 ClassID=4,而我们自定义的 MonoBehaviour 组件的 ClassID=114 等。ClassID 与下面一行记录的 Object 类型一一对应,例如 !u!1 的下一行一定是 GameObject, !u!4 的下一行一定是 Transform。 接着是第二串数字,它被称之为 **fileID**,**它是该 Asset 文件内不同 Object 所对应的一个唯一 ID。**举个例子,我们一个 Prefab 下可以挂很多相同的组件(比如好几个 BoxCollider),那么这些组件也就是 Object 它们的 ClassID 都是一样的,而 fileID 则会各不相同。并且跟其他类型的 Object 的 fileID 也都不相同,在当前文件内起到唯一性的作用。 fileID 除了通过文本编辑器来打开 Asset 查看外,其实也可以在 Unity 中查看,在 Inspector 面板右上角选择 Debug 模式即可: ![](https://pic2.zhimg.com/v2-02e241ef0f945faa236318296954a85d_r.jpg)![](https://pic3.zhimg.com/v2-3dffe22a6c850357d08a3495ba516f76_r.jpg) 其中 Local Identfier In File 对应的就是 fileID 的值。利用该方法,我们还可以查看第三方产生的 Asset 的 fileID,例如. png,.wav 这些文件的 fileID,如下图: ![v2-27e89d994324a11bc6e334cb00a68d0d_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-27e89d994324a11bc6e334cb00a68d0d_1440w.jpg) 从中可以发现,所有相同类型的 Asset 其 fileID 都是相同的,例如第三方生成的图片、声音或是 Unity 自己生成的 Material、Animation 等,它们都是一个 Asset 对应一个 Object。而 Prefab 或 Scene 文件内,由多个 Object 构成,因此各个 Object 的 fileID 都不相同,由 Unity 随机分配。由于只在单个 Asset 内保证是唯一的,因此 fileID 也常被称为 localID。 从前面的图中可以发现还有个 **InstanceID**,该 ID 并没有记录在我们的 Asset 文件中,我们可以通过 Object.GetInstanceID 的 API 来获取,每个 Object 的 InstanceID 都是唯一的**。**不过需要注意的是一个 Asset 的 InstanceID 在不同的机器上或者重启 Unity 以及切换场景(包括运行时)都会发生变化,因此不能作为一个持久化的数据。 接下来的内容是一系列类似键值对的数据,记录着当前 Object 的属性,不同的 Object 有着不同的属性,大部分的键值对我们都可以和 Unity 中的 Inspector 界面一一对应起来,如下图: ![](https://pic4.zhimg.com/v2-856fe174e5c247b2bbce8c36151b8577_r.jpg) 因此如果你想批量修改一些 Asset 的某些属性(比如把所有 prefab 的 scale 都设置为 1),可以不用在 Unity 里操作,而是自己写脚本,遍历所有相关的 Asset 文件,按照 YAML 的格式读出来,然后去做修改。 简单介绍几个常用的属性: 1.GameObject 的 m_Component: ``` m_Component: - component: {fileID: 3702433357692089522} - component: {fileID: 3757503778730075621} ...... ``` 我们知道一个 GameObject 上可以挂载各式各样的组件(component),该属性对应的就是该 GameObject 上挂载的所有组件。并且通过每个组件的 fileID 来做索引,例如 3702433357692089522 对应的就是 Transform。 因此**如果 Asset 内某个键值对的值是 fileID: xxx 那么它指向的就是该 Asset 内 fileID=xxx 的某个 Object**。 2.fileID 与 guid 的组合: 除了上面单独 fileID 的情况,我们在 Asset 里面还能经常看见 fileID 和 guid 的组合。例如我在一个 material 里面设置了_mainTex: ![v2-296ac829844e1d67243a7143d35bb881_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-296ac829844e1d67243a7143d35bb881_1440w.jpg) 那么查看 material 文件时会发现下面一行: ``` - _MainTex: m_Texture: {fileID: 2800000, guid: 6ad45645da157cf4989f9c4390aad785, type: 3} ``` fileID:2800000 前面已经介绍了,所有的 Texture2D 的 fileID 都是 2800000。此外这里又来了个新的 id:guid,它是 Asset 文件的唯一 ID(介绍 meta 文件的时候再详细介绍)。如果我们打开这张图片对应的 meta 文件就可以看到这个 id,如下: ![v2-7d1298477bbaa1739f993deca0bea576_1440w.png](https://tools.nxcloud.club:12500/images/2021/10/02/v2-7d1298477bbaa1739f993deca0bea576_1440w.png) 因此**如果 Asset 里面有 fileID 和 guid 的组合,那么它就是指向另一个 Asset 的 Object**。如果我们把 Material 上的图片换一换,其实改变的就是这个 guid,如下: ![](https://pic2.zhimg.com/v2-dfa3001838ad0e8507d3e437f9419c3d_r.jpg) 若我们打包的时候不想要某个资源,只需要打包前遍历所有 Asset 文件,找到引用它的地方,设置为 {fileID: 0} 即可。 3. 绑定事件: 有时候我们可能会在 Animation 文件里添加 Event 来调用一些函数,如下图: ![v2-010887cc49c3907c45b0032115d4b97d_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-010887cc49c3907c45b0032115d4b97d_1440w.jpg) 此时这些 Event 数据也会被记录在 Animation 文件中: ``` m_Events: - time: 0 functionName: AnimationEvent1 data: objectReferenceParameter: {fileID: 0} floatParameter: 1.1 intParameter: 0 messageOptions: 0 ``` 因此如果有时候我们想找某些函数是否被 Animation 所调用,可以通过 IDE 全局收 Animation 文件,而不是在 Unity 里一个个的找。此外,例如 UI Button 上添加的 OnClick 事件也是同理,会记录 m_OnClick 的数据。 此外,从另一个角度来看,Unity 的 Asset 又可分为下面两种: 1. 运行时(Runtime)Asset,它比较好理解,例如生成的纹理,声音,动画,最终打成包的时候是要跟着你的游戏一起发布出去的。玩家去玩的时候会看见这些东西,我们管这些 Asset 叫运行时的 Asset。 2. 编辑期(Editor)Asset,指的是 Editor 里面,当我们做一些 Editor 设置的时候,它会生成一些 Asset,这些 Asset 最终不会打到你的包里,但它会参与你整个的编辑以及生成包的过程,最常见的就是 ProjectSettings 文件夹里的 Asset。还有一些是在生成运行期的 Asset 的时候,它会有一部分的信息内容,是用于 Build 或者是编辑的时候才会用到的数据内容。这些数据最终不会打到你运行时的包体里面,但他对于你的编辑和如何生成最终的包体是有指导意义的,这种也说是编辑器的 Asset。 Asset 导入设置 ---------- 当我们在 Unity 导入一些第三方的 Asset 的时候,在 Inspector 界面会发现有很多的设置选项,如下图是图片的相关设置: ![v2-a79d5053732e66e11c0c53548832aa02_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-a79d5053732e66e11c0c53548832aa02_1440w.jpg) 这些设置我们称之为 Asset 的导入设置,接下来来了解了解它。 ### 1.meta 文件 前面我们说了每个 Asset 都会生成一个 meta 文件,它到底是什么?为什么每次 Unity 都会生成它,并且当你更改 Asset 的导入设置的时候它也会跟着发生变化。还有如果我们将资源上传到 VCS(Version Control System)的时候,没有上传 meta,那么下次还可能出错,例如打出来的 AssetBundle 不一样了,或者打出来的设置不一样了等。 同样的,我们用文本编辑器打开. meta 文件来一探究竟,下面是图片对应的. meta 文件内容的一部分: ``` fileFormatVersion: 2 guid: 6ad45645da157cf4989f9c4390aad785 TextureImporter: internalIDToNameTable: [] externalObjects: {} serializedVersion: 11 mipmaps: mipMapMode: 0 enableMipMap: 1 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 borderMipMap: 0 mipMapsPreserveCoverage: 0 alphaTestReferenceValue: 0.5 mipMapFadeDistanceStart: 1 mipMapFadeDistanceEnd: 3 bumpmap: convertToNormalMap: 0 externalNormalMap: 0 heightScale: 0.25 normalMapFilter: 0 isReadable: 0 ...... ``` meta 文件同样是以 YAML 的格式生成的,在第一行记录的是 fileFormatVersion 的值,它其实是告诉我们当前的 meta 文件是哪种格式类型,现在是第 2 版格式类型。Unity 在很久很久都没有改变该值了,而且预计的将来也不会变,因此基本可以不用管它。 第二行记录的是 **guid**,它就非常重要(在前面也简单提到了一下),当我们去生成或导入一个 Asset 的时候,Unity 要唯一的去标识这个 Asset,这个唯一值就是 guid。这也解释了为什么 VCS 上没有传 meta,再使用的时候可能会出错,就是因为这个 guid 变了。 举个例子,假如你做了个新功能,写了个新的组件,然后上传 VCS 的时候没有上传对应组件的 meta 文件。这个时候当你的同事拉取了你的代码后,就会发现找不到对应的组件了,出现了 **Script Missing**,如下图: ![v2-fe9e3c194e574969d938abdaa187ce21_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-fe9e3c194e574969d938abdaa187ce21_1440w.jpg) 可是明明该组件的 cs 文件都在,为什么会 Missing 呢?这就是因为 Unity 是通过我们前面所说的记录在 Asset 内的 fileid+guid 来关联对象的。当你上传 Asset 的时候,里面记录的是你本机对应 cs 文件的 guid,也就是 meta 文件里的。而由于没有上传 meta 文件,别人拉取下该 cs 文件的时候,Unity 会给该组件分配一个新的 guid,这就和 Asset 里面记录的不一样,因此也就导致 Asset 无法定位到正确的 cs 文件了,即 Script Missing。还有例如其他一些关联的资源找不到的问题(例如绑定的图片丢了等等),都可能是这个原因所造成,解决方法同样很简单,只需要把被关联资源的 meta 文件里的 guid 设置成 Asset 里面记录的 guid 即可。 此外,guid 还关联到了 Library 里面的东西,在稍后做介绍。 从第三行开始,meta 文件记录了 Importer 信息,也就是该 Asset 的导入设置,这些内容和在 Inspector 里面看见很多内容基本都是一一对应的。例如我们声音文件的设置: ![](https://pic3.zhimg.com/v2-f5f6a885317e838f5cae87f6c947c63a_r.jpg) 不管是我们修改 Unity 里面的设置,还是修改. meta 文件,另一边都会同步跟着变换。在很多时候,我们对于一些资源都会有一些特定的设置需求,例如图片在不同平台往往也会需要不同的 format,UI 图片关闭 mipmap,声音文件开启 ForceToMono 等等。如果每次导入新资源都要手动来设置的话,实在是太愚蠢了,因此 Unity 为我们提供了 [AssetPostprocessor](https://docs.unity.cn/2021.1/Documentation/ScriptReference/AssetPostprocessor.html) 类,利用它里面的 [AssetImporter](https://docs.unity.cn/2021.1/Documentation/ScriptReference/AssetImporter.html) 对象我们可以在每次 Asset 被导入的时候更改其导入设置。示例代码如下: ``` using UnityEditor; public class ImageImporter : AssetPostprocessor { public void OnPreprocessTexture() { TextureImporter textureImporter = (TextureImporter)assetImporter; if (textureImporter == null) return; UnityEngine.Debug.Log("change image import setting"); TextureImporterPlatformSettings settings = new TextureImporterPlatformSettings(); settings.name = BuildTarget.Android.ToString(); settings.overridden = true; settings.maxTextureSize = 1024; settings.format = TextureImporterFormat.ASTC_6x6; textureImporter.SetPlatformTextureSettings(settings); textureImporter.SaveAndReimport(); } } ``` 除了例子中用到的回调,还有诸如 Asset 导入成功后的回调:[OnPostprocessTexture](https://docs.unity.cn/2021.1/Documentation/ScriptReference/AssetPostprocessor.OnPostprocessTexture.html) 等,以及当所有 Asset 都导入完成后的回调:[OnPostprocessAllAssets](https://docs.unity.cn/ScriptReference/AssetPostprocessor.OnPostprocessAllAssets.html)。 此外,我们同样可以写一个脚本来遍历所有的 meta 文件,修改里面的导入设置。 ### 2.Library 文件夹 当我们往 Unity 导入第三方资源的时候,是否会思考过,到底导入什么样格式的文件才是最好的?例如声音文件,是 MP3 好呢还是 WAV 或者是其他。其实都可以,因为 Unity 不管你放入工程的 Asset 是什么,它最终会根据导入设置按你需要的格式把它导出。当我们在不停的修改 Unity 的导入设置时,原文件却从来没有被碰过,它依然保持这你放进去的样子。 因为实际上生成的 Asset 不管是 Unity 自己产生的还是第三方产生的,最终都会被导入到 Unity 引擎里面的 **Library** 文件夹下。**真正在 Unity 引擎里面或者是你运行时使用的,实际上是你 Library 文件夹里面的东西**。因此当我们修改导入设置时,影响的 Asset 是导入到 Library 文件夹里的那个文件,而并不是你的原文件。 按这个角度来说,放 WAV 是最好的,因为它的原始采样率是最好的,声音是无损的,那么经过压缩之后放到 Library 里只经过一次压缩。如果放的是 MP3,那么系统首先会去解压这个 MP3,然后再重新把它压缩成你最终需要的格式,那么这样的一个过程就会导致一个二次压缩的损害,使得声音或多或少的会有音质上的损伤。所以这里建议大家还是放原文件进去,甚至把 Photoshop 的. psd 放进去,Unity 会将你的. psd 文件转换和导入成纹理,非常的方便。 那么我们的 Asset 到底在 Library 里的什么地方呢?这里要分两个版本来介绍了。Unity 利用了 [Asset Database](https://docs.unity.cn/2021.1/Documentation/Manual/AssetDatabase.html) 来维护由我们原文件转换后在 Library 里的文件。在 **Unity2019.3 之前**,它并不是真的 Database,而是一套查询系统,称之为 **AssetPipeline Version 1**。在 Version 1 中,Library 下会有一个名为 **metadata** 的文件夹: ![](https://pic1.zhimg.com/v2-2bdd077be542ff47aaa368ef8846204c_r.jpg) 该文件夹里面存放的就是所有我们运行时真正用到的 Asset,子文件夹通过 Asset 的 guid 前两位进行分类。例如我们导入一张图片,通过. meta 文件发现它的 guid 如下: ![v2-7cffa99f2bfb3128520721242ebad73a_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-7cffa99f2bfb3128520721242ebad73a_1440w.jpg) 那么我们就可以在 5e 的文件夹下找到与 guid 相同的文件: ![](https://pic3.zhimg.com/v2-887f3dc39f04d7cc6364f4f8e8faa93e_r.jpg) 这是一个二进制文件,可以通过 Unity 在安装目录下 \ Editor\Data\Tools 提供的 binary2text.exe 工具将其转换成可读的文件,转换后的文件内容截图如下: ![](https://pic3.zhimg.com/v2-f89a2f5b76cf63be08be0c8afb9b33ce_r.jpg) 可以发现这里面存储了我们原文件图片的数据信息,除此之外还包含了 meta 信息,导入设置等等。 如果有时候我们发现我们的原文件更新了,但是在 Unity 里看见的效果或者打出来的 AB 包的内容都还是旧的,那么大概率就是因为 Library 里对应的文件没有重新生成,我们可以检查下里面的文件时间,对需要更新的文件 Reimport 一下,强制 Library 下对应的文件重新生成。 ![v2-4568ec6962b963dfbef9a27d29dbe52d_1440w.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-4568ec6962b963dfbef9a27d29dbe52d_1440w.jpg) 注:非必要的情况下,可别不小心点到 Reimport All 了,否则就要恭喜你,可以划水小半天了。 利用这个机制,在做 AssetBundle 的时候,可以算 Library 里面东西的系统时间来判断它需不需要被重新打一下,这样就可以实现只针对当前修改过的资源打 AssetBundle。 但是在 2019.3 版本之后,Unity 使用了一套新的 Asset Database 系统,叫 **Version 2**。可以在 Project Setting-Editor 里面进行切换: ![](https://pic3.zhimg.com/v2-e3c99070c592a237b99631669e85cac6_r.jpg) 注:在 Unity2020 之后的版本就彻底弃用了 Version 1,也就没有这个切换功能了。 Unity 在 Version 2 里使用了名为 [LMDB](https://github.com/LMDB/lmdb)(Lightning Memory-Mapped Database)的内存数据库,真正的把大家导入的东西放入到 database 里,因此使用 Version 2 的速度会比 Version 1 更加的快。 使用 Version 2 会发现,原本的 metadata 文件夹没了,多了一个叫 **Artifacts** 的文件夹,虽然里面的目录结构看着和原本的 metadata 一样,但是却无法根据 guid 找到对应文件了。不过当我们在 Unity 新导入一个 Asset 的时候,例如一张图片,还是可以发现在 Artifacts 下会同时新增两个文件,其中一个我们可以通过 binary2text.exe 进行转换,里面的内容如下图,包含了导入设置,以及最终的数据。 ![](https://pic4.zhimg.com/v2-1080fd168a984755401033b3d4c86f8f_r.jpg) 此外在 Library 下多了两个 DB 文件:**SourceAssetDB** 和 **ArtifactDB**。 ![](https://pic3.zhimg.com/v2-9d9ab36ddb93e511cb5479de2e2aff0e_r.jpg) Unity 通过这两个 DB 文件来追踪我们的 Asset,其中 SourceAssetDB 包含了我们原文件的最后修改时间,文件内容的 hash,guid 以及 meta 信息等,Unity 用它来判断原文件是否发生了变化,是否需要重新导入。而 ArtifactDB 中是原文件由 Unity 导入处理后得到的最终文件的信息,包含了导入依赖的信息,Artifact 文件的 meta 信息以及 Artifact 文件信息。 个人猜测:在 Version 2 中,由 Artifacts 文件夹存储 Unity 最终运行时用的 Asset,然后通过 ArtifactDB 来管理原文件和 Artifacts 文件的映射关系。然后利用 LMDB 的 **Memory Map** 文件映射的方式来读取 Artifacts 文件,该方式相比 Version 1 更加快速安全。 自己尝试了下用 LMDB 的源码去看 SourceAssetDB 以及 ArtifactDB 的存储内容,想看看原文件与 Artifacts 文件的映射关系,结果失败了。有 Unity 源码的大佬们要是知道,可以指点指点。顺便看了下 Unity 的 AssetImportWorker.log 文件,感觉也是对不上号 =。= ``` ======================================================================== Received Import Request. Time since last request: 12.844051 seconds. path: Assets/xun.png artifactKey: Guid(6ad45645da157cf4989f9c4390aad785) Importer(815301076,1909f56bfc062723c751e8b465ee728b) Start importing Assets/xun.png using Guid(6ad45645da157cf4989f9c4390aad785) Importer(815301076,1909f56bfc062723c751e8b465ee728b) -> (artifact id: '54ac80b7fa6200d6b374a546d79b8ab3') in 0.161613 seconds Import took 0.167522 seconds . ``` 不过在通过 UnityHub 咨询的时候,结果遇到了高川老师,也算是追星成功了,哈哈。让我们期待老师更多的技术分享! 然后不同的平台(Windows,Android...)由于导入的过程不一样,所以可能会产生差异。因此当我们在 Unity 引擎里切换一个平台,会重新 Loading(Importing)一次,这个过程是因为在不同的平台上,Asset 最终导入进来的结果是不一样的。 除此以外,Asset Database 还与 [Cache Server](https://docs.unity.cn/2021.1/Documentation/Manual/CacheServer.html) 以及 [Accelerator](https://docs.unity.cn/2021.1/Documentation/Manual/UnityAccelerator.html) 也有着密切的关系。 ### 3.StreamingAssets 文件夹 在我们打包的时候,往往会把事先生成好的 AB 包放到 StreamingAssets 目录下,该目录下的所有文件都会**原封不动**的打进包里,那是为什么呢?而且在 Android 系统上它们是可以被直接读出来。 在 Android 系统上,apk 最终打的是一个压缩包,那么 StreamingAssets 下的文件是怎么被直接读出来的,而其他文件夹里的文件不行。如果看 Android 的 Gradle 的 Build,不管是 AAPT(Android Asset Packaging Tool)或者是其他工具往 apk 里压东西,它身上是有选项的,利用这些选项我们可以指定哪些东西是不压缩,直接放进去的。 例如 [AAPT2](https://developer.android.com/studio/command-line/aapt2) 的 link 指令里,我们可以用 **-0** 来指定哪些文件不压缩: ![](https://pic4.zhimg.com/v2-b7db2ae6e8314fc623860f31e0bc66f7_r.jpg) 也就是说,Unity 在 Build apk 的时候就会对 StreamingAssets 下的文件标记为不压缩,也就是说它是真正的原样放进去了,那么读取的时候就可以直接读取出来了,不需要解压缩的操作。 ### 4. 害羞的波浪线 当我们把工程项目里的某个文件夹的名称后面加上一个小小的波浪线,那么 Unity 就会帮我们在项目中隐藏掉这个文件夹。这个技巧在做工程打包工程配置的时候非常的有用,很多的开发者在使用的时候呢没有注意过它,但是用起来却非常的好用。例如在开发的时候有些人很喜欢用 Resource 文件夹,然后 Build 的时候不希望要它,那么在 Build 的时候就可以在 Resource 后面加个波浪线。凡是这种以波浪线结尾的文件夹,在 Unity 里面是会被直接无视掉的。不会出现也不会导入,示意图如下: ![](https://pic1.zhimg.com/v2-b4da943af2eee90a5e7aa919d92d3e8c_b.jpg) 如图所示,在后面加了~ 的文件夹内的 Asset 都会被忽略,并且若之前有别的 Asset 引用了它们,就会导致 Missing 的情况出现。 3.Asset 与 AssetBundle --------------------- ### 1. 什么是 AssetBundle? [AssetBundle](https://docs.unity.cn/2021.1/Documentation/Manual/AssetBundlesIntro.html) 是什么?严格来说,AssetBundle 是 Asset 的一个集合,是个压缩包。是什么?严格来说,AssetBundle 是 Asset 的一个集合,是个压缩包。相比直接使用 Asset,它有如下几个好处: 1. 可以帮我们解决文件之间的依赖关系,一个项目中里面的资源依赖是非常复杂的,AssetBundle 可以帮我们解决这样的问题,即 [Dependencies](https://docs.unity3d.com/Manual/AssetBundles-Dependencies.html),但是也可能造成资源冗余的新问题。 2. 可以做一个跨平台,在打包的时候我们希望在不同的平台上用不同的东西,比如说不同平台不同的文件格式,希望打成不同的 bundle。 3. 可以帮我们做一个快速的索引。 4. 它是被压缩过的,可以节省内存。 所以说 AssetBundle 实际上是 Unity 的一套虚拟文件系统,它延展了 Unity 的跨平台性,使我们的 Build Pipline 的代码是可以一致性的,也就是说我们只需要写一份代码就可以打各个平台的 AB,只需要简单的调整一些参数即可。 AssetBundle 简单来说由两部分组成的,一部分就是压缩的内容,叫内容体,还有一部分就是它的头。也就是摘要信息,官方示意图如下: ![](https://pic2.zhimg.com/v2-afa12c88dd6ac87458c8d8c56fe0ceed_r.jpg) 实际的 AssetBundle 要比这个更复杂,它包裹了很多层,最里面那层是大家经常看到的 cab 为开头的这种 key(如下图,我们用 AssetStudio 查看 AssetBundle 时就可以看见这个 key),再外面一层叫 artifactKey,两层去包裹这样的一个 AssetBundle。 ![](https://pic1.zhimg.com/v2-c9986a6da43b7d50bac75cab56e152f4_r.jpg) 还有种特殊的 AssetBundle,就是 **Scene**,它是一个单独的 AssetBundle,因为它和其他的 Asset 的处理方式是不一样的,所以 Asset 和 Scene 是不能打到一起的,要分开打。 当我们加载一个 AssetBundle 的时候,它的头会立刻加载进内存,这个也是我们在 Profiler 里面经常看到的 **SerializedFile**。剩下的内容,也就是 Bundle 里面的 Asset,它是**按需加载**的。也就是说如果我们不去加载这个 Asset,它是不会从包体里被加载到内存中的。但是有一个例外,就是默认的 [LZMA](https://docs.unity.cn/2021.1/Documentation/Manual/AssetBundles-Cache.html) 的压缩,这种压缩格式用一个数据流代表整个 AssetBundle,因此要读取里面任意一个 Asset 的时候需要解压整个数据流。 ### 2.AssetBundle 的参数 当我们调用 Unity 的 API 去打 AssetBundle 的时候,实际上有很多的参数可以供我们选择。如果没有选择合适的参数,就可能会导致在包体,内存以及加载时间等方面造成很多的浪费。 实际上我们经常用到的有这么几个: 1. **ChunkBasedCompression**:这个参数是压缩 AssetBundle 的用的。前面提到 Android 的 StreamingAssets 是不压缩的。为了减小包体大小,可以使用该参数对 AssetBundle 进行压缩。它实际上是一个由 Unity 改良过的 LZ4,使它的算法更符合 Unity 的使用方式。 2. **DisableWriteTypetree**:这个其实是会被很多开发者忽略的一个参数,它非常有用,可以帮我们减小 AssetBundle 包体的大小,同时也可以减小内存,以及减少我们加载这个 AssetBundle 时的 CPU 时间。 3. **DisableLoadAssetByFileName**,**DisableLoadAssetByFileNameWithExtension**:当我们加载好一个 AssetBundle 然后使用 LoadAsset 加载 Asset 的时候,需要传递 Asset 的路径名称。这个名称有三种写法,分别是 Asset 的文件名,Asset 的文件名 + 扩展名,Asset 的全路径,如下: ``` AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "sphere")); Instantiate(ab.LoadAsset("Sphere")); Instantiate(ab.LoadAsset("Sphere.prefab")); Instantiate(ab.LoadAsset("Assets/Sphere.prefab")); ``` 如果我们打 AssetBundle 时,不设置上面两个参数,那么使用这三种名称都可以正确的加载 AB 里面的 Asset。但是其中只有全路径是被序列化到 AssetBundle 当中的,我们查看对应的. manifest 也可以发现里面存储的是全路径: ``` Assets: - Assets/Sphere.prefab ``` 而文件名和文件名 + 扩展名是在 AssetBundle 被加载成功后产生的,因此就会产生一定的代价的。当我们没有 Disable 打 AssetBundle 的时候,实际上是算了一个 Hash 进去的,当通过文件名去找 Asset 的时候,它会去生成这个文件名的原路径,然后去对比。所以呢,在 CPU 时间和内存上多多少少会有一些消耗。如果我们确定我们的加载 Asset 的方式是用全路径加载的话,那么就可以把它关闭掉。 实践出真知,我们来简单的测试一下,假如我们有如下代码来生成不同设置下的 AssetBundle: ``` public class AssetBundleEditor : Editor { [MenuItem("Build/BuildBundleWithUncompresse")] public static void BuildBundleWithUncompresse() { BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, BuildAssetBundleOptions.UncompressedAssetBundle, BuildTarget.StandaloneWindows64); } [MenuItem("Build/BuildBundleWithTypeTree")] public static void BuildBundleWithTypeTree() { BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64); } [MenuItem("Build/BuildBundleWithoutTypeTree")] public static void BuildBundleWithoutTypeTree() { BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DisableWriteTypeTree, BuildTarget.StandaloneWindows64); } [MenuItem("Build/BuildBundleWithoutExtraName")] public static void BuildBundleWithoutExtraName() { BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DisableLoadAssetByFileName | BuildAssetBundleOptions.DisableLoadAssetByFileNameWithExtension, BuildTarget.StandaloneWindows64); } } ``` 同样的 Prefab 在四种设置下得到的 AssetBundle 大小如下图: ![](https://pic2.zhimg.com/v2-6086c85e1882542adbc1241de6df3ab9_r.jpg) 可以发现在都使用压缩的情况下,用和不用 TypeTree 大小差了将近 30%,可见如果不写入 TypeTree,那么当 AssetBundle 很多的时候,可以节省的大小是非常可观的。个人测试了下,大约 2.5G 的 AssetBundle,如果不写入 TypeTree 可以缩小到 2.2G。 但是也会发现,不写入 FileName 这些,AssetBundle 似乎并没有什么缩小,这是因为我们的 AssetBundle 太小了,名字只有一个 sphere,所以基本看不出大小上的变化,若点击查看详情,可以发现大约有 1 字节的变化。DisableLoadAssetByFileName 更主要的是针对 CPU Time 和运行时内存的优化。 接下来来看看运行时的情况,写一个简单的 Demo,来看下不同参数打出来的 AssetBundle 在内存中的使用情况。点击按钮的时候调用下面代码,加载我们的 AssetBundle,首先测试的是使用 ChunkBasedCompression 打出的 AssetBundle: ``` void LoadAsset() { AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "sphere")); Instantiate(ab.LoadAsset("Sphere")); } ``` 然后我们打个 EXE 文件出来测试,一定要在运行时环境测试,build 时勾选 Development Build。然后运行我们的 exe 文件,Profiler 窗口选择我们运行的程序,点击 Load 按钮,Take Sample 查看 Memory 项,如下图: ![](https://pic4.zhimg.com/v2-002dc2c43ddfe255ccecb7e9b8a4da4f_b.jpg) 会发现我们的 AssetBundle 被加载进了内存(如下图),前面提到 CAB-xxx 包含的是 AssetBundle 头的信息,因此后面跟着的大小也是 AssetBundle 头的大小。 ![](https://pic3.zhimg.com/v2-586afbcb11d001c216a29458ac421d9e_r.jpg) 注意,如果在编辑器里面运行这个 Demo 测试,你会发现在 SerializedFile 下加载了茫茫多的资源,这也是因为 Unity 编辑器为了保证我们编辑时的体验,会预先加载一部分资源,因此我们需要打包出来在运行时测试,得到的结果才是真实环境下的结果。 ![](https://pic3.zhimg.com/v2-8d88446ef78b382071d0a54664695016_r.jpg) 然后我们把再加上 DisableWriteTypeTree 选项来测试一下,得到的结果如下,会发现不写入 TypeTree 的话,内存空间同样减少了一大截。 ![](https://pic3.zhimg.com/v2-d287b6c76d5f63c3fa61c55f966d23be_r.jpg) 接着我们使用 DisableLoadAssetByFileName(WithExtension) 来打测试一下,会发现我们的 Asset 加载不出来了,因为 LoadAsset("Sphere") 的结果为 null 了,只有使用全路径才能正确的加载。 ![v2-d8203e0ab591a18097f5adb9aae6ebee_1440w9f633132531232b9.jpg](https://tools.nxcloud.club:12500/images/2021/10/02/v2-d8203e0ab591a18097f5adb9aae6ebee_1440w9f633132531232b9.jpg) ### 3.AssetBundle 的识别 当我们前后两次打出 AssetBundle 的时候,如何判断哪些 AssetBundle 是有差异的,哪些 AssetBundle 是没有发生变化的呢? 很多人会通过计算两次打出来 AssetBundle 的 md5 来判断是否发生变化,实际上这种方式是不推荐的。因为在 Unity 打包的过程中,有一些因素是不稳定的,可能导致你两次打包之后的 AssetBundle,虽然你里面的东西没有变,但是打出来的 Binary 不是严格一致的,从而 md5 也是不一样的。所以不建议算打出来之后的 AssetBundle。那怎么算呢?我们可以算 Library 里的文件的 md5,或者是原文件以及对应的 meta 文件的 md5,用这些算出来的 hash 做为 AssetBundle 的变化依据是可以的。 在跟 AssetBundle 一起生成出来的 .manifest 文件中,包含有 AssetFileHash 的字段,可以用来作为我们的识别依据。此外在 .manifest 中还有个 [CRC](https://zh.wikipedia.org/zh-cn/%E5%BE%AA%E7%92%B0%E5%86%97%E9%A4%98%E6%A0%A1%E9%A9%97)(cyclic redundancy check)的字段用来判断 AssetBundle 的完整性,也常常被加入识别依据当中。 ### 4.AssetBundle 的策略 那么我们 AssetBundle 的大小怎么样是最合适的呢?简单来说不要去走极端,打的过大或过小都是不好的。 AssetBundle 过大的问题: 1. 不容易被下载下来,手机的网络速度相对比较慢,如果 AssetBundle 很大且没有断点续传,万一用户下了一半失败了,那么就得重新下载,极端的情况下可能就是根本下不下来。 2. 如果一个 AssetBundle 里面的东西非常非常的多,就会导致 AssetBundle 头里面记录的信息摘要,也会非常非常的多。 AssetBundle 过小的问题: 1. 例如打了一个 1K 的 AssetBundle,那么 AssetBundle 头的占比就会非常的大,这样有效的数据量就非常的小,大部分的数据量都变成了头文件。 Scene 打包 AssetBundle 是合适的,只是说一些组件不要都扔到一个 AssetBundle 里。官方推荐 1-2M 是一个比较合适的大小,5G 普及后,5-10m 也 ok。这些指的是需要从网上下载的 AssetBundle,如果是放在 StreamingAsset 下本地带的 AssetBundle 就可以 5-10m 左右,不要大于 10M,如果大于 10M 就会有很多问题出现。 4.Asset 的加载及管理 -------------- ### 1.Editor 和 Runtime 加载机制不同 比如说前面查看 Profiler 时看的 SerializedFile 项,在 Editor 下可以到五十几个,为什么呢?因为使用 Unity 编辑器的时候,Unity 首先保证的是开发过程中的流畅度。因为开发环境的设备一般比较好,所以会尽量把一些资源都提前加载进来,甚至有些情况下,会去额外的加载一些数据来方便和加速大家的编辑和制作过程。 而在 Runtime 的时候,Unity 会严格保证按需加载的方式,来尽量节省目标设备上的内存和 CPU,所以它们是完全不一样的两套模式,加载机制也都是完全不一样的。因此我们不能用以 Editor 的 Profiler 去当做衡量标准,一定要去 Profiler 真机,这个才是最终的衡量标准。 ### 2. 序列化和反序列化 先来做一个简单的测试,新建一个空场景,里面什么都没有,然后我们创建三个 Cube 在场景内,如下图: ![v2-0c993c2b6e60b355d116f18ede9dfa82_1440w.png](https://tools.nxcloud.club:12500/images/2021/10/02/v2-0c993c2b6e60b355d116f18ede9dfa82_1440w.png) 接着我们用文本编辑器打开该 Scene 文件看一看,会发现里面 Cube 相关的 Object 信息会有三份,例如三份 Transform,三份 MeshRenderer,整个文件大概有 400 行左右。 接下来我们把这三个 Cube 删掉,创建一个 Cube 的 Prefab,放三个该 Prefab 在场景内,如下图: ![v2-24cac18fb43aa7f80d61ef8f712aac4f_1440w.png](https://tools.nxcloud.club:12500/images/2021/10/02/v2-24cac18fb43aa7f80d61ef8f712aac4f_1440w.png) 再来看看 Scene 文件,神奇了,该文件变得只有 300 行左右了,原本很多重复的 Object 也没有了,变成了三份 **PrefabInstance** 信息: ``` --- !u!1001 &2515783927632439704 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: m_TransformParent: {fileID: 0} m_Modifications: - target: {fileID: 2515783927589610787, guid: d8e3e5db1969c994dacbd9ce76c282fa, type: 3} propertyPath: m_Name value: Cube objectReference: {fileID: 0} - target: {fileID: 2515783927589610791, guid: d8e3e5db1969c994dacbd9ce76c282fa, type: 3} propertyPath: m_RootOrder value: 0 objectReference: {fileID: 0} - target: {fileID: 2515783927589610791, guid: d8e3e5db1969c994dacbd9ce76c282fa, type: 3} propertyPath: m_LocalPosition.x value: 0 objectReference: {fileID: 0} ...... m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: d8e3e5db1969c994dacbd9ce76c282fa, type: 3} ``` 可以看出使用 Prefab 的方式,会利用一个 PrefabInstance 通过 guid 来引用外部的 Prefab,而不是把所有的 Object 信息都添加进来,来减少数据量。Unity 在根据 Asset 做序列化和反序列化的时候,是以 Asset 文件里的单位来做的,所以可以简单的理解为,里面的内容越多,要解析的也越多,速度就越慢。 这就意味着当 Unity 去解析打到版本里的场景的时候,解析使用了 Prefab 的场景会更快,而且更省内存。因为在解析使用了 Prefab 场景的时候,Unity 会优先把 PrefabInstance 指向的 Prefab(例子中的 Cube)解析出来,并且**让场景中相同 Prefab 的引用指向同一块内存**,所以在内存中,这三个 Cube 相对比较省的,只解析了一遍。而对于不使用 Prefab 的第一种情况,Unity 会认为那三个 Cube 是完全不同的三个东西,所以在读取这个场景的时候 Cube 会被解析三遍,这个过程就浪费了。 若我们做场景的时候拖一堆白色的东西在底下(即没有使用 Prefab),就会导致整个场景的加载速度非常的慢。所以建议大家能用 Prefab 的地方尽量用 Prefab,这不仅制作过程会变得方便,在 Runtime 时性能也会有所提升。 ### 3.TypeTree(兼容性之树) 在前面介绍打 AssetBundle 选项的时候,提到了不写入 TypeTree 的种种好处,那么既然写入 TypeTree 有这么多的缺点,Unity 为什么还要默认写入呢?因为它是为了给 Unity 跨版本之间做兼容性用的。 不管是在 Prefab 文件里或者是 meta 文件里,经常会看到有一个字段叫作 **serializedVersion**,值是一个数字,它代表着当前 Object 或者是 Importer 的数据格式版本。因为随着 Unity 引擎不断的发展,需要更多或更少的数据格式来描述一个数据内容,当我们每次去做数据更改的时候都会去修改 serializedVersion,来表示当前我用的是哪一个版本。 例如导入同一张图片,在不同 Unity 版本下的. meta 文件如下: ![](https://pic4.zhimg.com/v2-69a65f9ec89c5bc87e19a8ff86d0653f_r.jpg) 通过 serializedVersion 可以知道 2020.3.13 的版本在 TextureImporter 的结构上比起 2018.4.20 版本已经修改过两次了,例如新版本里多了名为 vTOnly 的属性。 如果我们不开启写入 TypeTree,Unity 在打 AssetBundle 的时候,只会把 Object 以及 Importer 里的值写入到 AssetBundle 当中,例如图中的 0、0、1、0、0 等等。然后当我们使用这个 AssetBundle 的时候,会按照当前 Unity 的版本对应的格式反向解析这些值,例如第一个值 0 是 mipMapMode,第二个值 0 是 enableMipMap,第三个值 1 是 sRGBTexture。 这样就会存在一个问题,假如我们的 AssetBundle 是 2018 版本打的,然后使用是在 2020 的版本,那么在反向解析的时候,格式按照的是 2020 的,那么原本是 grayScaleToAlpha 的值会被赋值到 vTOnly 上,并且后面的值也全乱了,就会导致 AssetBundle 在跨版本后就用不了。 而 TypeTree 就是上诉问题的一个解决方案,当我们开启 TypeTree 的写入,Unity 在打 AssetBundle 时会先把数据内容的树状结构先写入一遍,即 mipMapMode、enableMipMap、sRGBTexture 这些字段,然后才去写入它们的值,这也就导致了 AssetBundle 的大小增加。然后在使用的时候,Unity 会先解析 TypeTree,然后再去反向解析数据内容。这样当我们用 2020 解析 2018 的 AssetBundle 时,发现有个字段 vTOnly 是 TypeTree 里没有的,那么就会使用默认值填充。再比如有个字段是 2018 有的,2020 没有的,那么会丢弃这个字段对应的值,防止反向解析出错。 也就是说 Unity 通过 serializedVersion 以及 TypeTree 的方式,来把跨版本兼容性给做出来了,这就是 TypeTree 的作用。所以如果你写入了 TypeTree,那么你的 AssetBundle 中就会额外增加所有 TypeTree 的信息,还会在 CPU 加载的时候额外遍历一遍 TypeTree,同时会在内存中生成一个 TypeTree 的结构。所以这就是为什么磁盘空间加了,内存加了,CPU 时间也加了。 那在什么情况下,我们可以不用 TypeTree 呢?当你确认项目打 apk,ipa 这些包的 Unity 版本和打 AssetBundle 的版本是一致的,那么这个时候就可以放心的关掉 TypeTree,去节省掉这一部分内容。绝大多数的项目都可以关掉,除非项目需要做跨版本兼容。 ### 4. 同步和异步 在加载 Asset 的时候,Unity 提供了有同步和异步的 API,例如 LoadAsset 和 LoadAssetAsync,那么它们分别什么时候选用比较好呢? 实际上这只是一个策略的问题,并没有哪个更好。同步最大的优点是快,因为在这一帧里面主线程所有的 CPU 全都会归你用,所有的时间片全都归你用,它可以一门心思的把这件事情做完,再做其他的事情。但是同步的问题就是会造成主线程卡顿。异步可以简单的理解为多线程(其实还是有点区别的),最大的优点是不怎么会造成主线程的卡顿(也不是完全不卡顿),主线程可以尽量不卡顿的去跑。但是**异步永远比同步至少慢一帧**,也就是说我这一帧发起的异步,最快也要下一帧才会开始执行。而且异步涉及到一个时间片的问题,所以有的时候异步的总体时间会比同步来的长。比如你用同步去加载一个东西,可能 3ms 就加载完了,但是你用异步去加载可能就要 5ms 或 6ms 才加载完,甚至更长。但是异步花费的这些时间是分布在多个帧里面的,在后台线程里面去跑的,所以它会尽量减少卡顿。 也就是说你在一个对卡顿非常敏感的场景里面,比如战斗时的场景,那么你可以使用异步的方式,然后做一些兼容方式,保证它没有加载完之前有一些处理。但是你是在 Loading 的时候完全可以考虑分帧去使用同步,但是不能一帧里面加载太多东西,这样的话整体的速度会变快。 还需要注意的是,异步和同步如果混合使用,是会有问题的,下面一点就会介绍这个问题。 ### 5.Preload 与 Presistent 在 Unity 引擎内部,是由 **PreloadManager** 与 **PresistentManager** 来主要负责加载的。PreloadManager 负责调度任务,当上层有一个任务下来,比如说我要去加载 Asset,那么它会形成一个新的 **Operation**,这个 Operation 会给到 PreloadManager。在 PreloadManager 里面有个队列,然后每一帧会从里面去取出一个任务(也就是一个 Operation)去执行它。而当 Operation 执行的时候,会去调用 PresistentManager(持久化管理器)。PresistentManager 的主要的任务是把文件从硬盘上读取到内存当中,同时去给它分配一个 id。 如果我们现在有个 Operation 是异步的,它正在执行。而在下一帧,PreloadManager 又加载了一个同步的 Operation,这就会导致同步的和异步的 Operation 会去抢着用 PresistentManager。而 PresistentManager 分配 ID 和做 IO 这些都是要阻断线程的,所以它会 zao 造成 block。也就意味着你的异步工作可能会被你的同步工作阻断,反过来也有可能。所以同时使用的时候,会经常看见一个非常长的 Loading.LockPersistenManager,锁的现象出现。 注:在 2020 的版本里,应该会解决这个锁的问题,因此如果一起使用,主线程阻塞的情况会变少。 参考: [Jeffrey Zhuang:[U3D] GetPreloadData 崩溃分析](https://zhuanlan.zhihu.com/p/113049982) [AssetBundle lockpersistentmanager 开销 -- UWA 问答](https://answer.uwa4d.com/question/59eae0c4e0a5e637724a1572) 4.Asset 的卸载 ----------- ### 1.UnloadUnusedAssets 它可以卸载掉那些没用的 Asset,把它从内存中清除掉。它也是个 Operation,它和加载一样,也是归 PreloadManager 处理的,它必须独成的,不能并行。因为 Unity 在一次 Load Operation 开始的阶段就已经确定了哪些 Asset 要被 Load,所以在 Load 的过程中又发生了 Unload 这样的操作,那就会导致有些确定了使用且已经被 Load 的 Asset 被卸载掉了,就会导致最后的出错。 所以 Unity 现在的设计是一个同步的过程,所以这个过程会造成卡顿。Unity 在切换 Scene 的时候会自动调用一次 UnloadUnusedAssets,如果是通过 Scene 来管理的话就没太大的必要关心造成的卡顿了。如果不是,那就需要自己找些合适的时机去调用一下。 ### 2.AssetBundle.Unload 它又分 true 和 false,但是无论哪一个都和上面的不一样,它不是一个 Operation,也就是不归 PreloadManager 管。它会遍历当前加载过的东西,然后去把它删掉。 如果是 true 那就是把 AssetBundle 和它加载出来的 Asset 全都一起干掉。这个在不合适的时机就有可能发生 Runtime 的错误。如果是 false,那么只是把 AssetBundle 给丢掉,Asset 是不会被扔掉的。那么当你第二次去加载同一个 AssetBundle 的时候,在内存中就会有两份 Asset,因为当 AssetBundle 被卸载的时候,它和对应的 Asset 的关系就被切割掉了。所以 AssetBundle 不知道之前的 Asset 是不是还在内存中,是不是从自己这加载出来的。所以使用 AssetBundle.Unload 就很考验游戏的规划。 Unity 为什么不做成 Reference?因为 Unity 内部对于这些 Asset 实际上是没有 Reference 的,很多时候是通过遍历去查找,实际上不存在大家想象的 ReferenceCount,它和 C# 其实是不太一样的。目前 Unity 也是正在解决,或者用 [Addressables](https://docs.unity.cn/Packages/com.unity.addressables@1.19/manual/index.html) 可以解决一部分的。 视图变换和投影变换矩阵的原理及推导,以及OpenGL,DirectX以及Unity的对应矩阵 A-Star(A*)寻路算法原理与实现