Unity资源的加载释放最佳策略探讨

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

Unity的资源陷阱

游戏资源的加载和释放导致的内存泄漏问题一直是Unity游戏开发的一个黑洞。加载处理不佳,容易在游戏运行中导致问题,而释放不及时会再游戏运行一段时间以后出现问题,造成游戏拖慢,卡顿甚至闪退,成为了Unity游戏的一个常见症状。

究其根源,一方面是因游戏设备尤其是Unity擅长的移动设备运行内存非常有限,释放稍有不及就给系统带来极大的负担,另外一方面是因为Unity不太清晰的加载释放策略和谜一样的GC(垃圾收集)机制,共同赋予了Unity “内存杀手”“低效引擎”的恶名,但事实上如果能够深入的了解Unity的资源加载释放机制,亦步亦趋的根据自身情况管理好内存的使用,那么Unity游戏完全可以跳出内存泄漏的陷阱。

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

资源的加载方式

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

静态加载

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

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

优点:可以在场景加载过程中完成自身的加载过程,所以在场景运行期间不会因为加载该资源形成卡顿;另外在场景切换时会被完全释放,无须担心因为释放不及时不完整而导致内测泄漏问题。

缺点:只支持不变的静态资源,无法根据游戏的实际需要灵活更换不同资源;所有资源必须和场景同生共死,无法在场景运行过程中提前释放,如果该资源非常庞大并且只在短时间内需要,则会带来不小的内存浪费。

动态加载

    动态加载一般发生在场景的运行期间,游戏为了一定的需求动态的加载和表现不同的资源而产生的需求:如果游戏根据不同的玩家显示不同的头像,根据玩家选择的不同角色而显示不同的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资源被释放会造成资源丢失而损坏对象。

复制原资源内存空间的同时,还引用了原资源的数据,也就是说不但占用额外的内存,而且一旦原资源被释放,也会造成数据丢失而损坏对象。

同Mesh,复制并引用原资源。

同Shader,完全引用原资源。

要点2:从AssetBundle加载到GameObject实例化,大部分资源实际占用3处内存,那么最终我们要释放这3处内存才算将该资源完全释放,分别为:销毁GameObject对象,释放Asset,释放AssetBundle。

要点3:要特别注意和理解引用关系,这个在后面的资源释放章节中具有重大意义。

要点4:所以对于动态加载的资源,仅仅通过场景的关闭来销毁所有场景对象的操作,并不能释放所有的内存资源。

资源加载释放最佳策略

Resources资源加载

Resources加载是将游戏内部一部分以文件形式存储的资源加载出来供游戏使用,Resources加载的步骤一般有二步(下面是示例代码):

    Object  cubePreb= Resources.Load< GameObject >(cubePath);

    GameObject cube =Instantiate(cubePreb) asGameObject;

首先通过Resources.Load函数把对象资源(cubePreb)加载到内存镜像。

其次通过Instantiate实例化该资源的内存镜像变成游戏中可用的对象(cube),当然如果是Shader/Mesh/Material/Texture类型资源无须再次实例化,可以直接使用。由此可见Resources加载的资源一般占用2处内存空间:所用资源cubePreb的内存镜像和实例化对象cube的内存镜像。

 

这里顺便提下Resources资源加载的一个“黑科技”:OnDemand方式。以上述代码为例,cubePreb的所需资源在Resources.Load的时候不会加载,而将在第一次Instantiate的时候一起加载,也常常会导致一些比较大的对象在第一次实例化时造成卡顿现象,不过这个性能问题不在本文的探讨范畴。

Resources最佳加载策略:

]相同对象的Resources.Load只需调用一次,该资源对象可以共享,反复调用虽然不会引起内存镜像的重复建立,但依然存在性能损耗。

一般只对GameObject进行Instantiate实例化操作,尽量避免对Shader 、Mesh、Material、Texture资源进行实例化从而造成内存浪费。

除了明确需要全局共享的资源,尽量避免使用全局静态变量来引用Resources.Load出的资源对象,因为全局引用的对象会被GC忽略造成释放陷阱。


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来快速释放当前空闲资源其实是一招险棋,有欲速则不达的可能:

首先,如果大部分资源都存在引用,那么使用该函数徒劳无功。

其次,如果该资源在UnloadUnusedAssets以后又被起用,那么资源重新加载的损耗得不偿失。

最后,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.("MyObject");

var obj = Instantiate(prefab);


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


AssetBundle最佳加载策略:

相同内容的AssetBundle只Load一次,在其Unload之前反复加载会造成不必要的浪费和风险。

相同名称的资源用LoadAsset也只需加载一次,这个和Resources.Load基本类似,当然反复加载LoadAsset不会产生重复内存占用。

AssetBundle资源释放

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

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

    加载的资源prefab:因为是内存镜像,对象可以用Object.Destroy释放,Sprite等资源可以用Reources.UnloadAsset释放。但Texture类的资源就比较麻烦,只能通过Resources.UnloadUnusedAssets方法才能比较有效的释放,但条件比较苛刻,prefab的父(bundle)和子(obj)都要已经被释放的情况下,加上本身引用清空,然后使用UnloadUnusedAssets才有效。

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

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

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

 

AssetBundle最佳释放策略:

实例化的对象使用Destroy这个不加累述了。

已经加载的资源prefab,如果消耗巨大而且明确不再使用,可以直接使用Object.Destroy释放,部分资源如Sprite用Destory无法释放,可以用Reources.UnloadAsset释放。一些底层资源比如Texture如果一定要在游戏进行中释放,需要在合适的环节中采用 Resources.UnloadUnusedAssets。

如果AssetBundle能够一次性加载完成所需资源的,可以使用AssetBundle.Unload(false)将AssetBundle的内存立刻释放,然后再场景切换以后通过Resources.UnloadUnusedAssets方法释放所有加载的资源,这种方案的缺陷是不能在AssetBundle.Unload以后再次使用该AssetBundle。

如果在场景运行过程中需要不断从AssetBundle加载资源,在这种情况下无须提前做任何释放行为,可以在场景切换以后,最终调用AssetBundle.Unload(true) 将全部资源包和资源释放。这种方式的主要缺陷是,AssetBundle占用的资源会在整个场景过程中一直存在,造成内存浪费,但如果AssetBundle体积不大,这种方式也带来了一定的灵活性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容