一个反面例子:
让我们来看一看这个包含两个类的简单的应用程序,首先,是Midlet...
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class OptimizeMe extends MIDlet implements CommandListener { private static final boolean debug = false; private Display display; private OCanvas oCanvas; private Form form; private StringItem timeItem = new StringItem( "Time: ", "Unknown" ); private StringItem resultItem = new StringItem( "Result: ", "No results" ); private Command cmdStart = new Command( "Start", Command.SCREEN, 1 ); private Command cmdExit = new Command( "Exit", Command.EXIT, 2 ); public boolean running = true; public OptimizeMe() { display = Display.getDisplay(this); form = new Form( "Optimize" ); form.append( timeItem ); form.append( resultItem ); form.addCommand( cmdStart ); form.addCommand( cmdExit ); form.setCommandListener( this ); oCanvas = new OCanvas( this ); } public void startApp() throws MIDletStateChangeException { running = true; display.setCurrent( form ); } public void pauseApp() { running = false; } public void exitCanvas(int status) { debug( "exitCanvas - status = " + status ); switch (status) { case OCanvas.USER_EXIT: timeItem.setText( "Aborted" ); resultItem.setText( "Unknown" ); break; case OCanvas.EXIT_DONE: timeItem.setText( oCanvas.elapsed+"ms" ); resultItem.setText( String.valueOf( oCanvas.result ) ); break; } display.setCurrent( form ); } public void destroyApp(boolean unconditional) throws MIDletStateChangeException { oCanvas = null; display.setCurrent ( null ); display = null; } public void commandAction(Command c, Displayable d) { if ( c == cmdExit ) { oCanvas = null; display.setCurrent ( null ); display = null; notifyDestroyed(); } else { running = true; display.setCurrent( oCanvas ); oCanvas.start(); } } public static final void debug( String s ) { if (debug) System.out.println( s ); } } Second, the OCanvas class that does most of the work in this example... import javax.microedition.midlet.*; import javax.microedition.lcdui.*; import java.util.Random; public class OCanvas extends Canvas implements Runnable { public static final int USER_EXIT = 1; public static final int EXIT_DONE = 2; public static final int LOOP_COUNT = 100; public static final int DRAW_COUNT = 16; public static final int NUMBER_COUNT = 64; public static final int DIVISOR_COUNT = 8; public static final int WAIT_TIME = 50; public static final int COLOR_BG = 0x00FFFFFF; public static final int COLOR_FG = 0x00000000; public long elapsed = 0l; public int exitStatus; public int result; private Thread animationThread; private OptimizeMe midlet; private boolean finished; private long started; private long frameStarted; private long frameTime; private int[] numbers; private int loopCounter; private Random random = new Random( System.currentTimeMillis() ); public OCanvas( OptimizeMe _o ) { midlet = _o; numbers = new int[ NUMBER_COUNT ]; for ( int i = 0 ; i < numbers.length ; i++ ) { numbers[i] = i+1; } } public synchronized void start() { started = frameStarted = System.currentTimeMillis(); loopCounter = result = 0; finished = false; exitStatus = EXIT_DONE; animationThread = new Thread( this ); animationThread.start(); } public void run() { Thread currentThread = Thread.currentThread(); try { while ( animationThread == currentThread && midlet.running && !finished ) { frameTime = System.currentTimeMillis() - frameStarted; frameStarted = System.currentTimeMillis(); result += work( numbers ); repaint(); synchronized(this) { wait( WAIT_TIME ); } loopCounter++; finished = ( loopCounter > LOOP_COUNT ); } } catch ( InterruptedException ie ) { OptimizeMe.debug( "interrupted" ); } elapsed = System.currentTimeMillis() - started; midlet.exitCanvas( exitStatus ); } public void paint(Graphics g) { g.setColor( COLOR_BG ); g.fillRect( 0, 0, getWidth(), getHeight() ); g.setColor( COLOR_FG ); g.setFont( Font.getFont( Font.FACE_PROPORTIONAL, Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ) ); for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) { g.drawString( frameTime + " ms per frame", getRandom( getWidth() ), getRandom( getHeight() ), Graphics.TOP | Graphics.HCENTER ); } } private int divisor; private int r; public synchronized int work( int[] n ) { r = 0; for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) { for ( int i = 0 ; i < n.length ; i++ ) { divisor = getDivisor(j); r += workMore( n, i, divisor ); } } return r; } private int a; public synchronized int getDivisor( int n ) { if ( n == 0 ) return 1; a = 1; for ( int i = 0 ; i < n ; i++ ) { a *= 2; } return a; } public synchronized int workMore( int[] n, int _i, int _d ) { return n[_i] * n[_i] / _d + n[_i]; } public void keyReleased(int keyCode) { if ( System.currentTimeMillis() - started > 1000l ) { exitStatus = USER_EXIT; midlet.running = false; } } private int getRandom( int bound ) { // return a random, positive integer less than bound return Math.abs( random.nextInt() % bound ); } }
这个程序是一个模拟一个简单游戏循环的MIDlet:
*work 执行
*draw 绘制
*poll for user input 等待用户输入
*repeat 重复
对于快速游戏,这个循环一定要尽可能的紧凑和快速。我们的循环持续一个有限的次数(LOOP_COUNT=100),并且用系统timer来计算整个作业花费了多少毫秒,我们就可以测量并改善它的性能。时间和执行的结果会显示在一个简单的窗口上。用Start命令来开启测试。按任意键会提前退出循环,退出按钮用来结束程序。 在大多数游戏里面,主游戏循环中的作业会更新整个游戏状态-----移动所有的角色,检测并处理冲突,更新分数,等等。在这个例子里面,我们并没有做什么特别有用的事。程序仅仅是在一个数组之间做一些算数运算,然后把这些结果加起来。 run()函数计算了每次循环所花费的时间。每一帧,OCanvas.paint()方法都在屏幕上的16个随机的地方显示这个数。一般的,你可以用这个方法在你的游戏里面画出你的图像元素,我们的代码在该过程中作了一些有用的摹写。
不管这些代码看起来有多么的无意义,它给了我们足够的机会去优化它的性能。
******************第二页
哪里去优化 -- 90/10规则
在苛求性能的游戏里面,有90%的时间是在执行其中%10的代码。我们的优化努力就应该针对这10%的代码。我们用一个Profier来定位这 10%. 要运行J2ME无线开发包中的profier工具,选择edit菜单下的preferences选项. 这将会显示preferences窗口.选择Monitoring这一栏,将"Enable Profiling"悬赏,然后点ok按钮。什么也没有出现。这是对的,在Profier窗口显示之前,我们需要
在模拟器中运行我们的程序然后退出。现在就做.图1显示了如何打开Profiler工具。
我的模拟器(运行在Windows XP下,Inter P4 2.4GHz的CPU)报告我100次这个循环用了6,407毫秒,或者说6又1/2秒。这个程序报告说62或者63毫秒每帧。在硬件(一个 motorola的i85s)上运行会慢得多。 一帧的时间大约是500毫秒,整个循环用了52460毫秒。
在本文这一课中,我们将试着改善这个数据。
当你退出这个程序时,profiler窗口就会出现,然后你会看见一个文件夹浏览器中有一些东西,在左边的面板上会有一个熟悉的树形部件。方法间的联系会在这个结构列表中显示。每一个文件夹是一个方法,打开一个文件夹会显示它所调用过的方法。在该树中选择一
个方法会显示那个方法的profiling信息并在右边的面板显示所有被它调用过的方法。注意在每一个元素旁边显示了一个百分数。这就是该方法在整个执行过程中所占的执行时间的百分比。我们必须翻遍这棵树,来寻找时间都到哪里去了,并对占用百分比最高的方法进行优化,如果可能的话。
图2 -- Profiler程序调用的图
对这个profiler,有几点需要说明。首先你的百分比多半会和我的不一样,但是他们的比例会比较相似--总是在最大的数之后。我的数据在被次运行的时候都会改变。为了保持情况一致,你可能希望关掉所有的后台程序,像Email客户端,并在你测试的时候保持你正
在进行的任务最少。还有,不要在用profiler之前混淆(obfuscate)你的代码,不然你的方法会被神秘的标示为b或者a或者ff。最后profiler不会因为你运行模拟器的设备的差别而改变,它和硬件是完全独立的。
打开最高百分比的那个文件夹,我们看到有66.8%的时间在执行一个被称为 "com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run"的方法,这个对我们并没有什么帮助。用类似的方法,再往下寻找更深层次的方法,持续下去,你就会找到一个大的百分比停留在serviceRepaints()上,最后到了我们的 OCanvas.paint()方法.另
外有30%的时间在OCanvas.run()方法里.这两个方法都在我们的主程序循环中,这并不奇怪.我们不会在我们的MIDlet类中花任何时间做优化,同样地我们不会对游戏的主循环外的任何代码做优化.
在我们的例子程序中的百分比的划分在真实的游戏中并不是完全的没有特性. 你多半会在一个真实的视觉游戏中发现这个大的执行时
间的比例是在paint()方法中. 相比于非图形化程序,图形化程序总是要花很长的时间. 不幸的是,我们的图形程序已经被写在了J2ME API这一层下,对于改善它们的性能,我们没有多少可以做的.我们可以做的是在用哪个和如何用它们之间做出聪明的决定.