作者:NULL

  程序开发时,必须考虑到内存消耗,否则,它可能拖慢程序运行,占用系统大量内存或是直接引发崩溃。本教程旨在帮你避免一些潜在问题。

  我们来看看最终的效果,在舞台上随意点击,会生成烟花特效,留意舞台左上方的的内存指示器。

Flash: http://dev.gameres.com/Program/Other/Application.swf

步骤 1:简介

  如果你曾用过任何工具、代码或类库来获取程序当前的内存消耗以评估测试,你一定多次注意到:内存占用时高时低(当然,假若从未有过,说明你代码太强了!)。虽然这些峰看起来挺酷,但对于你的程序不是什么好事儿,最终受害者是用户。

步骤2:内存使用的优劣之分

  下面的图例是糟糕内存管理的很好例子。它来自一个游戏原型。你需要注意到两点:内存使用的大量尖刺和最大峰值。最大时几乎达到540Mb!这就是说此时这个游戏原型在用户PC的RAM里生生独吞下了540Mb——这显然要避免。

  问题是这样引发的:你在程序中创建了大量对象实例。在下一次垃圾回收运行之前,弃置的对象会一直呆在内存里,当它们被回收——内存占用解除,于是形成了巨大的峰。或者更坏的是,这些对象不满足回收条件,程序消耗内存持续增长,直到崩溃。如果你想了解后一种问题,参阅垃圾回收小贴士


  本教程不讨论垃圾回收机制。我们将建立一种结构,有效的管理内存中的对象,使它稳定利用并防止垃圾回收机制将其回收,以此来加快程序运行。看看还是那个游戏,在优化之后的性能表现

  这些都可以通过对象池技术来实现。接下来看看如何实现的吧?

步骤 3:对象池类型

  可以这样理解对象池技术:在程序初始化时,实例化预设数量的对象,并贮存在内存里直到程序结束。当程序索取对象时,它会给出,当程序不再需要某个对象,它就会将其重置为初始状态。对象池类型很多,我们今天今天只看两种:静态和动态对象池。

  静态对象池实例化预设数量的对象,并且在程序的整个生命周期都只保存这么多的对象。如果程序索取对象,但对象池已给出所有对象,它就返回一个null。使用这种对象池,一定要记住处理返回值为null的情形。

  动态对象池在初始化时,也是实例化预设数量的对象。但是,当程序索取对象而池子已“空”的时候,它会自动实例化一个对象,增大池子的容量然后将这个对象添加进池子。

  本教程中,我们将创建一个简单程序,但用户点击舞台,它会生成一些粒子。这些例子寿命有限,它们会被移出屏幕并回收到池子。为了实现效果,我们先做一个不使用对象池技术的demo,看看它的内存使用情况。然后再做一个采用此技术的,加以比较。

步骤 4:初始程序

  打开FlashDevelop新建一个AS3工程。我们将使用一个小的彩色方块儿来充当粒子,使用代码绘制并向随机方向移动。新建一个类Particle,继承自Sprite。我想你能够独立完成这个例子类,因此只贴出记录粒子寿命并移出屏幕这部分代码。如果你有确实无法独立完成粒子类,文章开头有整个源文件的下载。

private var _lifeTime:int;

public function update(timePassed:uint):void
{
// Making the particle move
x += Math.cos(_angle) * _speed * timePassed / 1000;
y += Math.sin(_angle) * _speed * timePassed / 1000;

// Small easing to make movement look pretty
_speed -= 120 * timePassed / 1000;

// Taking care of lifetime and removal
_lifeTime -= timePassed;

if (_lifeTime lt;= 0)
{
parent.removeChild(this);
}
}

  上面的代码负责将粒子移出屏幕。变量_lifeTime用来控制粒子在屏幕上存在的毫秒数。我们在构造函数中将其初始化为1000。update()函数将按帧频触发,他接受两帧之间的时间差值作为参数,并递减粒子的寿命值。

private var _oldTime:uint;
private var _elapsed:uint;

private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
stage.addEventListener(MouseEvent.CLICK, createParticles);
addEventListener(Event.ENTER_FRAME, updateParticles);

_oldTime = getTimer();
}

private function updateParticles(e:Event):void
{
_elapsed = getTimer() - _oldTime;
_oldTime += _elapsed;

for (var i:int = 0; i lt; numChildren; i++)
{
if (getChildAt(i) is Particle)
{
  Particle(getChildAt(i)).update(_elapsed);
}
}
}

private function createParticles(e:MouseEvent):void
{
for (var i:int = 0; i lt; 10; i++)
{
addChild(new Particle(stage.mouseX, stage.mouseY));
}
}

  复制代码粒子update()函数的代码你因该很熟悉:它是一个简单的时间循环的基础,在游戏中常用。别忘了导入声明:

