一、概要
在了解unity的资源管理方式之后,接下来细谈一下Unity的资源是如何从磁盘中加载到运行时的内存中,以及又是如何被卸载的。这部分较为繁琐,可能会写较多的过程。
二、脚本资源的加载和卸载
在unity中的脚本资源,大体可以分为C++编译的引擎dll文件,c#编译的dll文件,lua脚本文件(基于lua热更的方式下)。
2.1 dll文件的加载和卸载
在工程的Library/ScriptAssemblies文件夹下,会有当前工程非引擎相关的dll文件列表:
在游戏启动的时候,会执行Assembly.Load的操作,将这些dll文件加载进进程中(editor类相关的dll不会被加载)。
能够加载,自然也能在运行时被卸载,所以目前一种热更新方案ILRuntime就是对dll文件进行加载,热更新,卸载,加载最新的dll这样的方式进行操作。这种热更新方式主要是针对Assembly-CSharp.dll/Assembly-CSharp-firstpass.dll进行操作。
如果不是主动进行卸载,那么这些被加载的dll文件,在游戏进程中,是不会被卸载释放的,只有在游戏进程结束的时候,才会被系统从内存中卸载出去。
2.2 lua文件的加载和卸载
lua由于脚本文件的属性,可以被当做类文本文件进行热更新,同时在游戏启动的时候才开启一个Lua虚拟机,在Lua虚拟机中才执行lua文件的require相关操作。
这类文件的加载,其实质就是将这部分代码读入到lua虚拟机的全局缓存中,而所谓的卸载,就是将这部分缓存置为nil,和上面的dll文件的加载和卸载含义有一些差异。
三、非脚本资源的加载和卸载
非脚本资源,才是整个游戏进程中需要处理的主要部分,会伴随整个游戏进程,直到游戏进程结束。
个人对unity对资源的加载过程的理解,其本质就是一个反序列化的过程。
3.1 Serialization and Instance
unity在序列化的时候,对于每个组件,也是单独逐个的执行序列化的操作的,其序列化信息的关键信息是文件本身的fileID, 以及依赖文件的fileID 和guid.
对应的,在unity的Instance操作中,unity会为该GameObject创建一个唯一的InstanceID, 在进程内部会缓存这样一个InstanceID <-> gameobject的映射关系表,同时 fileID/GUID/LocalID 会对应的映射到该文件的源文件存储位置。这个InstanceID具有唯一性,当InstanceID创建完成后,如果object没有被load,则会触发unity执行一次资源的load,基于fileID/Guid/local id来执行object的加载。
实际的游戏运行中,并不会直接依赖fileID/GUID来执行文件的映射,而是会将这两个ID转换成一个新的ID,所以在实际运行的游戏中是看不到fileID/GUID相关文件的。
3.2 InstanceID的创建和失效
在游戏启动的时候,会将启动场景以及Resources目录下的资源逐个创建对应的InstanceID, 放入到缓存中,这部分InstanceID的创建耗时会随着Resources目录包含资源的增多而增大,所以尽量减小这部分资源的数目。
在游戏启动完成后,后续按需加载的Object,都会对应的创建InstanceID, 在卸载该资源的时候,会对应使这个映射关系失效,后续重新加载该object的时候,是会被重新创建对应的InstanceID, 先前创建的InstanceID是不会被重新定位到新加载的Object的。
3.3 AssetBundle中文件的加载
现在在Unity中主要的资源管理是基于AssetBundle的管理,那么运行时,是如何从bundle中加载出想要的Object的?
3.3.1 Bundle文件的组成
在Unity中,bundle会被分为两种大类,场景Bundle和非场景Bundle,利用unity自带的WebExtract 和 Binary2Text两个工具,是可以解压bundle为文本文件的。
场景Bundle在使用WebExtract解压后,会得到两类文件:
• BuildPlayer-sceneName: 场景序列化文件,也就是hierarchy序列化的结果
• BuildPlayer-sceneName.sharedasset: 场景依赖的文件
普通bundle在使用WebExtract解压后,会得到两类文件:
• CAB-GuidString: 该二进制文件为该bundle的序列化文件,以及可能包含的具体Object文件
• CAB-GuidString.resS:如果包含这个文件,则上面的文件目前是不能转换成txt文件的
以转换成功的bundle的序列化文件为参照,可以分析主要包含以下几个部分:
1) External References:
可以理解为依赖的外部assetbundle,这儿并不是依赖的bundle的名字,而是类似的cab-guidstring的形式,可见unity内部对于bundle的相互依赖处理,是基于这样一套的命名来进行管理的。
2) object map:
当前bundle包含的object的信息map:
3) bundle头文件: AssetBundle
4) bundle包含的object的详细序列化信息
3.3.2 Bundle文件的加载过程
分析完bundle的组成后,接下来分析从bundle中加载Object的过程,这儿以LZ4的压缩为标准,基于AssetBundle.LoadFromFile(Async)做为接口。 在加载Object的时候,会首先触发加载该object所在的bundle,如果该bundle有依赖bundle,那么需要先加载该bundle的依赖bundle,Unity并不会自动加载依赖bundle.
unity在加载bundle的时候,会先加载该bundle的序列化文件,也就是前面说到的External References/Object map/bundle头文件,然后基于得到序列化信息,进一步从bundle的object序列化信息中加载对应的object。
如果该object有多个依赖的资源,unity会在内部自动从该bundle或者依赖bundle中将依赖资源加载出来,然后执行资源的装载,最终返回一份实例到内存中,完成InstanceID 和 gameobject的映射。
3.3.3 Bundle文件中对script的加载
如果bundle中的object上有对应的script,那么在构建Bundle的时候,会为这个script构建一份特殊的资源:MonoScript,对应的存入到bundle中,monoscript这种资源,并没有包含实际的运行时的代码,而是存储这个脚本的assembly name, namespace name and class name,在装配该Object的时候,由于dll已经提前装载,所以会自动的索引到装载的assembly/namespace/class name脚本,然后装配到该object上。
3.4 资源的卸载
在不考虑资源计数管理的情况下,当资源的引用为空的时候,是可以执行资源的卸载的。对于资源的卸载,分为Resources资源和bundle资源:
3.4.1 Resources资源
1)可以调用Resources.UnloadAsset接口来卸载资源,使用该接口后,下次再加载该Object,会重新建立旧InstanceID和该Object的映射关系
2) 在执行场景切换的时候,如果选择 non-additively mode,这时候会自动的触发Resources.UnloadUnusedAsset
3.4.2 Bundle资源
bundle的卸载,有AssetBundle.Unload(true)/Unload(false)两种:
1)Unload(true): 将bundle从内存中卸载,同时将从bundle中加载的所有资源都卸载掉
2) Unload(false): 将bundle从内存中卸载,从bundle中已经加载的资源会被保留,如果再重新加载该bundle,对应的不会重新构建bundle和资源的映射关系,以前加载的资源就容易造成内存泄露。
目前推荐自己计数管理,只调用Unload(true)
四、Resources的使用
Unity的官方文档:
4.1. Best Practices for the Resources System
Don't use it. several reasons:
1) 使用resources使得精细化细粒度的内存管理更困难
2) 不恰当的使用Resources目录会增大游戏的启动时间以及build时间
而且随着Resources目录中文件的增加,对其中资源的管理会越来越困难
3) Resources目录无法根据平台定制资源内容(除非在build的时候重新拷贝指定的资源到Resource目录)
4) Resources目录无法提供热更新
4.2. Proper uses of the Resources system
在某些情况下,Resources目录还是可以使用:
1) rapidly prototype开发阶段
2) generally required throughout a project's lifetime
3) Not memory-intensive
4) Not prone to patching, or does not vary across platforms or devices
5) Used for minimal bootstrapping 某些和平台无关的配置文件,不占用较大内存,可以放入到Resources目录中
4.3. Serialization of Resources
在build的时候,Resources目录下的Assets/Objects会被序列化到一个序列化文件中。在这个文件中,包含了元数据以及索引信息,类似于AssetBundle。
索引信息其实就是一个序列化的查找树,用来定位资源名字到资源的file guid和local ID, 同时用于查找这个资源本身。
在大部分的平台上,查找树是BST, 其构建的时间复杂度为O(nlog(n)), 构建的时间会随着n的增大而增大(资源数越多构建时间越久)
在游戏启动的时候,这个BST树构建的过程是无法跳过的,如果Resources下的资源数目超过10,000,在低端手机上其占用的启动时间会达到几秒。而事实上这些资源索引并不是全部需要预先加载的,这会降低游戏的性能。
五、总结
简要的阐述了整个资源加载和卸载的流程,对于AssetBundle的使用和管理,属于新的分类内容,在后续再细谈。
本文转自:博客园 - zblade,转载此文目的在于传递更多信息,版权归原作者所有。
https://www.cnblogs.com/zblade/