注:搞网页游戏两年,两耳不闻窗外事(只是偷偷上微博),居然没了整理的习惯。是时候好好整理和总结了。
Flash大型项目尤其是多人同时在线的webgame,性能优化是一个很重要的部分。性能优化大概就是两个部分:内存优化、CPU优化。所谓CPU优化,说白了就是降低CPU使用率。我们首先要解决的一个问题是:让代码不要做当前不需要做的事。
最近我们的项目(MMOARPG)加东西比较多,从Windows资源管理器看CPU,即使站在场景中什么都不干,CPU使用率竟然还在20%+。所以描述一下问题就是:
问题描述:找出让CPU空转的Function。
于是去查那些帧循环里的方法,看看是不是耗时太长。一般耗时2ms以上的,我们都会想办法优化。
场景中有NPC和怪物,我们要查一下他们的定时器:
EnterFrameManager.getInstance(ANIMAL_TIMER).add(frameScript);
private function frameScript():void { var start:int = getTimer(); // something may cost time // code var cost = getTimer - start; trace("[$ms] frameScript : Animal".replace("$", cost)); }
结果偶尔打出来一些:
[0ms] frameScript : Animal [1ms] frameScript : Animal
看上去不是这方面的问题。过了几天,在厕所里抽着烟,猛然悔悟了一下,立刻回去查一下EnterFrameManager。顺带说一下,当我们有很多帧循环或者Timer事件要注册,通常写一个FrameManager或TimerManager来做。在Manager里统一响应Event.ENTER_FRAME,处理绑定的所有事件。EnterFrameManager这部分简化后的代码如下:
public function add(script:Function):void { var o:Object = {"func": script}; // you may want to add some other attributes scripts[script] = o; } private function enterFrameHandler(e:Event):void { for each (var i:Object in scripts) { i.func(); } }
我们给超过30ms的enterFrameHandler加上执行时间,看看一帧那么宝贵的时间里,耗时的部分在哪。
private function enterFrameHandler(e:Event):void { var s:int = flash.utils.getTimer(); for each (var i:Object in scripts) { i.func(); } var cost:int = flash.utils.getTimer() - s; if (cost > 30) { trace("[$ms] enterFrameHandler".replace("$", cost)); } }
果然发现一些超过30ms的帧循环:
[32ms] enterFrameHandler
然后我们加一些代码看看这些帧循环里都搞了哪些破事儿:
public function add(script:Function):void { var o:Object = {"func": script}; // you may want to add some other attributes o.name = getFunctionName(script); scripts[script] = o; } private function enterFrameHandler(e:Event):void { var s:int = flash.utils.getTimer(); for each (var i:Object in scripts) { var ss:int = flash.utils.getTimer(); i.func(); i.cost = flash.utils.getTimer() - ss; } var cost:int = flash.utils.getTimer() - s; if (cost > 30) { trace("[$ms] enterFrameHandler".replace("$", cost)); for each (var i:Object in scripts) { trace("[$ms] ".replace("$", i.cost) + i.name); } } }
输出的结果让人吃惊,32ms的enterFrameHandler里,居然有大量0ms的方法,超过40个:
[32ms] enterFrameHandler [0ms] frameScript [0ms] frameScript [0ms] frameScript [1ms] frameScript [0ms] frameScript [0ms] frameScript ....省略 [0ms] frameScript
这是什么意思呢?就是说,很多执行了0ms的方法,加起来的时间超过了30ms。经查,有很多NPC和场景特效,虽然出了视野,但是帧循环没有移除。当主角看不到这些NPC和场景特效的时候,这些循环还在跑。虽然执行时间很短(不超过1ms),但是加起来不容忽视。0ms方法加起来不是0ms!一帧的时间也就40来毫秒(24fps),这些空循环偶尔就占了30ms,难怪帧频会降低,难怪会占CPU,失误啊。
解决办法:果断在NPC和场景特效出视野的时候,移除帧循环frameScript,问题解决。
结论
在大型项目中,不要忽视了帧循环里的看上去不那么耗时间的函数,他们的执行时间可能只有0ms,但这只是因为我们测不准小于1ms的时间。即使你说,我这些方法里什么都没干,他们也可能是因为函数调用栈深度问题而消耗了时间。当有很多这样的方法时,它们众志成城,团结的力量就会给CPU带来压力了。我们应该去掉那些没用的帧循环,需要的时候再加上。
延伸
这次比较幸运,出视野之后帧循环还可以清掉。但如果很多玩意儿都在视野里呢?CPU是省不来的。想让玩家获得最好的体验,我们应该做的是让CPU保持稳定,也就是让帧频保持稳定。我认为,换句话说,在一个帧循环里不要干太多事儿。这个怎样在策略上优化呢,回头再总结这方面经验。
付上网上找的获得方法名称的函数:
private function getFunctionName(fun:Function):String{ try{ var k:Sprite = Sprite(fun); }catch(err:Error){ var fn:String = err.message.replace(/.+::(\w+\/\w+)\(\)\}\@.+/,"$1"); return fn==err.message?(err.message.replace(/.+ (function\-\d+) .+/i,"$1")):fn; } return null; }