游戏开发论坛

 找回密码
 立即注册
搜索
查看: 8550|回复: 1

对象池让你Flash项目平稳消耗内存

[复制链接]

1万

主题

1万

帖子

2万

积分

管理员

中级会员

Rank: 9Rank: 9Rank: 9

积分
20468
发表于 2012-8-14 13:29:00 | 显示全部楼层 |阅读模式
作者: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 <= 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()函数的代码你因该很熟悉:它是一个简单的时间循环的基础,在游戏中常用。别忘了导入声明:

    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("Trying to instantiate a Singleton!");
            }
        }
    }
}

  复制代码变量_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("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 = 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 == "IPoolable").length() > 0))
    {
        throw new Error("Can't pool something that doesn't implement IPoolable!");
        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("Can't get an object from a pool that hasn't 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()函数,目的是使它处于可以使用的状态。(即初始化)

  你可能会问,我们在这个函数里为什么不用那个很酷的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 < 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("Can't return an object from a pool that hasn't 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--;
    }
}

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

  接着,我们获取这个对象在对象池中的索引值。若它根本就不在(就是小于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 <= 0)
    {
        parent.removeChild(this);
        ObjectPool.instance.returnObj(this);
    }
}

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

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

下载配套代码

2

主题

9

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2012-9-7 15:26:00 | 显示全部楼层

Re:对象池让你Flash项目平稳消耗内存

看着晕死了。。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-2-27 19:54

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表