Link

首页

Tweet

书单

about me

Command

Category

Egret中对象池设计


这篇文章中所提及的知识点不是什么奇技淫巧,很多人在使用Egret开发HTML5游戏时,都会问道对象池问题。对象池的管理方式很多,这里介绍其中一种策略,也非常简单,不需要什么高深技巧。

结构设计

这里会涉及到一个简单的工厂方法模式和观察者模式。这里的示例代码主要设计到如下设计解构:

上图中我们可以看到,我们设计一个工厂方法,球的生成器 BallGenerator 类,该类的主要职责是负责球类对象的创建,但并不负责对所生成的对象进行管理。

Distributor 的主要职责在于球类对象的存储管理,于此同时,该类还作为球类对象的协议实现。

所有球类对象,全部实现 IBall 接口,该接口负责所有球类对象的必要的接口实现。

IDistributor 接口定义了分配器所有实现的功能接口。为了保证其功能的灵活性,你可以根据你的需求扩展或重新实现你自己的分配器。

上面的设计,主要遵循一条原则:

生成器负责对象生成,分配器负责对象管理,对象生命周期由分配器管理,对外不需要关心对象生命周期。

IBall 接口

我们来看一下 IBall 的具体实现。

enum BallType
{
    foot,    //足球
    basket,  //篮球
    base     //棒球
}

interface IBall
{
    hashc:number; //hashCode
    type:number; //类型标识
    isIdle:boolean; //标记是否空闲
    dispose():void; //释放对象内部引用
    del():void; //彻底释放对象
    reset():void;   //重置
    setProtocol( val:IDistributor ):void; //设置协议
    action():void; //动作
}

我们实现了三个球类实体类,均实现 IBall 接口,同时继承自 egret.HashObject 类。

这里需要提及一个问题,这里实现的对象池中,使用 Object 对象,借助 key-value 的方式访问对象。这里的 key ,使用对象的 hashCode 来实现。但 JavaScript 对象默认并不提供 hashCode ,这里借助egret.HashObject 类来实现。但我们无法重写其 get 方法,所以在 IBall 接口中定义一个名为 hashc 属性来间接访问。

FootBall

源代码如下:

class FootBall extends egret.HashObject implements IBall{
	
	private _isIdle:boolean = true;
	private _type:number = BallType.foot;
	private _dis:IDistributor = null;

	private _message:string = "";

	public constructor() {
		super();
	}

	public reset():void
	{
		this._isIdle = false;
		this._dis.distribution( this );

		this._message = "这是一个足球,踢了一脚。";
	}

	public dispose():void
	{
		this._isIdle = true;
		this._dis.distribution( this );

		//other code
	}

	public del():void
	{
		this.dispose();
		this._dis = null;
	}

	public setProtocol( val:IDistributor ):void
	{
		this._dis = val;
	}

	public get type():number
	{
		return this._type;
	}

	public get hashc():number
	{
		return this.hashCode;
	}

	public get isIdle():boolean
	{
		return this._isIdle;
	}

	public action():void
	{
		console.log( this._message );
	}
}

BasketBall

源代码如下:

class BasketBall extends egret.HashObject implements IBall{
	
	private _isIdle:boolean = true;
	private _type:number = BallType.basket;
	private _dis:IDistributor = null;

	private _message:string = "";

	public constructor() {
		super();
	}

	public reset():void
	{
		this._isIdle = false;
		this._dis.distribution( this );

		this._message = "这是一个篮球,三分线。";
	}

	public dispose():void
	{
		this._isIdle = true;
		this._dis.distribution( this );
		
		//other code
	}

	public del():void
	{
		this.dispose();
		this._dis = null;
	}

	public setProtocol( val:IDistributor ):void
	{
		this._dis = val;
	}

	public get type():number
	{
		return this._type;
	}

	public get hashc():number
	{
		return this.hashCode;
	}

	public get isIdle():boolean
	{
		return this._isIdle;
	}

	public action():void
	{
		console.log( this._message );
	}
}

