更多名人名博

上市公司专栏

实时股价每分更新

纳斯达克(美元)(市值:亿美元)
综指: 涨跌幅:
公司 股价 涨幅度 市值
盛大
网易
九城
畅游
完美
巨人
新浪
百度
恒生指数(港币)(市值:亿港币)
综指: 涨跌幅:
公司 股价 涨幅度 市值
腾讯
金山
网龙
创业板
综指: 涨跌幅:
公司 股价 涨幅度 市值
宝网
d
您现在的位置:首页> 开发|手游技术|手机游戏| > J2ME游戏优化秘密2

J2ME游戏优化秘密2

来源:游戏开发者08-31-2010

. public void paint(Graphics g) { g.setColor( COLOR_BG ); g.fillRect( 0, 0, getWidth(), getHeight() ); Font font = Font.getFont( Font.FACE_PROPORTIONAL, Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ); String msMessage = frameTime + "ms per frame"; Image stringImage = Image.createImage( font.stringWidth( msMessage ), font.getBaselinePosition() ); Graphics imageGraphics = stringImage.getGraphics(); imageGraphics.setColor( COLOR_BG ); imageGraphics.fillRect( 0, 0, stringImage.getWidth(), stringImage.getHeight() ); imageGraphics.setColor( COLOR_FG ); imageGraphics.setFont( font ); imageGraphics.drawString( msMessage, 0, 0, Graphics.TOP | Graphics.LEFT ); for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) { g.drawImage( stringImage, getRandom( getWidth() ), getRandom( getHeight() ), Graphics.VCENTER | Graphics.HCENTER ); } } 当我们运行这个版本的软件时,我们看到我们的paint()方法占用的时间百分比减少了一点点.往里看,我们看到drawString()方法只被 调用了101次,而且现在是敌人啊我Image方法执的次数最多,被调用了1616次。虽然我们做了更多的工作,,但是程序运行得快了一点 ,因为我们所用的graphics调用要快一点。你或许发现了吧一个字符串画到一个图片上会影响显示,因为J2MEbing不支持图片的透明,所以大量的背景被重写了。这是一个 weruhe优化可能导致你重新审核程序需求的例子。如果你真的需要与文字重合,你可能被迫要用更少的时间来处理。这个代码或许好了一点点,但是它仍然有很大的可改进空间。让我们来看一看我们的第一个低级优化技术。 第三页循环之外?循环多少次,在for()内部的代码就会执行多少次。要改善性能,那么,我们想要尽可能的把循环中的代码移动到循环外。我们可以在 profiler中看到paint()被调用了101次,并且在它之中的循环又循环了16次。在这两个循环中有哪些我们可以移出来呢?让我们从他 们的定义说明开始,每当调用paint()时,我们声明了一个字体,一个字符串,一个图片对象和一个图形对象.我们将要把它们移出到该 类的最前面. public static final Font font = Font.getFont( Font.FACE_PROPORTIONAL, Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL); public static final int graphicAnchor = Graphics.VCENTER | Graphics.HCENTER; public static final int textAnchor = Graphics.TOP | Graphics.LEFT; private static final String MESSAGE = " ms per frame"; private String msMessage = "000" + MESSAGE; private Image stringImage; private Graphics imageGraphics; private long oldFrameTime; 你会发现,我把Font对象变成了一个公共的常量.这一点在你的程序中通常是有用的,你可以把你所要用到的字体声明都集中到一个地方 .我发现anchor也一样,所以我也把文本和图像坐标放到了一起.对这些的预处理,保持了这些运算--虽然不怎么重要--在循环之外了. 我把MESSAGE也变成了一个常量.那是因为Java喜欢到处创建字符串对象.字符串如果没有被控制,它们可能导致大量的内存消耗.不要 把它们留给自动回收,否则你很可能会遇到内存泄露,那最终会影响你的程序性能,特别是当垃圾回收器被调用得过于频繁时.字符串创 造垃圾,而垃圾不好.用一个字符串常量减少了这类问题.稍后我们会看到如何运用一个StringBuffer来完全的阻止字符串滥用带来的 内存流失. 既然我们把那些变成了实例变量,我们需要在构造函数里面添加这些代码: stringImage = Image.createImage( font.stringWidth( msMessage ), font.getBaselinePosition() ); imageGraphics = stringImage.getGraphics(); imageGraphics.setFont( font ); 另一个很酷的对于图形对象的大写字符的事是,我们可以设置一次字体然后就可以忘掉它了,不用每次在循环中都设置一次. 每次我们 还需要用fillRect()擦去图片对象. 热情的编码者可能会发现这里有一个机会从同一个图片创建两个图形对象,然后为fillRect()的 调用预设其中一个的颜色为COLOR_BG,并为 drawString()的调用预设另一个的颜色为COLOR_FG.不幸地,对同一个图片的多次调用 getGraphics()没有被定义,在不同的平台上不一样,于是你的技巧可能在Motorola上有效但在NOKIA上不行.如果不确定,就不做. 还有另一种改进我们的paint()的方法.再次使用我们的大脑我们认识到,如果从上次调用以来frameTime的值改变了,那么我们只需要 重画这个字符串.那是我们的新变量oldFrameTime到来的地方,下面是新的方法: public void paint(Graphics g) { g.setColor( COLOR_BG ); g.fillRect( 0, 0, getWidth(), getHeight() ); if ( frameTime != oldFrameTime ) { msMessage = frameTime + MESSAGE; imageGraphics.setColor( COLOR_BG ); imageGraphics.fillRect( 0, 0, stringImage.getWidth(), stringImage.getHeight() ); imageGraphics.setColor( COLOR_FG ); imageGraphics.drawString( msMessage, 0, 0, textAnchor ); } for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) { g.drawImage( stringImage, getRandom( getWidth() ), getRandom( getHeight() ), graphicAnchor ); } oldFrameTime = frameTime; } 现在Profiler显示OCanvas的paint总共所花费的时间百分比已经降低为42.01%了.对比结果frameTime在 paint()中的调用,对 drawString()和fillRect()的调用次数已经从101变为69了.那时一个不错的节约,没有多少可以做的了,现在是该认真的时候了.你优 化得越多,它就变得越困难.现在我们要去挖掉最后几块循环中的代码.我们现在正在剃去非常小的百分比或者说百分比的碎片了,但是 我们比较幸运,他们加起来还是比较可观的. 让我们从一些简单的开始.让我们调用那些函数一次并且把结果暂存在循环之外,而不是每次都调用getHeight()和getWidth(). 下一 步,我们将停止使用字符串并手动使用StringBuffer来做所有事.依靠在Graphics.setClip()的调用中限制绘画区域,我们将剃掉一些 对drawImage()的调用.最后,我们将避免在循环中对java.util.Random.nextInt()的调用. 这是些新的变量... private static final String MESSAGE = "ms per frame:"; private int iw, ih, dw, dh; private StringBuffer stringBuffer; private int messageLength; private int stringLength; private char[] stringChars; private static final int RANDOMCOUNT = 256; private int[] randomNumbersX = new int[RANDOMCOUNT]; private int[] randomNumbersY = new int[RANDOMCOUNT]; private int ri; ...这里是我们构造函数里的新代码: iw = stringImage.getWidth(); ih = stringImage.getHeight(); dw = getWidth(); dh = getHeight(); for ( int i = 0 ; i < RANDOMCOUNT ; i++ ) { randomNumbersX = getRandom( dw ); randomNumbersY = getRandom( dh ); } ri = 0; stringBuffer = new StringBuffer( MESSAGE+"000" ); messageLength = MESSAGE.length(); stringLength = stringBuffer.length(); stringChars = new char[stringLength]; stringBuffer.getChars( 0, stringLength, stringChars, 0 ); 你现在可以看到我们在预处理显示(Display)和图片(Image).我们也在暂存512次getRandom()的调用的结果,有了StringBuffer也不 再需要msMessage这个字符串.当然,肉依然在paint()方法中: public void paint(Graphics g) { g.setColor( COLOR_BG ); g.fillRect( 0, 0, dw, dh ); if ( frameTime != oldFrameTime ) { stringBuffer.delete( messageLength, stringLength ); stringBuffer.append( (int)frameTime ); stringLength = stringBuffer.length(); stringBuffer.getChars( messageLength, stringLength, stringChars, messageLength ); iw = font.charsWidth( stringChars, 0, stringLength ); imageGraphics.setColor( COLOR_BG ); imageGraphics.fillRect( 0, 0, iw, ih ); imageGraphics.setColor( COLOR_FG ); imageGraphics.drawChars( stringChars, 0, stringLength, 0, 0, textAnchor ); } for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) { g.setClip( randomNumbersX[ri], randomNumbersY[ri], iw, ih ); g.drawImage( stringImage, randomNumbersX[ri], randomNumbersY[ri], textAnchor ); ri = (ri+1) % RANDOMCOUNT; } oldFrameTime = frameTime; } 我们现在正在用一个StringBuffer来写我们的消息中的字符.相比于在开头插入一个字符,在StringBuffer的后面添加要容易得多,所 以我把字符显示的顺序调换了,现在frameTime在消息的最后了,比如:"ms per frame:120".我们每次重写最后的几位frameTime字符 ,保持消息的一部分不变. 像这样明白的运用StringBuffer会节约paint()方法内系统从创建到销毁字符串的时间.它是额外的工作,但 值得做.注意,我在把 frameTimer强制转换为一个整数.我发现用append(long) 导致了一个内存泄露.我不知道为什么,但这是一个为 什么你需要用软件注意事情的例子. 我们用font.charsWidth()来计算消息图片的宽度,以让我们可以画得最少.我们使用均衡字符,来使"ms per frame:1"的宽度比这绘 制它图片小,我们用Graphics.setClip(),所以我们就不需要画更多的. 这同样意味着我们只需要填充一个足够遮掩我们需要的区域那 么大的一个矩形.我们希望绘图省下的时间比调用font.charWidth()花去的时间多. 在这里这可能不会带来多大区别,但它确实是一个绘制玩家的分数到屏幕上的不错的技术.那种情况下,在绘制0分和150,000,000分之 间有着巨大的差别. 这多少是因为font.getBaselinePosition()的不正确的返回值,这个值好像和font.getHeight()的返回值一样, 啊! (叹气) 最后,我们刚刚搞定了我们的两个数组中预先计算的"随机"数,这节约了我们产生随机数的一些调用.注意用取模运算来实现一个循环数 组的用法.注意我们用同一个TextAnchor绘制图片和字符串,所以现在setClip()工作正常. 我们已经在一个灰色地带,怀着对这个版本的代码产生的数据。Profiler高速我们这个代码比没有这些改变的代码在paint()方法里 多花了大概7%多一点的时间。对font.charsWidth()的调用可能是个原因,它占了4.6%。(这并不多,但它可以被减少。注意我们每 次都会获取 MESSAGE字符串的宽度。我们可以简单的在循环体之间计算它,并简单的把它加到frameTime的宽度上。)同样,新的对 setClip()的调用被标识为0.85%,而且看起来大大的增加了drawImage所占用的时间百分比(从27.58%到33.94%)。到这一点了,看起来所有额外的代码肯定会使执行慢下来,但是程序产生的书记和这个假设矛盾了。在模拟器上的数据上下波动,看起 来好像没有长时间的测试,是不能下决定了,但是我的i85s报告说额外的代码比不加要快一点点,在没有对setClip()或者 charsWidth()时数据是 37130毫秒,而两个都有的时候是36540。我做了我的耐心所能忍受的那么多次,结果都一致。这使执行环境 差异这一点的影响突出起来。一旦你到了一个你不能确定会不会有进展的地方,你可能会被迫继续所有在硬件上的测试,这需要大量的 对JAR文件的安装和卸载。看起来我们已经从我们的图形程序段压榨出了大量的性能。现在是对我们的work()方法进行同样的高级和低级优化的时候了。让我们 来回顾一下那个方法: 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; } 每次在run()中的循环,我们都传递一个数组参数。在work()中的循环外计算了我们的除数,然后调用workMore()来做这个除法。这 里所有事都错了,你可能也发现了。因为一开始,程序员已经把getDivisor()的调用放到了循环内。如果j的值在循环内部没有改变, 那么除数是不变的,真的属于内循环外面。但是让我们多想一想,这个调用本身就是完全不必要的。下面的代码做了同样的事情... public synchronized int work( int[] n ) { r = 0; divisor = 1; for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) { for ( int i = 0 ; i < n.length ; i++ ) { r += workMore( n, i, divisor ); } divisor *= 2; } return r; } ...没有对getDivisor()的调用。现在我们的profiler告诉我们run()方法花了23.72%的时间,对应于我们做这些改进以前的38.78% 。请总是在使用低级优化技术之前首先使用你的大脑来优化。接下来,让我们看一看它们中的一些技术。 第四页低级优化所有的程序员都对子程序和函数---为了避免在多个地方重复,把程序中共用的代码从应用程序中提出来--的概念很熟悉。不幸地,这 个通常的 “好”变成习惯会影响性能,因为方法的调用会带来一定量的开销。最简单的减少对一个方法的调用所耗费的时间的方法是 ,仔细地挑选他们的声明修饰语。我们的程序员已经很小心了,已经把他的work()和workMore()方法同步了,以防万一其它的线程同 时调用了它们。这很不错,但是如果我们对性能很看重,我们常常要做出一些牺牲,今天我们的牺牲是安全的。好了,我们知道不会有其它人会调用这些方法,那么我们可以不怎么担心地把他们同步起来。还有什么其它的可以做?让我们来看一看 这个方法类型的列表: *synchronized 该方法是最慢的,因为需要获取一个对象锁 *interface 该方法是次慢的 *instance 这个方法居中 *final 该方法比较快 *static 该方法是最快的所以我们不应该让所有的都是synchronized,而且看起来我们甚至可以把work()和workMore标示为final static方法。这样做会减 少1%模拟器中run()方法所花的时间。另一个影响方法调用性能的因素是,传递给该方法的参数的个数。我们调用了workMore()51712次,每次都传递一个整形数组和两个 整形变量给这个参数并返回一个整形变量。在这个有点微不足道的例子中,可以很容易地把workMore()方法拆散到work()的方法体中 来完全地避免这个调用。在真实世界中,这是个很难的决定,特别是当这意味着需要把代码复制到你程序周围的时候。在profiler上 测试一下来看和没做这一步之前到底有多大的差别。如果你不能把所有的方法去掉,试着减少你传递的参数的个数。参数越多,开销就 越大。 public final static int work( int[] n ) { divisor = 1; r = 0; for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) { for ( int i = 0 ; i < n.length ; i++ ) { r += n * n / divisor + n; } divisor *= 2; } return r; } 哇哦!去掉workMore()的调用把run中的时间开销砍到了9.96%。