【Unity】Asset简介 您所在的位置:网站首页 assets文件夹解密 【Unity】Asset简介

【Unity】Asset简介

2023-03-29 22:21| 来源: 网络整理| 查看: 265

前言

在开发Unity项目的时候,必然会经常和Asset打交道,例如我们工程的Assets文件夹下,基本都是各式各样的Asset,接下来会从以下几个方面来聊聊它:

Asset是如何产生的Asset的导入设置Asset和Assetbundle的一些关系Asset的加载与管理Asset的卸载

文章参考自:

Asset是如何产生的

Asset在我们平时使用的时候主要是由下面两种方法产生的:

用第三方的工具来产生Asset,这种情况最常见的有:FBX,Texture,Sound等。它们经常是由第三方工具(例如maya,3dmax,photoshop)等生成,然后放到Unity中来用。Unity自己产生的Asset,比如说Prefab,Scene,Animator文件等。

注:其中Script从Unity引擎的角度来看也是一种Asset,但是Unity在管理它以及进一步处理时,和其他的Asset有一定的区别。

不管哪种方式产生的Asset,一般都是由下面两部分组成的:

本身的数据内容,是文件的主旨,原始数据的所在。meta文件,主要是记录了一些额外的信息。

比如我们导入一张.png图片,导进来的png文件本身是一个数据内容,同时Unity会为你产生一个同名的meta文件。以及我们生成一个Prefab同样也会生成一个同名的meta文件。

对于第三方的Asset,例如.png、.wav、.fbx等文件,它们的文件内容和它们各自所对应的文件格式有关,不做过多介绍。我们来看看Prefab文件的内容,Sphere.prefab其实就是Unity自带的球体,其在Unity中的Inspector界面如下图:

接着我们使用文本编辑器来打开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。其中第一行第二行记录的类似于一个版本信息。

接着我们看后面的内容,可以发现这些信息被 --- 给切割成一块一块的,每一块代表的都是一个Object所对应的信息(在Unity中所有的类都是继承于Object),并且每一块的开头的格式都是由 !u! 后面跟两串用 & 符合相连的数字,例如:

---!u!1&2704994518885799472

其中第一串数字,我们称之为ClassID,是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模式即可:

选择Debug

其中Local Identfier In File对应的就是fileID的值。利用该方法,我们还可以查看第三方产生的Asset的fileID,例如.png,.wav这些文件的fileID,如下图:

从中可以发现,所有相同类型的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界面一一对应起来,如下图:

因此如果你想批量修改一些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:

那么查看material文件时会发现下面一行:

- _MainTex: m_Texture: {fileID: 2800000, guid: 6ad45645da157cf4989f9c4390aad785, type: 3}

fileID:2800000前面已经介绍了,所有的Texture2D的fileID都是2800000。此外这里又来了个新的id:guid,它是Asset文件的唯一ID(介绍meta文件的时候再详细介绍)。如果我们打开这张图片对应的meta文件就可以看到这个id,如下:

因此如果Asset里面有fileID和guid的组合,那么它就是指向另一个Asset的Object。如果我们把Material上的图片换一换,其实改变的就是这个guid,如下:

修改被引用的资源

若我们打包的时候不想要某个资源,只需要打包前遍历所有Asset文件,找到引用它的地方,设置为 {fileID: 0} 即可。

3.绑定事件:

有时候我们可能会在Animation文件里添加Event来调用一些函数,如下图:

此时这些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又可分为下面两种:

运行时(Runtime)Asset,它比较好理解,例如生成的纹理,声音,动画,最终打成包的时候是要跟着你的游戏一起发布出去的。玩家去玩的时候会看见这些东西,我们管这些Asset叫运行时的Asset。编辑期(Editor)Asset,指的是Editor里面,当我们做一些Editor设置的时候,它会生成一些Asset,这些Asset最终不会打到你的包里,但它会参与你整个的编辑以及生成包的过程,最常见的就是ProjectSettings文件夹里的Asset。还有一些是在生成运行期的Asset的时候,它会有一部分的信息内容,是用于Build或者是编辑的时候才会用到的数据内容。这些数据最终不会打到你运行时的包体里面,但他对于你的编辑和如何生成最终的包体是有指导意义的,这种也说是编辑器的Asset。Asset导入设置