BaseBall

源代码如下:

class BaseBall extends egret.HashObject implements IBall{
	
	private _isIdle:boolean = true;
	private _type:number = BallType.base;
	private _dis:IDistributor = null;

	private _message:string = "";

	public constructor() {
		super();
	}

	public reset():void
	{
		this._isIdle = false;
		this._dis.distribution( this );

		this._message = "这是一个棒球,本垒打。";
	}

	public dispose():void
	{
		this._isIdle = true;
		this._dis.distribution( this );

		//other code
	}

	public del():void
	{
		this.dispose();
		this._dis = null;
	}

	public setProtocol( val:IDistributor ):void
	{
		this._dis = val;
	}

	public get type():number
	{
		return this._type;
	}

	public get hashc():number
	{
		return this.hashCode;
	}

	public get isIdle():boolean
	{
		return this._isIdle;
	}

	public action():void
	{
		console.log( this._message );
	}
}

生成器

源代码如下:

class BallGenerator 
{

	private _dis:IDistributor = null;

	public constructor( val:IDistributor ) 
	{
		this.init( val );
	}

	private init( val:IDistributor ):void
	{
		this._dis = val;
	}

	public getBall( type:number ):IBall
	{
		let vo:IBall = this._dis.getVO( type );
		if( vo == null )
		{
			// create new Object
			vo = this.createVO( type );
			this._dis.addVO( vo );
			vo.reset();
		}
		return vo;
	}

	private createVO( type:number ):IBall
	{
		switch( type )
		{
			case BallType.foot:
				return new FootBall();
			case BallType.basket:
				return new BasketBall();
			case BallType.base:
				return new BaseBall();
		}
	}
}

生成器只暴露了一个 getBall 方法,每一次通过给定的类型来获取对应的对象。

分配器接口 IDistributor

interface IDistributor
{
    distribution( val:IBall ):void; //分配
    addVO( val:IBall ):void; //添加元素
    getVO( type:number ):IBall; //获取元素
    clear():void; //清除所有未使用的对象
}

分配器实现

源码如下:

class Distributor implements IDistributor
{

	private _UsedPool:Object = null; //使用中的对象
	private _IdlePool:Object = null; //未使用的对象

	public constructor()
	{
		this._IdlePool = {};
		this._UsedPool = {};
	}

	public distribution( val:IBall ):void
	{
		if( val.isIdle )
		{
			this._IdlePool[ val.hashc ] = val;
			delete this._UsedPool[ val.hashc ];
		}
		else
		{
			this._UsedPool[ val.hashc ] = val;
			delete this._IdlePool[ val.hashc ];
		}
	}

	public addVO( val:IBall ):void
	{
		val.setProtocol( this );
		if( val.isIdle )
		{
			this._IdlePool[ val.hashc ] = val;
			
		}
		else
		{
			this._UsedPool[ val.hashc ] = val;
		}
	}

	public getVO( type:number ):IBall
	{
		let obj:IBall = null;
		for (let key in this._IdlePool) {
			obj = this._IdlePool[key] as IBall;
			if ( obj.type == type ) {
				obj.reset();
				return obj;
			}
		}
		return null;
	}

	public clear():void
	{
		let obj:IBall = null;
		for (let key in this._IdlePool) {
			obj = this._IdlePool[key] as IBall;
			obj.del();
		}
		this._IdlePool = null;
		this._IdlePool = {};
	}

}

我们将对象池分为两种,一种为当前在使用状态中的对象,另外一个为空闲状态对象。当用户需要新的对象是,我们在空闲状态的对象池中进行搜索,如果发现类型相匹配的对象,则返回,同时将其移入使用中的对象池。

如果一个对象不再被使用,则将其移动到空闲对象池中。

启动方法

启动代码如下:

class Main extends egret.DisplayObjectContainer
{

    public constructor() {
        super();

        this.init();
    }

