A闪的 BLOG 技术与人文
程序开发时,必须考虑到内存消耗,否则,它可能拖慢程序运行,占用系统大量内存或是直接引发崩溃。本教程旨在帮你避免一些潜在问题。
我们来看看最终的效果
在舞台上随意点击,会生成烟花特效,留意舞台左上方的的内存指示器。
步骤 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 <= 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 < numChildren; i++)
{
if (getChildAt(i) is Particle)
{
Particle(getChildAt(i)).update(_elapsed);
}
}
}
private function createParticles(e:MouseEvent):void
{
for (var i:int = 0; i < 10; i++)
{
addChild(new Particle(stage.mouseX, stage.mouseY));
}
}
粒子update()函数的代码你因该很熟悉:它是一个简单的时间循环的基础,在游戏中常用。别忘了导入声明:你现在可以测试这个程序,使用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,它会被用到的!首先,我们想一下如何安全有效的使对象实现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()函数的运行。简单起见,对象池将设计成单例模式。这样我们写代码随时随地都能用得它。新建一个类“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("Trying to instantiate a Singleton!");
}
}
}
}
变量_allowInstantiation是这个单例类的核心:它是private的,所以只有类本身才能修改,它唯一需要修改的时候,也就是在第一个实例被创建之前。既然我们已经有方法来标识每个对象池,现在就用代码来实现吧。我们的对象池应该足够灵活,同时支持静态和动态类型(我们在步骤3提到的概念,还记得吧?)。我们还需要存储每个对象池的容量和内部活动对象(就是已经被程序使用)的数量。一个好方法是建立一个私有类,用它存储所有这些信息,并将对象池保存到一个Object里。
package { public class ObjectPool { private static var _instance:ObjectPool; private static var _allowInstantiation:Boolean;代码看起来有些微妙,但先别害怕,这就来解释。第一个if语句看起来很晦涩。你可能以前从未见识过这些函数,下面罗列出它们的功能: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("Trying to instantiate a Singleton!"); } _pools = {}; } }
}
class PoolInfo { public var items:Vector.<IPoolable>; 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.<IPoolable>(size, !isDynamic); this.size = size; this.isDynamic = isDynamic; active = 0; initialize(); } private function initialize():void { for (var i:int = 0; i < size; i++) { items[i] = new itemClass(); } }
}
上面的代码创建了一个私有类,它可以保存一个对象池所有信息。我们还创建了一个对象_pools来持有对所有对象池的引用。下面,我们将在这个类中,创建一个函数来注册对象池: ***代码 public function registerPool(objectClass:Class, size:uint = 1, isDynamic:Boolean = true):void { if (!(describeType(objectClass).factory.implementsInterface.(@type == “IPoolable”).length() > 0)) { throw new Error(“Cant pool something that doesnt implement IPoolable!”); return; }var qualifiedName:String = getQualifiedClassName(objectClass); if (!_pools[qualifiedName]) { _pools[qualifiedName] = new PoolInfo(objectClass, size, isDynamic); }
}
下面是getObj()函数:
public function getObj(objectClass:Class):IPoolable
{
var qualifiedName:String = getQualifiedClassName(objectClass);
if (!_pools[qualifiedName])
{
throw new Error("Cant get an object from a pool that hasnt been registered!");
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()函数,目的是使它处于可以使用的状态。(即初始化)我们现在修改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 tempParticle:Particle;
for (var i:int = 0; i < 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("Cant return an object from a pool that hasnt been registered!");
return;
}
var objIndex:int = PoolInfo(_pools[qualifiedName]).items.indexOf(obj);
if (objIndex >= 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--;
}
}
我们来梳理一下这个函数:首先检验传递进来的对象是否有相应的对象池。想必你很熟悉代码了——唯一的不同点,就是此处我们使用一个实例来获得类的限定名,前面用的是类,但这不影响输出。现在我们已经创建了返还对象的代码,我们可以使粒子在“将死之时”自动的将自己返还到对象池。在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 <= 0)
{
parent.removeChild(this);
ObjectPool.instance.returnObj(this);
}
}
注意到我们增加了一个函数调用ObjectPool.instance.returnObj()。这就是为何它能自动将自己返还给对象池。我们现在可以测试程序了。
看到没,即使点击产生上百的粒子,内存依然相当稳定!