当我们在Unity导入一些第三方的Asset的时候,在Inspector界面会发现有很多的设置选项,如下图是图片的相关设置:

这些设置我们称之为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,如下图:

Script Missing

可是明明该组件的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里面看见很多内容基本都是一一对应的。例如我们声音文件的设置:

不管是我们修改Unity里面的设置,还是修改.meta文件,另一边都会同步跟着变换。在很多时候,我们对于一些资源都会有一些特定的设置需求,例如图片在不同平台往往也会需要不同的format,UI图片关闭mipmap,声音文件开启ForceToMono等等。如果每次导入新资源都要手动来设置的话,实在是太愚蠢了,因此Unity为我们提供了 AssetPostprocessor类,利用它里面的AssetImporter对象我们可以在每次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等,以及当所有Asset都导入完成后的回调:OnPostprocessAllAssets。

此外,我们同样可以写一个脚本来遍历所有的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来维护由我们原文件转换后在Library里的文件。在Unity2019.3之前,它并不是真的Database,而是一套查询系统,称之为AssetPipeline Version 1。在Version 1中,Library下会有一个名为metadata的文件夹:

metadata

该文件夹里面存放的就是所有我们运行时真正用到的Asset,子文件夹通过Asset的guid前两位进行分类。例如我们导入一张图片,通过.meta文件发现它的guid如下:

那么我们就可以在5e的文件夹下找到与guid相同的文件:

这是一个二进制文件,可以通过Unity在安装目录下\Editor\Data\Tools提供的binary2text.exe工具将其转换成可读的文件,转换后的文件内容截图如下:

可以发现这里面存储了我们原文件图片的数据信息,除此之外还包含了meta信息,导入设置等等。

如果有时候我们发现我们的原文件更新了,但是在Unity里看见的效果或者打出来的AB包的内容都还是旧的,那么大概率就是因为Library里对应的文件没有重新生成,我们可以检查下里面的文件时间,对需要更新的文件Reimport一下,强制Library下对应的文件重新生成。

注:非必要的情况下,可别不小心点到Reimport All了,否则就要恭喜你,可以划水小半天了。

利用这个机制,在做AssetBundle的时候,可以算Library里面东西的系统时间来判断它需不需要被重新打一下,这样就可以实现只针对当前修改过的资源打AssetBundle。

但是在2019.3版本之后,Unity使用了一套新的Asset Database系统,叫Version 2。可以在Project Setting-Editor里面进行切换:

注:在Unity2020之后的版本就彻底弃用了Version 1,也就没有这个切换功能了。

Unity在Version 2里使用了名为LMDB(Lightning Memory-Mapped Database)的内存数据库,真正的把大家导入的东西放入到database里,因此使用Version 2的速度会比Version 1更加的快。

使用Version 2会发现,原本的metadata文件夹没了,多了一个叫Artifacts的文件夹,虽然里面的目录结构看着和原本的metadata一样,但是却无法根据guid找到对应文件了。不过当我们在Unity新导入一个Asset的时候,例如一张图片,还是可以发现在Artifacts下会同时新增两个文件,其中一个我们可以通过binary2text.exe进行转换,里面的内容如下图,包含了导入设置,以及最终的数据。

此外在Library下多了两个DB文件:SourceAssetDBArtifactDB

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以及Accelerator也有着密切的关系。

3.StreamingAssets文件夹

在我们打包的时候,往往会把事先生成好的AB包放到StreamingAssets目录下,该目录下的所有文件都会原封不动的打进包里,那是为什么呢?而且在Android系统上它们是可以被直接读出来。