    private _gen:BallGenerator = null;
    private init()
    {
        this._gen = new BallGenerator( new Distributor() );

        let fb:FootBall = this._gen.getBall( BallType.foot ) as FootBall;
        fb.action();

        this._gen.look();

        fb.dispose();
        fb = null;

        this._gen.look();
    }
}

为了方便查看效果,我们在 Distributor 分配器中添加如下代码,来打印对象池的内容,以便查看:

public look():void
{
	console.log("[LOOK]");
	console.log("---------- IdlePool 空闲对象 ----------");
	let num = 0;
	for (let key in this._IdlePool) {
		num++;
		console.log( "KEY: " + key + " ,type: "+ (this._IdlePool[key] as IBall).type );
	}
	console.log("共"+num+"个空闲对象");
	num = 0;
	console.log("---------- UsedPool 使用对象 ----------");
	for (let key in this._UsedPool) {
		num++;
		console.log( "KEY: " + key + " ,type: "+ (this._UsedPool[key] as IBall).type );
	}
	console.log("共"+num+"个使用对象");
	console.log("\n\n");
}

运行后,打印如下内容:

这是一个足球,踢了一脚。
[LOOK]
---------- IdlePool 空闲对象 ----------
共0个空闲对象
---------- UsedPool 使用对象 ----------
KEY: 27 ,type: 0
共1个使用对象


[LOOK]
---------- IdlePool 空闲对象 ----------
KEY: 27 ,type: 0
共1个空闲对象
---------- UsedPool 使用对象 ----------
共0个使用对象

可以看到,被创建的 FootBall 对象在两个对象池中被来回移动。

值得注意的是,当我们想销毁一个对象时,你需要调用它的 dispose 方法,然后将你的变量赋值为 nulldispose 方法内部应该将当前对象中的数据进行清除,同时切换自身状态,并通过协议来通知分配器,将自己的对象池分组进行修改。

我们继续添加一下测试代码,来查看一下对象池的复用状态。

private init()
{
    this._gen = new BallGenerator( new Distributor() );

    let fb:FootBall = this._gen.getBall( BallType.foot ) as FootBall;
    fb.action();

    this._gen.look();

    fb.dispose();
    fb = null;

    this._gen.look();

    fb = this._gen.getBall( BallType.foot ) as FootBall;

    this._gen.look();

    let bb:BaseBall = this._gen.getBall( BallType.base ) as BaseBall;
    bb.action();
    let bb2:BaseBall = this._gen.getBall( BallType.base ) as BaseBall;
    bb2.action();

    this._gen.look();

    bb.dispose();
    bb2.dispose();

    this._gen.look();
    
}

打印结果如下:

这是一个足球,踢了一脚。
[LOOK]
---------- IdlePool 空闲对象 ----------
共0个空闲对象
---------- UsedPool 使用对象 ----------
KEY: 27 ,type: 0
共1个使用对象



[LOOK]
---------- IdlePool 空闲对象 ----------
KEY: 27 ,type: 0
共1个空闲对象
---------- UsedPool 使用对象 ----------
共0个使用对象



[LOOK]
---------- IdlePool 空闲对象 ----------
共0个空闲对象
---------- UsedPool 使用对象 ----------
KEY: 27 ,type: 0
共1个使用对象



这是一个棒球,本垒打。
这是一个棒球,本垒打。
[LOOK]
---------- IdlePool 空闲对象 ----------
共0个空闲对象
---------- UsedPool 使用对象 ----------
KEY: 27 ,type: 0
KEY: 28 ,type: 2
KEY: 29 ,type: 2
共3个使用对象



[LOOK]
---------- IdlePool 空闲对象 ----------
KEY: 28 ,type: 2
KEY: 29 ,type: 2
共2个空闲对象
---------- UsedPool 使用对象 ----------
KEY: 27 ,type: 0
共1个使用对象

总结

以上就是一个简单的对象池实现,我们将对象内存持久化的权限交与分配器处理,对外并不负责每个对象的声明周期管理。同时,分配器提供了一个 clear 操作,在你的内存需要清除的时候,可以将当前空闲状态的对象清除掉。

enjoy!