注:本文中用到的大部分术语和函数都是Unity中比较基本的概念,所以本文只是直接引用,不再详细解释各种概念的具体内容,若要深入了解,请查阅相关资料。
Unity的资源陷阱
游戏资源的加载和释放导致的内存泄漏问题一直是Unity游戏开发的一个黑洞。因此导致游戏拖慢,卡顿甚至闪退问题成为了Unity游戏的一个常见症状。
究其根源,一方面是因游戏设备尤其是Unity擅长的移动设备运行内存非常有限,另外一方面是因为Unity不太清晰的加载释放策略和谜一样的GC(垃圾收集)机制,共同赋予了Unity “内存杀手”“低效引擎”的恶名,但事实上如果能够深入的了解Unity的资源加载释放机制,亦步亦趋的根据自身情况管理好内存的使用,那么Unity游戏完全可以跳出内存泄漏的陷阱。
那么下面,我们从资源的加载方式,资源的相关概念,加载释放的最佳策略三个方面来逐步探讨这个Unity的“危险领域”。
资源的加载方式Unity的资源加载方式分两大种类:静态加载和动态加载。
静态加载顾名思义,直接通过设置属性的办法,把资源直接绑定在场景内的任意对象上,如2D对象的Sprite属性和3D对象的Materials属性;另外通过自定义代码上的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中的游戏对象不能简单的用引用和复制来进行区分,大部分的对象不同部分采用了不同模式甚至混合模式,使得游戏对象的内存分配显得错综复杂。
资源加载时对内存的使用