import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.getTimer;

  你现在可以测试这个程序,使用FlashDevelop内置的分析器。在屏幕上点击多次。下面是内存消耗的显示图:

  我拼命地点,直到垃圾回收机制运行。程序产生了2000多个符合回收条件的粒子。看起来像不像刚才那个游戏原型?很像,显然不能这样子做程序。为了更简便的测试内存消耗,我们将添加在步骤1提到的一个功能类。下面是Main.as:

private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
stage.addEventListener(MouseEvent.CLICK, createParticles);
addEventListener(Event.ENTER_FRAME, updateParticles);

addChild(new Stats());

_oldTime = getTimer();
}

  复制代码别忘了导入net.hires.debug.Stats,它会被用到的!

步骤 5:定义一个可以poolable(可以用对象池管理的)对象。

  步骤4中的程序看起来非常简单,它产生了简单的粒子特效,但在内存方面表现糟糕。接下来,我们将开始使用对象池来解决这一问题。

  首先,我们想一下如何安全有效的使对象实现pooled(被对象池管理)。在一个对象池中,我们必须确保它给出的对象符合使用标准,回收的对象完全独立(也就是不再被部分引用)。为了使每一个对象池中的对象都能满足以上条件,我们创建一个接口。这个接口约定两个函数renew()和destroy()。这样,我们在调用对象的这些方法时,就不必担心它们是否具备了。这也意味着,任何想进入对象池管理的对象都必须实现这个接口。下面是接口代码:

package
{
public interface IPoolable
{
function get destroyed():Boolean;

function renew():void;
function destroy():void;
}
}

  复制代码显然我们的粒子需要由对象池管理,因此要让它们实现Ipoolable接口。我们把构造函数中的代码都搬到renew()函数中,而且通过对象的destroy()函数解除了所有外部引用。代码如下

/* INTERFACE IPoolable */

public function get destroyed():Boolean
{
return _destroyed;
}

public function renew():void
{
if (!_destroyed)
{
return;
}

_destroyed = false;

graphics.beginFill(uint(Math.random() * 0xFFFFFF), 0.5 + (Math.random() * 0.5));
graphics.drawRect( -1.5, -1.5, 3, 3);
graphics.endFill();

_angle = Math.random() * Math.PI * 2;
_speed = 150; // Pixels per second
_lifeTime = 1000; // Miliseconds
}

public function destroy():void
{
if (_destroyed)
{
return;
}

_destroyed = true;

graphics.clear();
}

  复制代码构造函数并不接受参数。如果你想给对象传递什么信息,需要通过它的函数来完成。鉴于renew()内部的运行方式,我们需要在构造函数里将_destroed变量设置为true,以保证renew()函数的运行。

  这样我们的Particle类就实现了Ipoolable约定的功能,对象池也就能创建一池子的粒子了。

步骤 6:开始构建对象池

  简单起见,对象池将设计成单例模式。这样我们写代码随时随地都能用得它。新建一个类“ObjectPool”,写入下面的代码,构建一个单例模式:

package
{
public class ObjectPool
{
private static var _instance:ObjectPool;
private static var _allowInstantiation:Boolean;

public static function get instance():ObjectPool
{
if (!_instance)
{
_allowInstantiation = true;
_instance = new ObjectPool();
_allowInstantiation = false;
}

return _instance;
}

public function ObjectPool()
{
if (!_allowInstantiation)
{
throw new Error(quot;Trying to instantiate a Singleton!quot;);
}
}
}
}

  复制代码变量_allowInstantiation是这个单例类的核心:它是private的,所以只有类本身才能修改,它唯一需要修改的时候,也就是在第一个实例被创建之前。

  接下来思考在这个类内部该如何保存多个对象池。因为它将是全局的(也就是要做到将程序内任何一个合法对象进行对象池管理),我们必须给每个对象池取一个唯一的名字。怎样实现?方法很多,但目前我所想到的最好方法,就是使用对象自己的类名称。这样我们就会有“Particle”池,“Enemy”池等等...但这样存在一个问题。我们知道类名称只在包内具有唯一性,这即是说,在“enemies”包和“structures”包内可以同时存在一个“BaseObject”类,如果同时对他们实例化,对象池的管理就存在问题。

  使用类名称作为对象池的标示符仍具有可行性,不过这就得求助于flash.utils.getQualifiedClassName()了。这个函数能够生成类的全称,含包路径在内。这样,用每个对象的类名称作为对象池标示符就没问题了。我们将在下一步来实现。