在Android系统上,apk最终打的是一个压缩包,那么StreamingAssets下的文件是怎么被直接读出来的,而其他文件夹里的文件不行。如果看Android的Gradle的Build,不管是AAPT(Android Asset Packaging Tool)或者是其他工具往apk里压东西,它身上是有选项的,利用这些选项我们可以指定哪些东西是不压缩,直接放进去的。

例如AAPT2的 link 指令里,我们可以用 -0 来指定哪些文件不压缩:

也就是说,Unity在Build apk的时候就会对StreamingAssets下的文件标记为不压缩,也就是说它是真正的原样放进去了,那么读取的时候就可以直接读取出来了,不需要解压缩的操作。

4.害羞的波浪线

当我们把工程项目里的某个文件夹的名称后面加上一个小小的波浪线,那么Unity就会帮我们在项目中隐藏掉这个文件夹。这个技巧在做工程打包工程配置的时候非常的有用,很多的开发者在使用的时候呢没有注意过它,但是用起来却非常的好用。例如在开发的时候有些人很喜欢用Resource文件夹,然后Build的时候不希望要它,那么在Build的时候就可以在Resource后面加个波浪线。凡是这种以波浪线结尾的文件夹,在Unity里面是会被直接无视掉的。不会出现也不会导入,示意图如下:

如图所示,在后面加了~的文件夹内的Asset都会被忽略,并且若之前有别的Asset引用了它们,就会导致Missing的情况出现。

3.Asset与AssetBundle1.什么是AssetBundle?

AssetBundle是什么?严格来说,AssetBundle是Asset的一个集合,是个压缩包。是什么?严格来说,AssetBundle是Asset的一个集合,是个压缩包。相比直接使用Asset,它有如下几个好处:

可以帮我们解决文件之间的依赖关系,一个项目中里面的资源依赖是非常复杂的,AssetBundle可以帮我们解决这样的问题,即Dependencies,但是也可能造成资源冗余的新问题。可以做一个跨平台,在打包的时候我们希望在不同的平台上用不同的东西,比如说不同平台不同的文件格式,希望打成不同的bundle。可以帮我们做一个快速的索引。它是被压缩过的,可以节省内存。

所以说AssetBundle实际上是Unity的一套虚拟文件系统,它延展了Unity的跨平台性,使我们的Build Pipline的代码是可以一致性的,也就是说我们只需要写一份代码就可以打各个平台的AB,只需要简单的调整一些参数即可。

AssetBundle简单来说由两部分组成的,一部分就是压缩的内容,叫内容体,还有一部分就是它的头。也就是摘要信息,官方示意图如下:

实际的AssetBundle要比这个更复杂,它包裹了很多层,最里面那层是大家经常看到的cab为开头的这种key(如下图,我们用AssetStudio查看AssetBundle时就可以看见这个key),再外面一层叫artifactKey,两层去包裹这样的一个AssetBundle。

还有种特殊的AssetBundle,就是Scene,它是一个单独的AssetBundle,因为它和其他的Asset的处理方式是不一样的,所以Asset和Scene是不能打到一起的,要分开打。

当我们加载一个AssetBundle的时候,它的头会立刻加载进内存,这个也是我们在Profiler里面经常看到的SerializedFile。剩下的内容,也就是Bundle里面的Asset,它是按需加载的。也就是说如果我们不去加载这个Asset,它是不会从包体里被加载到内存中的。但是有一个例外,就是默认的LZMA的压缩,这种压缩格式用一个数据流代表整个AssetBundle,因此要读取里面任意一个Asset的时候需要解压整个数据流。

2.AssetBundle的参数

当我们调用Unity的API去打AssetBundle的时候,实际上有很多的参数可以供我们选择。如果没有选择合适的参数,就可能会导致在包体,内存以及加载时间等方面造成很多的浪费。

实际上我们经常用到的有这么几个:

1. ChunkBasedCompression:这个参数是压缩AssetBundle的用的。前面提到Android的StreamingAssets是不压缩的。为了减小包体大小,可以使用该参数对AssetBundle进行压缩。它实际上是一个由Unity改良过的LZ4,使它的算法更符合Unity的使用方式。

2. DisableWriteTypetree:这个其实是会被很多开发者忽略的一个参数,它非常有用,可以帮我们减小AssetBundle包体的大小,同时也可以减小内存,以及减少我们加载这个AssetBundle时的CPU时间。

3. DisableLoadAssetByFileNameDisableLoadAssetByFileNameWithExtension:当我们加载好一个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大小如下图:

AssetBundle Size

可以发现在都使用压缩的情况下,用和不用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项,如下图:

会发现我们的AssetBundle被加载进了内存(如下图),前面提到CAB-xxx包含的是AssetBundle头的信息,因此后面跟着的大小也是AssetBundle头的大小。

ChunkBasedCompression AssetBundle

注意,如果在编辑器里面运行这个Demo测试,你会发现在SerializedFile下加载了茫茫多的资源,这也是因为Unity编辑器为了保证我们编辑时的体验,会预先加载一部分资源,因此我们需要打包出来在运行时测试,得到的结果才是真实环境下的结果。

Editor环境下的Profiler

然后我们把再加上DisableWriteTypeTree选项来测试一下,得到的结果如下,会发现不写入TypeTree的话,内存空间同样减少了一大截。

ChunkBasedCompression | DisableWriteTypeTree AssetBundle

接着我们使用DisableLoadAssetByFileName(WithExtension)来打测试一下,会发现我们的Asset加载不出来了,因为LoadAsset("Sphere")的结果为null了,只有使用全路径才能正确的加载。

DisableLoadAssetByFileName(WithExtension) AssetBundle

3.AssetBundle的识别

当我们前后两次打出AssetBundle的时候,如何判断哪些AssetBundle是有差异的,哪些AssetBundle是没有发生变化的呢?

很多人会通过计算两次打出来AssetBundle的md5来判断是否发生变化,实际上这种方式是不推荐的。因为在Unity打包的过程中,有一些因素是不稳定的,可能导致你两次打包之后的AssetBundle,虽然你里面的东西没有变,但是打出来的Binary不是严格一致的,从而md5也是不一样的。所以不建议算打出来之后的AssetBundle。那怎么算呢?我们可以算Library里的文件的md5,或者是原文件以及对应的meta文件的md5,用这些算出来的hash做为AssetBundle的变化依据是可以的。

在跟AssetBundle一起生成出来的 .manifest 文件中,包含有 AssetFileHash 的字段,可以用来作为我们的识别依据。此外在 .manifest 中还有个CRC(cyclic redundancy check)的字段用来判断AssetBundle的完整性,也常常被加入识别依据当中。

4.AssetBundle的策略

那么我们AssetBundle的大小怎么样是最合适的呢?简单来说不要去走极端,打的过大或过小都是不好的。

AssetBundle过大的问题:

不容易被下载下来,手机的网络速度相对比较慢,如果AssetBundle很大且没有断点续传,万一用户下了一半失败了,那么就得重新下载,极端的情况下可能就是根本下不下来。如果一个AssetBundle里面的东西非常非常的多,就会导致AssetBundle头里面记录的信息摘要,也会非常非常的多。

AssetBundle过小的问题:

例如打了一个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在场景内,如下图:

接着我们用文本编辑器打开该Scene文件看一看,会发现里面Cube相关的Object信息会有三份,例如三份Transform,三份MeshRenderer,整个文件大概有400行左右。

接下来我们把这三个Cube删掉,创建一个Cube的Prefab,放三个该Prefab在场景内,如下图:

再来看看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文件如下:

数据结构差异

通过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引擎内部,是由PreloadManagerPresistentManager来主要负责加载的。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 崩溃分析

AssetBundle lockpersistentmanager开销 -- UWA问答

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可以解决一部分的。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有