对于Assets一般来说我们有两层认知,一层来自于Unity的默认工程目录Assets,一层来自于Unity的打包系统AssetBundles。那么我们就从这两个方面来归一化的去理解Unity的Assets究竟是什么。
当我们用Unity创建一个工程的时候,大家有没有关注过工程下面默认创建的几个目录呢?我们用Unity2018.3.8创建一个空工程看一下(本系列文章的工程或者示例都基于Unity2018.3.8创建,后面不再重复申明),目录如下:
一共有4个目录,它们的作用分别是:
当你的工程开始有脚本文件的时候,还会增加一个obj的目录用于代码编译。所以当你要迁移一个工程,或者将工程复制给别人的时候,只需要将Assets 、Packages以及ProjectSettings三个目录备份即可,至于Library会在Unity打开的时候进行检查和自动转化。当然如果你的工程非常庞大,资源非常多,那么迁移的时候连Library一起拷贝传递会节省大量的转化时间。
另外,实际的项目开发期间,我们可能要针对不同的平台进行打包,比如PC包和安卓包。那么复制两份工程,一份设置为PC平台,一份设置为安卓平台 效率会远远大过于需要时再切换平台,因为你每次切换到不同的平台,Unity都需要全部重新处理一遍内置资源,非常非常耗时。
抛开所有其他的理解,单从英文命名来看,这是一种捆绑包,是对Asset进行归档的格式,概念更趋向于我们使用zip或者RAR等格式对资源或者目录进行压缩、加密、归档、存储等等。而区别就在于zip等压缩格式是针对文件的,而AssetBundles则是针对Unity的Asset。但如果再转换一下概念来理解的话,zip其实操作归档的是操作系统能识别的文件,而AssetBundles操作归档的则是Unity能识别的文件,这么理解的话二者的作用几乎是一致的。
做了铺垫之后,我们再来看一下Unity官方是怎么定义的:
AssetBundles是一个包含了特殊平台、非代码形式Assets的归档文件。
官方文档链接:https://docs.unity3d.com/Manual/AssetBundlesIntro.html
这里有几个重要信息,首先它是一个归档文件(即捆绑形式的文件类型)、其次它拥有平台的差异性、再次它不包含代码、最后它存储的是Unity的Assets。
到目前为止,我们一直在提及Assets,那么究竟Assets是什么呢?以及它在Unity整个引擎中占据的位置又是什么呢?
windows操作系统识别文件是通过后缀名实现的。在系统中注册后缀名和对应的处理软件,那么双击文件的时候系统就会调用指定的软件解析和处理文件。如果没有在系统中注册,或者后缀被删除了,那么操作系统将无法识别这个文件。
Unity的Asset也是一样,我们把一个Asset叫做一个资产,可以理解为Unity能够识别的文件。这里其实又包含两种类型,一种是Unity原生支持的格式,比如材质球;一种是需要经过Unity处理之后才能支持的,比如 FBX。对于需要处理才能支持的格式,Unity都提供了导入器(Importer)。如下图,代码里输入Importer可以看到有很多很多个种类的导入器。
要注意,所有的资产原始文件都必须要放在Unity工程的Assets目录,然后经过Unity处理之后存放在Library目录下。
作为资产文件,Assets有非常多的类型。比如材质球、纹理贴图、音频文件、FBX文件、各种动画、配置或者Clip文件等等。我们通常习惯于在Unity里进行拖拽、新增、修改、重命名甚至变更目录等等各式各样的操作,但不管你在Unity引擎里如何操作(不包括删除),那些相关的引用都不会丢失。这是为什么呢?
在进行后面的阐述之前,先统一一下概念,包括如果在后面章节里提到的话,都会遵循这里统一的概念。Assets这里以及后续的内容都指Unity的资产,可以意指为Unity的Projects窗口里看到的单个文件(或者文件夹)。而Objects这里我们指的是从UnityEngine.Object继承的对象,它其实是一个可以序列化的数据,用来描述一个特定的资源的 实例 。它可以代表任何Unity引擎所支持的类型,比如mesh,sprite, AudioClip or AnimationClip等等。
大多数的Objects都是Unity内置支持的,但有两种除外:
Assets和Objects之间是一对多的关系,比如一个Prefab我们可以认为是一个Asset,但是这个Prefab里可以包含很多个Objects,比如 :如果是一个UGUI的Prefab,就可能里面会有很多个Text、Button、Image等组件。
熟悉Unity的人都知道,UnityEngine.Objects之间是可以互相引用的。这就会存在一个问题,这些互相引用的Objects有可能是在同一个Asset里,也有可能是在不同的Assets里。比如UGUI的一个Image需要引用一张Sprite Atlas里的Sprite。这就要求Unity必须有健壮的资源标识,能稳定的处理不同资源的引用关系。除此之外的话,Unity还必须考虑这些资源标识应该与平台无关,不能让开发者在切换平台的时候还需要关注资源的引用关系,毕竟它自己是一个跨平台部署的引擎。
基于这些特定的需求,Unity把序列化拆分成两个表达部分,第一部分叫做File GUID。标识这个资产的位置,这个GUID是由Unity根据内部算法自动生成的,并且存放在原始文件的同目录、同名但是后缀为.meta的文件里。
这里需要注意几个点:
确定了资产文件之后,还需要一个Local IDs来表示当前的Objects在资产里的唯一标识。File GUID确保了资产在整个Unity工程里唯一,Local ID确保Objects在资产里唯一,这样就可以通过二者的组合去快速找到对应的引用。
Unity还在内部维护了一张资产GUID和路径的映射表,每当有新的资源进入工程,或者删除了某些资源。又或者调整了资源路径,Unity的编辑器都会自动修改这张映射表以便正确的记录资产位置。所以如果.meta文件丢失或者重新生成了不一样的GUID的话,Unity就会丢失引用,在工程内的表现就是某个脚本显示“Missing”,或者某些贴图材质的丢失导致场景出现粉红色。
前面我们提到了非Unity支持的格式,需要由导入器进行资源转换。之所以要分到这个小节来讲位置是因为它涉及到了File GUID。之所以需要对资源转换和存储,也是为了方便下一次启动时候不需要再处理资源,比较每次导入资源是巨耗时的操作。
简单来讲,所有的转换结果都会存储在Library/metadata/目录下,并以File GUID的前两位进行命名的文件夹里。比如这样:
注意:原生支持的Assets资源也会有同样的存储过程,只是不需要再用导入器进行转化而已。
File GUID和Local ID确实已经能够在编辑器模式下帮助Unity完成它的规划了,与平台无关、快速定位和维护资源位置以及引用关系。但若投入到运行时,则还有比较大的性能问题。也就是说运行时还是需要一个表现更好的系统。
于是Unity又弄了一套缓存(还记得前面那套缓存嘛,是用来记录GUID和文件的路径关系的)PersistentManager,用来把File GUIDs 和 Local IDs转化为一个简单的、Session唯一的整数。这些整数就是Instance Id。Instance Id很简单,就是一个递增的整数,每当有新对象需要在缓存里注册的时候,简单的递增就行。
有关PersistentManager的详细分析,我找到了一篇侑虎学堂的文章,大家可以在这里详细了解:
《深度剖析PersistentManager.Remapper内存占用》 地址:https://edu.uwa4d.com/course-intro/0/113?
简单的来说:
PersistentManager会维护Instance ID和File GUID 、Local ID的映射关系,定位Object源数据的位置以及维护内存中(如果有的话)Object的实例。只要系统解析到一个 Instance ID,就能快速找到代表这个Instance ID的已加载的对象。如果Object没有被加载的话,File GUID 和 Local ID也可以快速的定位到指定的Asset资源从而即时进行资源加载。
本来这章节的内容我是打算整体配图讲解的,但我其实发现两篇以及写好的并且配好图的大佬文章,所以就不重复造车了,直接给出链接:
一篇是来自侑虎 US的博客文章:
《Unity文件、文件引用、Meta详解》,地址:https://blog.uwa4d.com/archives/USparkle_inf_UnityEngine.html
另外一篇是来自腾讯GAD的:
《程序丨入门必看:Unity资源加载及管理》,地址:
https://mp.weixin.qq.com/s/0XFQt8LmqoTxxst_kKDMjw?
另外Unity的Prefab或者meta文件都是使用YAML的语法格式存储的,因为放了传送门链接,所以这里就不细讲了,有关YAML的语法详情,大家可以参考:
https://baike.baidu.com/item/YAML/1067697?fr=aladdin
到现在为止我们已经搞清楚了Unity的Asset在编辑器和运行时的关联和引用关系。那么接下来我们还要关注一下这些资源的生命周期,以及在内存中的管理方式,以便大家能更好的管理加载时长和内存占用。
当Unity的应用程序启动的时候,PersistentManager的缓存系统会对项目立刻要用到的数据(比如启动场景里的这些或者它的依赖项),以及所有包含在Resources 目录的Objects进行初始化。如果在运行时导入了Asset或者从AssetBundles(比如远程下载下来的)加载Object都会产生新的Instance ID。
另外Object在满足下列条件的情况时会自动加载,比如:
1、某个Object的Instance ID被间接引用了。
2、Object当前没有被加载进内存。
3、可以定位到Object的源位置(File GUID 和 Local ID)。
另外,如果File GUID和LocalID没有Instance ID,或者有Instance ID,但是对应的Objects已经被卸载了,并且这个Instance ID引用了无效的File GUID和LocalID,那么这个Objects的引用会被保留,但是实际Objects不会被加载。在Unity的编辑器里会显示为:“(Missing)”引用,而在运行时,根据Objects类型不一样,有可能会是空指针,有可能会丢失网格或者纹理贴图导致场景或者物体显示粉红色。
除了加载之外,Objects会在一些特定情况下被卸载。
1、当没有再使用的Asset在执行清理的时候,会自动卸载对应的Object。一般是由切场景或者手动调用了Resources.UnloadUnusedAssets的 API时候触发的。但是这个过程只会卸载那些没有任何引用的Objects。
2、从Resources目录下加载的Objects可以通过调用Resources.UnloadAsset API进行显式的卸载。但这些Objects的 Instance ID会保持有效,并且仍然会包含有效的File GUID 和 LocalID 。当任何Mono的变量或者其他Objects持有了被Resources.UnloadAsset卸载的Objects的引用之后,这个Object在被直接或者间接引用之后马上被加载。
3、从AssetBundles里得到的Objects在执行了AssetBundle.Unload(true) API之后,会立刻自动的被卸载。并且这会立刻让这些Objects的File GUID 、 Local ID以及Instance ID立马失效。任何试图访问它的操作都会触发一个NullReferenceException。但如果调用的是AssetBundle.Unload(false)API的话,那么生命周期内的Objects不会随着AssetBundle一起被销毁,但是Unity会中断 File GUID 、Local ID和对应Object的Instance IDs之间的联系,也就是说,如果这些Objects在未来的某些时候被销毁了,那么当再次对这些Objects进行引用的时候,是没法再自动进行重加载的。
另外,如果Objects中断了它和源AssetBundle的联系之后,那么再次加载相同Asset的时候,Unity也不会复用先前加载的Objects,而是会重新创建 Instance ID,也就是说内存里会有多份冗余的资源。
下一章节,我们谈谈Unity中的一些特殊目录。