Unity开发实战探讨-资源的加载释放最佳策略

注:本文中用到的大部分术语和函数都是Unity中比较基本的概念,因此本文只是直接引用,再也不详细解释各类概念的具体内容,若要深刻了解,请查阅相关资料。网络

 

Unity的资源陷阱

游戏资源的加载和释放致使的内存泄漏问题一直是Unity游戏开发的一个黑洞。所以致使游戏拖慢,卡顿甚至闪退问题成为了Unity游戏的一个常见症状。异步

究其根源,一方面是因游戏设备尤为是Unity擅长的移动设备运行内存很是有限,另一方面是由于Unity不太清晰的加载释放策略和谜同样的GC(垃圾收集)机制,共同赋予了Unity “内存杀手”“低效引擎”的恶名,但事实上若是可以深刻的了解Unity的资源加载释放机制,亦步亦趋的根据自身状况管理好内存的使用,那么Unity游戏彻底能够跳出内存泄漏的陷阱。函数

那么下面,咱们从资源的加载方式,资源的相关概念,加载释放的最佳策略三个方面来逐步探讨这个Unity的“危险领域”。性能

资源的加载方式

    Unity的资源加载方式分两大种类:静态加载和动态加载。设计

静态加载

    顾名思义,直接经过设置属性的办法,把资源直接绑定在场景内的任意对象上,如2D对象的Sprite属性和3D对象的Materials属性;另外经过自定义代码上的Public属性绑定的任何资源也属于静态加载范畴。对象

    静态加载是最为常见的资源加载方式,其资源的生命周期与其所在的场景彻底一致,在场景加载时加载,在场景切换时释放,因此这种方式的优缺点也是显而易见的:blog

优势:能够在场景加载过程当中完成自身的加载过程,因此在场景运行期间该资源没有任何性能隐患;另外在场景切换时会被彻底释放,无须担忧由于释放不及时不完整而致使内测泄漏问题。生命周期

缺点:只支持不变的静态资源,没法根据游戏的实际须要灵活更换不一样资源;全部资源必须和场景同生共死,没法在场景运行过程当中提早释放,若是该资源很是庞大而且只在短期内须要,则会带来不小的内存浪费。游戏

动态加载

    动态加载通常发生在场景的运行期间,游戏为了必定的需求动态的加载和表现不一样的资源而产生的需求:若是游戏根据不一样的玩家显示不一样的头像,根据玩家选择的不一样角色而显示不一样的3D模型。动态加载的优缺点是很是极端的:内存

优势:根据游戏设计要求,有些资源在场景开始时没法肯定,必须动态加载;动态资源能够在场景运行的任什么时候间加载,也能够在任什么时候间释放,开发者具备很强的灵活性和主动性。

缺点:很明显,动态资源的控制须要开发者亲力亲为和更高的技巧;而一旦缺少对其合理的控制,内存陷阱将会遍地开花,游戏的性能问题和内存泄漏将没法避免。

动态加载的常见方式

Resources 本地资源加载:经过引擎内部的Resources类,对项目中全部Resources目录下的资源进行动态加载。

AssetBundle本地或者远程资源包加载:经过引擎内部的AssetBundle类,对网络,内存和本地文件中的AssetBundle资源包进行加载。而后从资源包中获取资源,在游戏中使用。

Instantiate实例化游戏对象:经过Resources或AssetBundle中的加载的对象,通常不能直接在场景中使用,须要经过Instantiate方法,实例化这些对象,使其成为场景中可用的游戏对象。

AssetDatabase加载资源:经过AssetDatabase的相关函数加载资源,因为仅适用于Editor环境,在这里不加累述。

基本资源加载概念

资源的类型

Unity中常见的资源包括如下几种:

GameObject(游戏对象)

Shader(着色器)

Mesh(网格)

Material(材质)

Texture/Sprite(贴图/精灵)

资源内存镜像的引用和复制

要理解Unity资源的使用,必须先了解如下几个概念:

内存镜像:任何游戏资源或对象一旦加载,都会占用设备的一部份内存区域,这个内存区域就是资源或对象的内存镜像,若是内存镜像过多达到设备的极限,游戏必然会发生性能问题。

引用和复制:Unity的“黑科技”之一, 也是资源加载和释放的主要难点。

引用:指对原资源仅仅是引用关系,再也不从新复制一分内存镜像,但引用的关键在于,若是原资源被删除会致使引用关系损坏,使得引用的对象发生资源丢失。

复制:复制原资源的内存镜像,从而产生两个不一样的内存区域,若是被复制的资源被释放,不会影响复制的资源。

但不幸的是,Unity中的游戏对象不能简单的用引用和复制来进行区分,大部分的对象不一样部分采用了不一样模式甚至混合模式,使得游戏对象的内存分配显得错综复杂。

 

资源加载时对内存的使用

下面经过一个实例来讲明资源加载会使用多少内存,好比一个普通的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资源释放的一个陷阱。

首先UnloadUnusedAssets对全部须要释放资源有一个很是重要的前置条件:只有不存在任何引用关系的资源才能被该函数释放,看起来这是一个明确的要求,但因为Unity资源的相互引用关系比较隐晦繁复,想要明确的判断某一个资源不存在引用关系是有必定难度的,而且,若是一个咱们想释放的资源存在任何隐性的引用关系,UnloadUnusedAssets将会无视这个资源而无任何反馈,这种状况经常会被开发人员忽略而形成内存的泄漏。

