【翻译】在Flash中使用对象池技术稳定内存使用(上)
难度: 初学者
平台:Flash/AIR
语言:AS3
软件需求:FlashDevelop
预计完成时间:30分钟
内存的占用是你在做项目的时候需要特别注意的一点,不然它会拖慢整个应用,占用很多内存,更糟糕的是使项目崩溃。
本文就是教你如何避免这样的悲剧发生。
最终结果预览
点击舞台上的任意地方创建一个烟花效果,看内存使用分析工具,如右上角。
第一步 介绍
你是否曾经有使用性能测试工具来查看你的应用的内存占用情况?你会发现,你的内存时跌时涨。如果没有的话,证明你的程序太牛逼了。好的,尽管这些大量内存使用造成的三角看起来还蛮漂亮的,但是对于你的应用和你的用户来说,这可不是什么好消息。继续读下面的文章了解为什么会出现这样的情况以及如何避免其发生。
第二步 好的和坏的使用
下面这幅图是一个典型的内存管理不当的例子。这是来自某个游戏的原型。你明显可以发现两点:内存的使用有巨大的峰值和底值。峰值最高接近540M,这意味着游戏原型单独占用了玩家电脑RAM的540M -- 这种事情是绝对不能容许发生的。
这种问题是从你的项目中创建了一大堆对象开始的,无用的对象在垃圾回收之前会一直占用你的内存。当他们被垃圾回收释放的时候,就出现了这么大的一个跌落缺口,更糟糕的是当你的对象无法被释放的时候,你的应用的内存占用会持续攀升直到你的应用崩溃或者停止为止。如果你想知道你的应用是如何崩溃的,你可以看看这篇文章【英文版】【中文版】
在本文中我们不会涉及垃圾回收的知识。我们将要学习简历一个架构,用来控制内存的使用,使他们绝对的稳定,由此可以使GC无须清理内存,这样让应用运行得更快。查看以下内存使用情况,它与上面的来自同一原型。但是这个是使用本文的知识优化过的。
以上的改善可以通过对象池(Object Pooling)来实现。继续读下去了解它是什么以及它是如何运作的。
第三步 池子的类型
对象池是一种在应用初始化的时候预先创建一些对象然后在应用有生之年一直将它保存在内存当中的一种技术。
对象池在应用申请对象的时候给应用返回对象,并在应用用完对象之后将对象重置回初始状态。市面上有很多种对象池类型,但是我们只讲两种:静态池和动态池。
静态池创建一定数量的对象并且在应用程序的整个生命周期当中只持有这些对象。如果向对象池申请对象但是它早已将全部对象给出,池子将返回null。使用这样的池子需要判别返回是对象是否是空值。
动态池也同样在初始化的时候创建一定数量的实例,但是当申请的时候对象池为空,对象池将自动创建另一个实例返回之,然后增加池子的容量并纳入这个新对象。
在本文中,我们将创建一个简单的应用,在用户点击舞台的时候生成粒子。这些粒子将被赋予无穷的生命,然后从屏幕上移除掉返回池内。
我们得创建一个没有对象池的应用并检测他的内存的使用情况,然后我们创建一个由对象池的应用对比他们之间的内存使用情况。
第四步 初始化应用
打开FlashDevelop(查看此引导)创建一个as项目。我们将用一个简单的彩色小方块代替粒子。这个小方块将通过代码来创建,并且会以一个随机的角度移动。创建一个新类,命名为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()方法将会在每帧调用,并且接收帧与帧之间的毫秒数,这样它可以减去粒子的生命周期。当这个值小于0的时候,粒子将会自动向父容器请求移除。余下的代码负责粒子的移动。
现在我们将在每次监听到点击的时候创建一堆粒子,移步到Main.as:
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)); } }
更新粒子的代码再熟悉不过了:他是一个简单的基于时间的循环,在游戏中使用很普遍。别忘记导入:
import flash.events.Event; import flash.events.MouseEvent; import flash.utils.getTimer;
现在你可以测试你的应用。使用FD内置的profiler检测。在屏幕上连续多次点击。你看到的结果跟下面的图片应该很像:
我一直点直到执行垃圾回收,应用创建了2000多个粒子。这看起来是不是和那个原型的内存使用很像?当然像了。并且这样绝对是不行的。为了让内存使用的观察更简单,我们将在第一步里面提到的工具添加到项目,以下是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,因为导入之后才能用。