步骤 7:创建对象池

  既然我们已经有方法来标识每个对象池,现在就用代码来实现吧。我们的对象池应该足够灵活,同时支持静态和动态类型(我们在步骤3提到的概念,还记得吧?)。我们还需要存储每个对象池的容量和内部活动对象(就是已经被程序使用)的数量。一个好方法是建立一个私有类,用它存储所有这些信息,并将对象池保存到一个Object里。

package
{
public class ObjectPool
{
private static var _instance:ObjectPool;
private static var _allowInstantiation:Boolean;

private var _pools:Object;

public static function get instance():ObjectPool
{
if (!_instance)
{
_allowInstantiation = true;
_instance = new ObjectPool();
_allowInstantiation = false;
}

return _instance;
}

public function ObjectPool()
{
if (!_allowInstantiation)
{
throw new Error(quot;Trying to instantiate a Singleton!quot;);
}

_pools = {};
}
}
}

class PoolInfo
{
public var items:Vector.lt;IPoolablegt;;
public var itemClass:Class;
public var size:uint;
public var active:uint;
public var isDynamic:Boolean;

public function PoolInfo(itemClass:Class, size:uint, isDynamic:Boolean = true)
{
this.itemClass = itemClass;
items = new Vector.lt;IPoolablegt;(size, !isDynamic);
this.size = size;
this.isDynamic = isDynamic;
active = 0;

initialize();
}

private function initialize():void
{
for (var i:int = 0; i lt; size; i++)
{
items = new itemClass();
}
}
}

The code above creates the private class which will contain all the information about a pool. We also created the _pools object to hold all object pools. Below we will create the function that registers a pool in the class:

  上面的代码创建了一个私有类,它可以保存一个对象池所有信息。我们还创建了一个对象_pools来持有对所有对象池的引用。下面,我们将在这个类中,创建一个函数来注册对象池:

***代码

public function registerPool(objectClass:Class, size:uint = 1, isDynamic:Boolean = true):void
{
if (!(describeType(objectClass).factory.implementsInterface.(@type == quot;IPoolablequot;).length() gt; 0))
{
throw new Error(quot;Cant pool something that doesnt implement IPoolable!quot;);
return;
}

var qualifiedName:String = getQualifiedClassName(objectClass);

if (!_pools[qualifiedName])
{
_pools[qualifiedName] = new PoolInfo(objectClass, size, isDynamic);
}
}

  复制代码代码看起来有些微妙,但先别害怕,这就来解释。第一个if语句看起来很晦涩。你可能以前从未见识过这些函数,下面罗列出它们的功能:

  我们传递一个对象给describeType()函数,它就能够生成一个XML用来描述这个对象的所有信息。

  若对应一个类,那么它的所有信息被包含在factory标签里。

  在这个标签里,XML又将所有的接口信息描述在一个implementsInterface标签里。

  我们检测一下Ipoolable接口是否在里面。如果找到了,我们就可以把这个类加入对象池,因为我们可以将它转化为IObect。

  接下来的代码,若该对象池不存在,那就在_pools中新建一个。随之,PoolInfo类的构造函数会调用内部的initialize()函数,从而创建一个由我们预设大小的对象池。接下来就要使用它了!

步骤 8:获取一个对象

  上一步我们创建了一个能够注册对象池的函数,但现在为了使用它,我们需要从中取得对象。这很简单:如果池子未“空”我们就返回一个对象。如果池子已“空”,我们就看看它是不是动态类型;如果是,那就增加它的容量,创建一个对象并返回。若不是,那就返回null。(你也可以选择抛错,但最好让代码在这种情况下保持继续运行)

  下面是getObj()函数:

public function getObj(objectClass:Class):IPoolable
{
var qualifiedName:String = getQualifiedClassName(objectClass);

if (!_pools[qualifiedName])
{
throw new Error(quot;Cant get an object from a pool that hasnt been registered!quot;);
return;
}

var returnObj:IPoolable;

if (PoolInfo(_pools[qualifiedName]).active == PoolInfo(_pools[qualifiedName]).size)
{
if (PoolInfo(_pools[qualifiedName]).isDynamic)
{
returnObj = new objectClass();

PoolInfo(_pools[qualifiedName]).size++;
PoolInfo(_pools[qualifiedName]).items.push(returnObj);
}
else
{
return null;
}
}
else
{
returnObj = PoolInfo(_pools[qualifiedName]).items[PoolInfo(_pools[qualifiedName]).active];

returnObj.renew();
}

PoolInfo(_pools[qualifiedName]).active++;

return returnObj;
}

  复制代码在这个函数中,我们首先检验对象池是否存在。若存在,我们接着检验对象池是否为“空”:若已经“空”了,但类型是动态类,我们就创建一个对象并把它加入对象池。若它不是动态类型,代码返回null。如果pool尚有未被使用的对象,我们就取出最开头儿的那个未被使用的对象,调用它的renew()函数。这一点很重要:我们对一个已经存在于池子的对象调用renew()函数,目的是使它处于可以使用的状态。(即初始化)

  你可能会问,我们在这个函数里为什么不用那个很酷的describeType()呢?原因很简单:describeType函数在每次被调用的时候都会生成一个XML,所以,我们尽量不要创建那些极耗内存且我们无法控制的对象。而且,仅仅检验对象池是否存在已经足够:如果这个类压根儿就没有实现IPooable接口,那它就不可能拥有自己的对象池。如果它不用有自己的对象池,函数的第一个if语句就足以将其捕获。

  我们现在修改Main类并使用对象池啦!代码如下:

