比起其他语言,Flash ActionScript3的上手过程要简单许多,对于很多人我想大多都是被这一点吸引进了咱们的圈子,很快就能看到很cool的效果,很好玩。不过实现一个效果容易,想对一个不论是简单还是复杂的应用做到运行时一直保持高效率地运转就是一个比较考验Flash开发人员的事情了。
“为什么我的应用越运行越卡?”这个问题有非常多的原因啦,我们一个个来看,对于效率优化也是一个很长的话题,这就是为什么我的标题里要写上一个一,以后想到一点加一点吧,希望对列位爱卿有所帮助,come on,let`s go! Enjoy!
●避免创建过多实例
这个问题是很基础的一点,一般新手比较容易犯,有一些老手在不注意的时候也容易出现实例过多造成的内存溢出情况。我们一起来看一个相当简单的例子,我们想要在点击舞台时产生一个随机颜色的小球,但是舞台上一次只能存在一个小球,第二次点击舞台时,第一次点击产生的小球会消失。来看看下面这种写法:
我们需要的球类:
- public class Ball extends Shape
- {
- private var _radius:Number;
-
- public function Ball( radius:Number = 5, color:uint = 0xff0000 )
- {
- _radius = radius;
- var g:Graphics = this.graphics;
- g.clear();
- g.beginFill( color );
- g.drawCircle(0, 0, _radius);
- g.endFill();
- }
- }
复制代码
接下来开始测试:
- public class OptimizeYourFlashApp extends Sprite
- {
- private var ball:Ball;
-
- public function OptimizeYourFlashApp()
- {
- stage.addEventListener(MouseEvent.CLICK, onClick);
- }
-
- private function onClick( e:MouseEvent ):void
- {
-
- if( this.numChildren > 0 )
- {
- for(var i:int=0; i<this.numChildren; i++)
- {
- this.removeChildAt(i);
- i--;
- }
- }
- ball = new Ball(30, Math.random() * 0xffffff);
- addChild( ball );
- ball.x = mouseX;
- ball.y = mouseY;
- }
- }
复制代码
很一般的做法对不对?好,让我们用Flash Builder的profile来看一下内存测试结果:
<ignore_js_op>
我们看到Ball类的实例数instances有两百多个,这是当然的,因为每次点击后都会new一个新的Ball实例出来,这直接导致的结果就像我们上图所示,占有了47000多的内存,占全应用内存使用量的99%以上,当前内存使用量(current memory)高达65K……如此简单的一个应用就能在我点击两百次鼠标后达到65K的内存占用。
虽然把没有用的实例置为空(如 ball = null )可以消除实例释放内存,但是最好的办法还是优化你的算法!
事实上我们可以改用别的方式来避免创建太多的实例,因为我们要实现的功能是让场景里只存在一个球,而且每次点击的颜色不同,那么我们仅需每次点击后改变原先的球的颜色即可。于是我们为Ball类增加一个改变颜色的接口:
- class Ball extends Shape
- {
- private var _radius:Number;
-
- public function Ball( radius:Number = 5, color:uint = 0xff0000 )
- {
- _radius = radius;
- this.color = color;
- }
-
- public function set color( value:uint ):void
- {
- var g:Graphics = this.graphics;
- g.clear();
- g.beginFill( value );
- g.drawCircle(0, 0, _radius);
- g.endFill();
- }
- }
复制代码
这样就不必每次都new一个实例了。在文档类里面应用起来:
- public class OptimizeYourFlashApp extends Sprite
- {
- private var ball:Ball;
-
- public function OptimizeYourFlashApp()
- {
- stage.addEventListener(MouseEvent.CLICK, onClick);
-
- }
-
- private function onClick( e:MouseEvent ):void
- {
-
- if( this.numChildren > 0 )
- {
- for(var i:int=0; i<this.numChildren; i++)
- {
- this.removeChildAt(i);
- i--;
- }
- }
- if(ball == null)
- {
- ball = new Ball(30, Math.random() * 0xffffff);
- }
- else
- {
- ball.color = Math.random() * 0xffffff;
- }
- addChild( ball );
- ball.x = mouseX;
- ball.y = mouseY;
- }
- }
复制代码
最后我们在profile里看看内存使用情况:
<ignore_js_op>
你懂的!
对于一些老鸟来说也会出现莫名其妙的内存溢出问题,有时候这种“偷袭”会让我们不知所措,此时可能只有Flash Builder的profile能告诉我们是哪个狗东西搞得鬼。我们敬爱的导师MoonSpirit在使用Tweenlite时就有过内存溢出的经验,Tweenlite是一个Flash动画补间引擎,在之前的案例里面我有提到过,它最最常用的方法是TweenLite.to()方法,我们一般都是直接使用这个静态方法,而忽略了该方法会返回一个TweenLite的实例,因此我们每次调用TweenLite.to()方法时都会新声明一个新的TweenLite实例,待实例一多就会出现内存溢出的危险,因此一个好的习惯是每次用TweenLite.to()方法后都及时把它生成的实例给干掉,我们可以使用TweenLite实例它自带的kill方法来做到:
- var tween:TweenLite = TweenLite.to(ball, 1,
- {x:mouseX, y:mouseY, onComplete: function(){ tween.kill() } });
复制代码
这样就可以在每一次补间动画播放完毕后让to方法新生成的TweenLite实例停止播放并标记为“回收”状态,在下一次Flash Player垃圾回收阶段会回收这些无用的实例,释放内存。
●及时清理事件侦听
这也是个老生常谈的问题,很多前辈们都已经多次提到,我这里只简言带过:一个实例中若是存在事件侦听,那么该实例及时用不到它它也会占用内存,不能被垃圾回收。因此我们必须及时地移除事件侦听,保持内存不会堆积废物。被广泛使用的一种设计模式是重载addEventListener方法使每次添加事件侦听时都能把它保存到一个数组中,在该实例要下岗之时把数组中保存的事件侦听一一移除,这样就干净了……
- private var listeners:Array = new Array();
-
- override public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void
- {
- super.addEventListener(type, listener, useCapture, priority, useWeakReference);
- var newListener:Object = {lType:type, lListener:listener};
- listeners.push( newListener );
- }
-
- //调用此方法以清除全部已添加了的事件侦听器
- public function clear():void
- {
- for each( var listener:Object in listeners)
- {
- this.removeEventListener( listener.lType, listener.lListener );
- }
- listeners = [];
- }
复制代码
有的时候,我们希望一个实例中的事件侦听器长期侦听事件,但是这个事件又触发得不是很频繁(通常是一些自定义的事件),那么我们希望在事件一段时间内一直没有触发过的情况下不让这个事件侦听器妨碍垃圾回收,让FP能顺利释放该实例所在内存。这时我们可以使用事件侦听器的“弱引用”来做到在不移除事件侦听器的情况下顺利实现实例的垃圾回收。将一个事件侦听器设置为“弱引用”的方式及其简单,只需设置addEventListener的第5个参数“useWeakReference”为true即可。如下:
- addEventListener(MouseEvent.CLICK, onClick, false, 0, true);
复制代码
我们保持第3,4个参数为默认值,设置第五个参数为true即可设置该事件侦听为弱引用,若一个实例中存在若引用的事件侦听器那么该实例在闲置时依然会被顺利地垃圾回收并释放内存,但是事件触发时依然能够准确地侦听到事件。为事件侦听设置弱引用能够极大地缓解Flash应用的“疲劳”,它在大型Flash应用的开发中已被广泛使用。
另外,由于事件侦听器会影响实例的垃圾回收,所以建议尽可能少地添加侦听器。在addEventListener方法中存在多个参数,我们刚才提到的“弱引用设置”是最后一个参数,那么还有两个参数是比较陌生的,这里谈谈第三个参数useCapture(使用捕获),我们知道Flash事件机制中每一个事件的触发都是从捕获阶段开始的,这个阶段是从最外层容器向内部子容器遍历以搜索事件发生者。那么我们如果在一个父容器中设置useCapture=true,那么该容器中所有子容器在发生事件时父容器都能侦听到,这样就可以避免为每个子容器添加事件侦听器:
- public function Test()
- {
- for(var i:int=0; i<100; i++)
- {
- var child:CustomClass = new CustomClass ()//某个自定义类
- addChild( child );
- }
- this.addEventListener( MouseEvent.CLICK, onClick, true)
- }
- private function onClick( event:Event ):void
- {
- var c:CustomClass = event.target as CustomClass;
- trace( c );
- }
复制代码
另外,对于带多个动画的Flash应用,建议不要为每个动画元件中添加ENTER_FRAME或者TIMER事件侦听,而是建立一个总的时间管理者,在这个管理者中添加ENTER_FRAME或者TIMER事件侦听并在事件处理函数中对所有动画元件进行统一调度。
●避免过多运算
AS语言的计算效率不必C语言等底层语言,我们在计算时应尽可能避免过多过频繁地计算。比如下列一个循环:
- for( var i:int=0; i< array.length; i++ )//arrary是一个长度很长的数组
- {
- //do something
- }
复制代码
对于这个循环,每次循环开始时都会计算array的长度,如果array长度比较短还好,长度很长,循环次数很多时就会做很多冗余的计算,好的习惯是事先计算好数组的长度再开始循环:
- var len:int = array.length;
- for( var i:int=0; i< len; i++ )//arrary是一个长度很长的数组
- {
- //do something
- }
复制代码
这里注意一下数组长度会改变的情况,需要动态地更改长度才行:
- var len:int = array.length;
- for( var i:int=0; i< len; i++ )//arrary是一个长度很长的数组
- {
- arrary.pop();
- len--;
- }
复制代码
在《动画高级教程》中的第一章:高级碰撞检测中也提到过类似问题,在做大量物体的碰撞检测时若是在每一帧都对每个对象进行遍历,让一个对象与其他对象依次进行碰撞检测的话会进行大量的运算,对于N个对象,每帧将会进行(N^2-N)/2次测试,100个对象在每一帧都会进行4950次碰撞检测,这将会拖垮CPU,因此我们必须采用一些好的算法来避免大量运算。