下面通过一个实例来说明资源加载会使用多少内存,比如一个普通的3D对象,包括了Shader/Mesh/Material/Texture等资源,这些资源需要从AssetBundle加载,如果要将其实例化到场景,那么将会占用如下图所示的内存空间:
首先,从文件、网络或者其他内存空间加载AssetBundle以后,会形成AssetBundle内存镜像(上图紫色部分)。
其次,从AssetBundle内存镜像中再加载GameObject以后,该GameObject用到的Shader/Mesh/Material/Texture也同时被加载出来,形成各自不同的内存镜像(注意:请参考上图紫色虚线框中的内容,可知这些资源内存镜像与AssetBundle内存镜像是不同的)
最后Instantiate实例化GameObject以后,GameObject会再一次复制GameObject资源的内存镜像到一个新的内存区域,形成全新的对象数据。(上图上方绿色框中内容)
资源的加载需要理解以下要点
要点1:尽管GameObject是对原有资源内存镜像的完全复制,但由于Unity对各种资源种类的处理方式不同,导致GameObject中的其他相关资源并不是简单的复制关系:
Shader:完全的引用,不占用额外内存,如果原Shader资源被释放会造成资源丢失而损坏对象。
Mesh:复制原资源内存空间的同时,还引用了原资源的数据,也就是说不但占用额外的内存,而且一旦原资源被释放,也会造成数据丢失而损坏对象。
Material:同Mesh,复制并引用原资源。
Texture:通Shader,完全引用原资源。
要点2:从AssetBundle加载到GameObject实例化,大部分资源实际占用3处内存,那么最终我们要释放这3处内存才算将该资源完全释放。
要点3:要特别注意和理解引用关系,这个在后面的资源释放章节中具有重大意义。
资源加载释放最佳策略 Resources资源加载
Resources加载是将游戏内部一部分以文件形式存储的资源加载出来供游戏使用,Resources加载的步骤一般有二步(下面是示例代码):
Object cubePreb = Resources.Load< GameObject >(cubePath);
GameObject cube = Instantiate(cubePreb) as GameObject;
首先通过Resources.Load函数把对象资源(cubePreb)加载到内存镜像。
其次通过Instantiate实例化该资源的内存镜像变成游戏中可用的对象(cube),当然如果是Shader/Mesh/Material/Texture类型资源无须再次实例化,可以直接使用。由此可见Resources加载的资源一般占用2处内存空间:所用资源cubePreb的内存镜像和实例化对象cube的内存镜像。
这里顺便提下Resources资源加载的一个“黑科技”:OnDemand方式。以上述代码为例,cubePreb的所需资源在Resources.Load的时候不会加载,而将在第一次Instantiate的时候一起加载,也常常会导致一些比较大的对象在第一次实例化时造成卡顿现象,不过这个性能问题和内测泄漏无关,不在本文的探讨范畴。
Resources最佳加载策略:
相同对象的Resources.Load只需调用一次,该资源对象可以共享,反复调用虽然不会引起内存镜像的重复建立,但依然存在性能损耗。
一般只对GameObject进行实例化操作,尽量避免对Shader 、Mesh、Material、Texture资源进行实例化从而造成内存浪费。
除了明确需要全局共享的资源,尽量避免使用全局静态变量来引用Resources.Load出的资源对象,因为全局引用的对象存在释放陷阱。
Resource 资源释放
单体释放Reources.UnloadAsset(Object)
主动卸载独立资源,主要作用在于及时释放场景的中的资源,减低运行时的内存损耗,提高游戏性能;但这种方式也带来了不小的风险,由于Unity游戏的资源引用关系错综复杂,如果要单独释放一个资源,要明确该资源已经在场景中不再被引用,否则轻者造成游戏显示错误,重则造成游戏报错。
另外,Reources.UnloadAsset(Object)还有一些暗坑,比如释放Sprite需要先释放Sprite.Texture否则Texture就会存留在内存,所以在使用这个函数的时候,要清楚释放的对象有无内部引用资源。
统一释放Resources.UnloadUnusedAssets
这是一个统一的,一次性的,比较完整的释放闲置资源的函数,而且是Unity官方非常推荐的一种方式,但这个函数实际的使用效果并没有想象的那么美好,该函数本身就是Unity资源释放的一个陷阱。