private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
stage.addEventListener(MouseEvent.CLICK, createParticles);
addEventListener(Event.ENTER_FRAME, updateParticles);

_oldTime = getTimer();

ObjectPool.instance.registerPool(Particle, 200, true);
}

private function createParticles(e:MouseEvent):void
{
var tempParticlearticle;

for (var i:int = 0; i lt; 10; i++)
{
tempParticle = ObjectPool.instance.getObj(Particle) as Particle;
tempParticle.x = e.stageX;
tempParticle.y = e.stageY;

addChild(tempParticle);
}

}

  复制代码点击编译并测试内存消耗。这是我得到的结果:

  相当的酷,是吧?

步骤 9:向对象池返还对象

  我们已经成功实现了一个可以获取对象的对象池。但还不算完。我们现在只是从池子里取对象,但还没有在废置后把它们放回去。接着在ObjectPool.as类里加入一个返还对象的函数:

public function returnObj(obj:IPoolable):void
{
var qualifiedName:String = getQualifiedClassName(obj);

if (!_pools[qualifiedName])
{
throw new Error(quot;Cant return an object from a pool that hasnt been registered!quot;);
return;
}

var objIndex:int = PoolInfo(_pools[qualifiedName]).items.indexOf(obj);

if (objIndex gt;= 0)
{
if (!PoolInfo(_pools[qualifiedName]).isDynamic)
{
PoolInfo(_pools[qualifiedName]).items.fixed = false;
}

PoolInfo(_pools[qualifiedName]).items.splice(objIndex, 1);

obj.destroy();

PoolInfo(_pools[qualifiedName]).items.push(obj);

if (!PoolInfo(_pools[qualifiedName]).isDynamic)
{
PoolInfo(_pools[qualifiedName]).items.fixed = true;
}

PoolInfo(_pools[qualifiedName]).active--;
}
}

  复制代码我们来梳理一下这个函数:首先检验传递进来的对象是否有相应的对象池。想必你很熟悉代码了——唯一的不同点,就是此处我们使用一个实例来获得类的限定名,前面用的是类,但这不影响输出。

  接着,我们获取这个对象在对象池中的索引值。若它根本就不在(就是小于0)对象池内,我们就忽略它。一旦确定它在对象池内,我们就把它从当前位置删除并重新插入到最后面。为什么呢?因为我们计算被程序使用的对象数量时,是从对象池的最前端往后数的,我们需要将对象池从新整理,以使得所有被返还并闲置的对象处于对象池尾部。这就是我们在这个函数实现的功能。

  对于静态类型的对象池,由于我们创建的Vector对象是固定长度的。因此无法使用splice()和push()方法。变通方案就是,暂时改变它的fixed属性为false,移出对象并从末尾重新插入,然后再将fixed属性改回true。我们还需要将活动对象的数量递增1.这样之后,就完成了返还对象的工作。

  现在我们已经创建了返还对象的代码,我们可以使粒子在“将死之时”自动的将自己返还到对象池。在Particle.as内部这样写:

public function update(timePassed:uint):void
{
// Making the particle move
x += Math.cos(_angle) * _speed * timePassed / 1000;
y += Math.sin(_angle) * _speed * timePassed / 1000;

// Small easing to make movement look pretty
_speed -= 120 * timePassed / 1000;

// Taking care of lifetime and removal
_lifeTime -= timePassed;

if (_lifeTime lt;= 0)
{
parent.removeChild(this);
ObjectPool.instance.returnObj(this);
}
}

  复制代码注意到我们增加了一个函数调用ObjectPool.instance.returnObj()。这就是为何它能自动将自己返还给对象池。我们现在可以测试程序了。

  看到没,即使点击产生上百的粒子,内存依然相当稳定!

下载配套代码


锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