通常状况下,要明确一个资源再也不被引用,首先要把全部用到该资源使用GameObject.Destroy函数进行销毁,而后要把全部引用到该资源的变量显性的设置为Null,尤为要关注的是类成员和静态变量的引用,最后调用UnloadUnusedAssets才能有效地释放这个资源。

根据实战经验来看,最佳使用UnloadUnusedAssets的时机仍是在场景切换的时候,因为Unity的场景关闭会有效地销毁全部的对象和全部代码的引用,那么在场景切换,尤为是在新场景的开头UnloadUnusedAssets上一个场景的资源处理是比较稳妥的作法;而在场景运行过程当中但愿不断调用UnloadUnusedAssets来快速释放当前空闲资源实际上是一招险棋,有欲速则不达的可能:

  1. 首先,若是大部分资源都存在引用,那么使用该函数徒劳无功。
  2. 其次,若是该资源在UnloadUnusedAssets之后又被起用,那么资源从新加载的损耗得不偿失。
  3. 最后,UnloadUnusedAssets是一个异步函数,在其执行过程当中,一旦资源又被使用将会致使没法预知的后果。实际开发中发如今场景运行中反复调用UnloadUnusedAssets存在闪退的风险。

 

Resources最佳释放策略:

  • 实例化的对象,在再也不使用之后必须马上Destroy,该清理操做不会引发资源的丢失,风险较小,要充分知足。
  • 对于内存消耗很是巨大,而且在场景运行过程当中可以明确再也不使用的资源内存镜像,能够主动使用Reources.UnloadAsset进行强制释放。对于消耗不大的,等场景结束后进行统一释放是更稳妥的选择。
  • 大部分资源建议在场景切换之后,经过Resources.UnloadUnusedAssets方法进行后置释放,必要时再加上GC.Collect。(在下一个场景的开始甚至在一个独立的换场场景中调用都是比较稳妥的选择)
  • 全局静态变量和类成员变量引用的资源,务必先把引用设为Null值,而后再调用Reources.UnloadUnusedAssets才能正确释放。

 

AssetBundle资源加载

    AssetBundle是Unity提供的另外一种资源加载方式,开发者能够把一批资源打包,而后经过网络下载或者文件加载的方式进行加载。

    介于Resources方式的资源必须一块儿打入游戏包体,AssetBundle方式则提供了一种更为灵活的资源加载方式,AssetBundle无需进入游戏包体,大大减小了游戏文件的体积,另外,AssetBundle容许经过网络下载,也为游戏资源的获取和升级提供了更为灵活的选择。

AssetBundle加载资源通常分3步,(下面是示例代码):

 

var bundle= AssetBundle.LoadFromFile(path);

var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");

var obj = Instantiate(prefab);

   

    根据前面提到的资源的内存使用和以上示例代码所示,能够得知AssetBundle资源加载到最终加入游戏场景,须要存在3个对象:bundle自己,加载的资源prefab,和实例化出来的obj。这3个对象分别对应不一样的内存镜像,在释放的时候须要分别考虑。

 

AssetBundle最佳加载策略:

  • 相同内容的AssetBundle只Load一次,在其Unload以前反复加载会形成没必要要的浪费和风险。
  • 相同名称的资源用LoadAsset也只需加载一次,这个和Resources.Load基本相似。

AssetBundle资源释放

    根据AssetBundle的3级对象,咱们分别说下各自的释放办法:

    实例化的obj:用GameObject.Destroy释放。

    加载的资源prefab:由于是内存镜像,也能够用Object.Destroy释放。另外Resources.UnloadUnusedAssets方法对这种资源释放也是有效的,但条件比较苛刻,prefab的父(bundle)和子(obj)都要已经被释放的状况下,加上自己引用清空,而后使用UnloadUnusedAssets才有效,因此这种办法并不十分推荐。

    加载的资源包bundle:AssetBundle.Unload方法是惟一的释放手段。这个方法有2个参数,都有必定的意义:

    参数为false的时候,仅仅把资源包内存释放,但保留任何已经加载的资源和实例化对象,这些资源和对象的释放有待后续代码完成。

    参数为true的时候,是一次比较完全的内存释放,资源包和全部被加载出的资源都会被释放,固然实例化的obj不会被释放,但引用关系会被破坏,因此在使用这种方式前必须提早销毁全部实例化对象。

 

AssetBundle最佳释放策略:

  • 实例化的对象使用Destroy这个不加累述了。
  • 已经加载的资源prefab,若是消耗巨大并且明确再也不使用,能够直接使用Object.Destroy释放。
  • 若是AssetBundle可以一次性加载完成所需资源的,可使用AssetBundle.Unload(false)将AssetBundle的内存马上释放,而后再场景切换之后经过Resources.UnloadUnusedAssets方法释放全部加载的资源,这种方案的缺陷是不能在AssetBundle.Unload之后再次使用该AssetBundle。
  • 若是在场景运行过程当中须要不断从AssetBundle加载资源,在这种状况下无须提早作任何释放行为,能够在场景切换之后,最终调用AssetBundle.Unload(true) 将所有资源包和资源释放。这种方式的主要缺陷是,AssetBundle占用的资源会在整个场景过程当中一直存在,形成内存浪费,但若是AssetBundle体积不大,这种方式也带来了必定的灵活性。
相关文章
相关标签/搜索