Effective J2ME

Effective J2ME(1)
关键字: j2me
http://whitesock.javaeye.com/blog/162708
本文节选于笔者在数年前开发手机游戏时总结的一份文档。一家之言,贻笑大方。
 

1 J2ME开发基础

1.1 计算几何在J2ME开发中的应用
      由于支持J2ME的设备通常不支持浮点运算,所以对于某些特定的问题可以通过计算几何学的方法解决。例如判断两条线段是否相交的算法,在支持浮点运算的设备上可以通过联立方程组求解的方法解决。在不支持浮点运算的设备上则需要通过计算几何的向量运算来解决。常见的可以通过计算几何学的方法解决的问题有 1、判断两线段是否相交;2、判断线段、折线、多边形是否在矩形中;3、判断矩形是否在矩形中;4、判断圆是否在矩形中;5、判断点是否在多边形中;6、判断折线是否在多边形内;7、求线段或直线与折线、矩形、多边形的交点;8、凸包的求法等等。具体的算法可以查阅计算几何学相关的教科书。

1.2 有限状态机在J2ME开发中的应用
      有限状态机在硬件设计、编译理论等许多方面有着广泛的应用。它同样适合游戏开发。例如在ForrestWarrior游戏中,Player类就采用有限状态机来控制各种状态之间的迁移。Player有以下几种状态1、站立;2、站立行走;3蹲;4、蹲着行走;5、跳跃;6、下落。同时在1、3、5和6状态下可以进行攻击(由于J2ME不响应同时按键,所以在2和4状态下不能攻击)。如果采用条件分支语句进行状态判断的话,判断逻辑将会异常复杂而且难于修改和调试。而在采用了有限状态机之后,控制逻辑大大简化。具体代码可以参考ForrestWarrior游戏中的Player类和State类。

1.3 J2ME游戏中寻径算法的设计策略
      一提到寻径算法,大家通常都会想到A*算法。但是由于支持J2ME设备的运算能力有限,堆栈的大小也不适合做复杂的递归运算,所以寻径算法的设计要针对每款游戏的特点进行设计。例如在雷鸟号游戏中,每架飞机(军舰、坦克)的寻径策略已经编码在剧情文件中,这样做的优点在于可以有效的控制飞机的编队移动。在炸弹超人游戏中,利用动态的阀值控制NPC的移动。具体策略就是通过设置不同的阀值上下限,同时在每一次移动之后对阀值进行微调的方法来控制移动中变向的频率。在荒岛探宝游戏中则采用了更精确的寻径算法。具体策略是采用广度优先算法搜索与Player连通的区域(通过调整搜索队列的大小来控制搜索的范围)。在搜索的过程中,如果某个点和Player连通,则同时标记与Player的距离。有了这些数据之后,NPC的寻径策略就变得相对简单。在NPC的每次移动之前,首先判断目前所在的位置和Player是否连通,如果不连通就采用同炸弹超人游戏中的NPC类似的算法进行移动;如果连通,就走向与Player的距离更小的那个点。

Effective J2ME(2)
关键字: j2me
本文节选于笔者在数年前开发手机游戏时总结的一份文档。一家之言,贻笑大方。

2 J2ME开发技巧

2.1 性能
    在模拟器上可以使用JProbe Profiler等分析工具来分析性能瓶颈。但是需要注意的是模拟器的瓶颈与实机中的瓶颈可能存在差别。

2.1.1 设计可重用的对象
    在Java程序中,对象的创建是个不小的开销。同时过多的无用对象也会导致耗时的垃圾回收。因此在程序中,每次编码类似 mBullet=null;的语句的时候,都要考虑一下这个对象是否可以被重用。推荐使用设计模式之一的工厂模式来控制对象的创建。例如在雷鸟号游戏的子弹类(在整个战斗场景中,子弹需要不停地被创建和销毁)的设计中,其构造函数被定义为私有,同时提供一个公共的静态方法public static Bullet newInstance(…);来创建子弹的实例。这样就可以对子弹实例的创建进行有效的控制。子弹类也定义了方法public void destroy();来控制子弹实例的销毁。destroy();方法并不是简单的将实例引用赋值成null,而是将这个实例保存到一个缓冲(程序中称之为回收站)里,当程序需要创建新的子弹实例的时候,newInstance方法首先检查回收站是否保存有使用过的子弹实例,如果没有就调用子弹类的构造函数创建一个新的实例;如果有,就给这个实例的成员重新赋值。如果需要,也可以将类设计为不可变的,这样做的好处之一就是该类的实例是线程安全的。总之,不要在程序里不必要的丢弃任何对象。

2.1.2 减少线程间的切换次数
    在J2ME游戏的开发中,推荐使用单线程驱动的结构。由于Java在语言层面上提供了线程级的支持,所以相对而言,使用Java语言可以更容易地开发出多线程应用。但是由于线程切换通常是一个代价高昂地操作,所以在设计阶段要尽量避免不必要的线程切换。例如线程间的同步通信,如果一个线程以同步的方式与另外一个线程通信,那么第一个线程在发出消息之后,调度程序就必须让第一个线程进入休眠状态(在单处理器的情况下),同时唤醒第二个线程处理消息。因此大量的线程间同步通信对程序的性能有很大的冲击。在J2ME游戏的开发中,单线程驱动的结构通常可以达到良好的效果,这样的结构既减少了线程切换的次数,又避免的棘手的线程间同步的问题。如果需要实现特殊的功能,可以通过定制特定的定时器(Timer)的方法解决。

2.1.3 减少基本数据类型间的强制类型转换
    由于J2ME设备的运行时内存有限,所以J2ME应用都倾向于的使用更精确的数据类型来描述数据。例如如果可以用byte型来定义变量就不使用int型。但是这样做通常会导致程序中有大量的基本数据类型之间的强制转换。就像永远不要过早优化一样,这种对更精确的数据类型的使用也不是灵丹妙药,应该分析程序的具体特点。通常应该对那些在程序中使用比较频繁而且使用量较大的对象进行优化。例如如果在程序里需要创建500个Building对象,那么就可能需要对Building类进行优化,将Building的成员变量定义为更准确的数据类型。在一般情况下,还是应该使用缺省int型。这样会大大减少基本数据类型间的强制类型转换对程序性能的冲击。

2.1.4 尽量减少重画的次数,如果可能,采用局部重画技术
    就我个人的经验而言,在J2ME游戏中,背景图的刷新占用了很大一部分CPU资源。特别是在那种用图块(Tile)拼接出整个背景的程序(比如炸弹超人和荒岛探宝游戏)中,这个现象比较明显。例如在128×128的屏幕上,如果用12×12的图块拼接出整个背景,那么需要做11×11=121次绘图操作。如果采用全屏刷新的策略,那么代价就比较可观了。常用的解决方案是:1、如果背景图不可变,例如雷鸟号游戏(背景图只是循环向下移动,其内容不变),那么强烈推荐采用双缓冲(只是双缓冲背景图)技术。具体做法就是将图块绘制到一张内存图片中。在每次重画背景时,将这张内存图片绘制到屏幕上,而不是用每次都用图块拼接出背景。如果内存情况允许的话,采用这种做法会显著地提升性能;2、如果背景图可变,仍然可以采用双缓冲技术,但是会比较复杂。程序必须能够识别脏图块(即已经改变而需要重画的图块),如果不能的话,就失去了双缓冲的意义。

Effective J2ME(3)
关键字: j2me
本文节选于笔者在数年前开发手机游戏时总结的一份文档。一家之言,贻笑大方。

2.2 规模
    通常J2ME设备对Jar文件的大小进行了限制,例如Nokia6610允许最大的Jar大小为64K。因此在开发J2ME应用时,应该尽量编写精简的代码,使用具有可以接受效果的最小的图片。

2.2.1 编写精简的代码
    很多人都曾提出过在J2ME开发中减小代码大小的建议,例如减少类和方法的个数、缩短变量和方法名、打破类的继承关系等等。但是我认为,这在做法都是在牺牲了程序的可读性、可扩展性的基础之上的,而且代价高昂。没有必要为了减少代码的大小刻意地这样做,这种代码通常难以调试和修改。与此相反,好的设计同样可以有效地减小代码的大小。代码冗余往往是编写精简代码的最大敌人而不是过长的变量名。唯一例外的是,我认为在J2ME开发中可以使用公共的类成员变量,而不必象在J2SE和J2EE开发中那样,将类的状态封装的那样完美。这样做既可以加快程序的速度,又比提供额外的访问方法(get方法和set方法)的做法生成的代码更小。在我编写的四款游戏中,雷鸟号、炸弹超人和荒岛探宝的代码规模都控制在2000-2200行左右。丛林武士的代码在3100行左右。其中用于程序框架部分(这四款游戏重用的基本的框架代码)的代码在800-900左右。

2.2.2 合并图片以减小图片的总大小
    在J2ME应用中通常使用.png格式的图片。由于.png图片使用彩色查找表(调色板)来存储颜色,所以将在颜色上类似的多个图片合并成一张图片可以有效地减少图片的总大小。例如以下四张图片 总大小为649字节。如果合并为一张图片之后 ,大小为191字节。在使用合并后的图片时,可以利用Graphics类的setClip(int sx ,int sy, int width ,int height)来设置实际的绘图区域。例如要在屏幕上(50,50)这个点上绘出第三把钥匙的代码是:(假设每把钥匙的大小为12×12)

Java代码
g.setClip(50,50,12,12);   
g.drawImage(image,50-12*2,50,20);  

g.setClip(50,50,12,12);
g.drawImage(image,50-12*2,50,20);
 

2.3 可移植性
    尽管目前我们的开发都是基于MIDP1。0之上的,但是不同的手机厂商往往提供了特定的开发包。例如Nokia提供了DirectGraphics类、DeviceControl类等;Motorola提供了Game API等。此外,不同型号手机的屏幕尺寸和处理器速度往往也不相同。因此在开发的过程中,应该充分认识到这些不可移植的因素,尽量增强程序的可移植性和自适应能力。

2.3.1 尽量使用标准的类库
    Motorola 的Game API针对游戏的开发提供了一些底层支持。但是如果我们在程序中使用了这些API,那么就意味着这款游戏将很难移植到Motorola以外的手机上,也无法移植到不支持Game API的Motorola 手机。就我们目前的开发要求而言,这是不明智的。同样,Nokia提供了DirectGraphics类,支持图片的旋转、对图片象素的访问等等。在某些情况下,使用这些API的确可以带来便利。例如如果使用DirectGraphics的图片旋转功能就可以节省图片资源,不必另外在资源中提供旋转后的图片。利用DirectGraphics的对图片象素的访问功能就可以利用象素的α通道计算出图片可见部分的边缘。在这种情况下,就要在可移植性和便利性之间做出取舍。就我而言,应该选择前者。

2.3.2 运行时得到屏幕的大小
    不要在程序中使用硬编码的屏幕尺寸。比如语句 g.drawString(“载入中…”,64,100,Canvas.HCENTER);这句代码明显是针对128象素宽的屏幕的。不适合移植到具有其它显示大小的设备上。如果可能,应该使用类似于g.drawString(“载入中…”,sCanvasWidth/2,sCanvasHeight-16,Canvas.HCENTER);的语句。对于变量sCanvasWidth,一种做法是将其定义为静态常量,如public static final int sCanvasWidth=128;这种做法的缺点在于,当程序需要移植到其它设备上时需要修改变量的初值。我个人推荐的做法是将其定义为静态变量,同时在程序运行时为其赋值一次。比如在显示闪屏之前(因为这个操作只需执行一次)利用sCanvasWidth=getWidth();获得屏幕的尺寸。

2.3.3 将控制游戏速度的参数保存在Jad文件中
    在目前的J2ME游戏中,通常通过调整游戏主线程的休眠时间来控制游戏的速度。对于处理速度较快的机型,可以适当增加休眠的时间;对于处理速度较慢的机型,可以适当减少休眠时间。通过这种方法来消除具有不同处理速度的手机之间的差异。推荐将这个参数保存在Jad文件中,而不是将其硬编码在程序代码中。这样做的优点在于可以在不必重新编译整个工程的情况下,针对目标机的特点调整游戏的速度。

2.3.4 使用GameAction来处理按键事件
    使用GameAction会增强应用程序的可移植性。MIDP 1.0规范中定义了KeyCode和GameAction之间的转换规则。Canvas类定义了抽象游戏动作集,例如UP、DOWN、LEFT、RIGHT和FIRE等等。通过Canvas类的public int getGameAction(int keyCode)和public int getKeyCode(int gameAction)两个方法可以实现KeyCode和GameAction之间的转换。需要注意的是,getKeyCode只能返回基于GameAction的一个KeyCode,即使MIDP 1.0允许超过一个KeyCode被实现到相同的GameAction。另外,GameAction映射对于要求快速键盘响应的游戏来说可能并不适用。不推荐在同一个应用程序中混合使用KeyCode和GameAction

Effective J2ME(4)
关键字: j2me
本文节选于笔者在数年前开发手机游戏时总结的一份文档。一家之言,贻笑大方。

3 J2ME开发中的常见问题
    由于J2ME相关资料比较少,所以在开发过程中经常会遇到现象诡异的问题。有些问题解决了,有些绕过了。以下总结了一些比较典型的问题和解决方法。

3.1 按键响应迟钝
    造成按键响应迟钝的最常见的原因是程序中的其它线程占用了绝大多数的CPU时间,从而导致负责事件转发的系统线程(EventDispatcher)得不到足够的CPU时间来处理事件。例如在从游戏屏幕返回到主菜单屏幕的时候,主线程通常没有被停止,而是不再驱动游戏剧情的发展。如果在这个时候主线程占用了过多的CPU时间,那么会影响到主菜单屏幕中对按键事件的响应。

3.2 丢失按键事件

3.2.1  我们通常采用的按键响应策略是在keyPressed(或者commandAction)方法中保存用户按下的键值;在keyReleased方法中取消对这个键值的保存。然后在每次重画时通过查询这个键值来决定是否需要移动以及移动的方向(我们的游戏通常都是采用单线程驱动重画的结构)。这种策略实现起来比较简单,但是存在一个致命的缺陷,即可能会导致丢失按键事件(这通常是用户无法忍受的)。具体原因如下:假设用户在按下键后很快又松开按键,如果这一切都是在线程重画的间隔中完成的,那么这个按键事件就不会得到响应。

 

3.2.2  针对策略1存在的问题,以下是改良后的策略2。当用户按下方向键时,在mCurRoute中保存用户选择的方向值(假设使用变量mCurRoute保存当前用户选择的方向值,取值范围 0~3,代表4个方向),同时将mRequestMoving(mRequestMoving 是用户请求移动的标志)置为true。在用户松开键时将mRequestMoving置为false。在每次重画时首先检查mCurRoute,如果是在0~3之间,那么说明用户已经按下方向键,需要做相应的移动。在移动之后检查mRequestMoving,如果是false,那么取消对用户按键的保存(例如将mCurRoute置为负值)。这种策略可以避免丢失按键事件的问题。具体原因如下:假设用户在按下键后很快又松开按键,如果这一切都是在线程重画的间隔中完成的,那么在重画之前mCurRoute仍然会保存用户曾经选择的方向值,mRequestMoving的值会在keyReleased方法中被置为false。由于对mCurRoute的检查是在对mRequestMoving的检查之前,所以仍然会根据mCurRoute的值做相应移动。

   策略1和策略2之间最大的区别在于是谁负责取消已经保存的方向值。在策略1中,是keyReleased方法负责取消已经保存的方向值。由于keyReleased方法无法知道这个事件是否已经被处理,所以这样做会造成按键事件的丢失;在策略2中,是move方法负责取消已经保存的方向值。由于在move方法中采用先移动后判断是否已经松开按键的方法,所以可以在某种程度上保证不丢失按键事件。如果用户在重画间隔内反复地快速按下和松开按键,那么这种策略只会保存最后一次的按键(这是可以容忍的)。   

   策略2虽然不会丢失按下按键的事件,但是仍然存在一个缺陷,即对松开按键事件的响应比较迟钝(如果程序中仍然无法忍受这种情况,可以考虑使用策略3)。例如,在用户按住键一段时间后,如果在重画的间隔内松开了按键,那么在下次重画时会“多”移动一次(原因在于先移动,后判断是否松开按键)。具体代码如下:   

Java代码
public void move()   
{   
    // 移动   
    if(mCurRoute>=0 && mCurRoute<=3) 移动…   
 
    // 移动后检查   
    if(!mRequestMoving) mCurRoute=-1;   
}   
 
public void keyPressed(int key)   
{   
    byte route=-1;   
    switch(key)   
    {   
      case 56: // 向下   
          route=0;   
          break;   
      case 50: // 向上   
          route=1;   
          break;   
      case 54: // 向右   
          route=2;   
          break;   
      case 52: // 向左   
          route=3;   
          break;   
    }   
 
    if(route>=0 && route<=3) // 如果用户按下方向键   
    {   
        mCurRoute=route; // 保存用户选择的方向值   
        mRequestMoving=true; // 设置请求移动标志   
    }   
}   
 
public void keyReleased(int key)   
{   
    mRequestMoving=false; // 清除请求移动标志   
}  

public void move()
{
    // 移动
    if(mCurRoute>=0 && mCurRoute<=3) 移动…

    // 移动后检查
    if(!mRequestMoving) mCurRoute=-1;
}

public void keyPressed(int key)
{
    byte route=-1;
    switch(key)
    {
      case 56: // 向下
          route=0;
          break;
      case 50: // 向上
          route=1;
          break;
      case 54: // 向右
          route=2;
          break;
      case 52: // 向左
          route=3;
          break;
    }

    if(route>=0 && route<=3) // 如果用户按下方向键
    {
        mCurRoute=route; // 保存用户选择的方向值
        mRequestMoving=true; // 设置请求移动标志
    }
}

public void keyReleased(int key)
{
    mRequestMoving=false; // 清除请求移动标志
}

3.2.3  针对策略2存在的问题,以下是再次改良后的策略3。策略3与策略2相比,多了一个变量mMovePending,同时增加了移动前的检查。当用户按下方向键时,同时将mMovePending置为true。在每次移动后将mMovePending置为false。mMovePending的作用在于保证在按下按键的第一次移动之后,如果用户松开按键,那么快速地响应松开按键的事件。具体代码如下:

Java代码
public void move()   
{   
    // 移动前检查   
    if(!mMovePending && !mRequestMoving) mCurRoute=-1;   
 
    // 移动   
    if(mCurRoute>=0 && mCurRoute<=3) 移动…   
 
    // 移动后检查   
    mMovePending=false;   
    if(!mRequestMoving) mCurRoute=-1;   
}   
 
public void keyPressed(int key)   
{   
    byte route=-1;   
    switch(key)   
    {   
        case 56: // 向下   
            route=0;   
            break;   
        case 50: // 向上   
            route=1;   
            break;   
        case 54: // 向右   
            route=2;   
            break;   
        case 52: // 向左   
            route=3;   
            break;   
    }   
 
    if(route>=0 && route<=3) // 如果用户按下方向键   
    {   
        mCurRoute=route; // 保存用户选择的方向值   
        mRequestMoving=true; // 设置请求移动标志   
        mMovePending=true; // 设置等待移动标志   
   }   
}   
 
public void keyReleased(int key)   
{   
    mRequestMoving=false; // 清除请求移动标志   
}  

public void move()
{
    // 移动前检查
    if(!mMovePending && !mRequestMoving) mCurRoute=-1;

    // 移动
    if(mCurRoute>=0 && mCurRoute<=3) 移动…

    // 移动后检查
    mMovePending=false;
    if(!mRequestMoving) mCurRoute=-1;
}

public void keyPressed(int key)
{
    byte route=-1;
    switch(key)
    {
        case 56: // 向下
            route=0;
            break;
        case 50: // 向上
            route=1;
            break;
        case 54: // 向右
            route=2;
            break;
        case 52: // 向左
            route=3;
            break;
    }

    if(route>=0 && route<=3) // 如果用户按下方向键
    {
        mCurRoute=route; // 保存用户选择的方向值
        mRequestMoving=true; // 设置请求移动标志
        mMovePending=true; // 设置等待移动标志
   }
}

public void keyReleased(int key)
{
    mRequestMoving=false; // 清除请求移动标志
}

3.3 图片无法被绘制到屏幕上
    造成这个问题的原因比较复杂,在查找问题的时候,有以下三点需要注意:1、paint(Graphics g);被异步调用,除非使用Canvas.serviceRepaints()强制执行;2、paint(Graphics g);方法被系统同步(MIDP用户界面API的设计是线程安全的);3、removeCommand(Command cmd) 和addCommand(Command cmd)方法会请求屏幕的刷新。而这种刷新可能是局部刷新(目前只是根据现象猜测,还没有找到相关文档)。基于以上三点,如果在paint方法中调用removeCommand或者addCommand会导致额外的paint的调用,而不是在当前这次paint方法中完成屏幕的刷新。

3.4 程序长时间地无任何响应
    造成这个问题的常见原因是程序死锁。例如,当调用Canvas.serviceRepaints()方法强制屏幕刷新时需要注意,当前线程之外的某个不同的线程也可能会调用paint方法(比如按键响应线程)。如果paint方法试图在已经被调用的serviceRepaints加锁的任何对象上同步,应用就会死锁。更严格的讲,就是在调用serviceRepaints的时候不应该持有任何锁。
    另外一个常见的原因是错误地使用了Object.wait()方法。在说明这个问题之前,有必要强调一下同步(synchronized关键字)的作用。同步的作用有两个:1、互斥访问;2、线程间的可靠通信。大家通常对其第一种作用比较了解,但是对其第二种作用比较陌生。在目前Java的内存模型(memory model)下,如果缺少有效的同步,那么一个线程对某个状态所做的修改并不一定对另一个线程是可见的。另外一个由目前Java的内存模型引起的著名问题是在C语言中正确的双重检查模式在Java中不能正确地工作。
    Object.wait()方法的作用是使一个线程等待某个条件。它一定是在一个同步区域中被调用,而且该同步区域锁住了被调用的对象。下边是使用Object.wait()方法的标准模式:

Java代码
synchronized(obj)   
{   
    while( condition checking)   
    {   
       obj.wait();   
    }   
    …// Other operations   
}  

synchronized(obj)
{
    while( condition checking)
    {
       obj.wait();
    }
    …// Other operations
}   关于同步相关更多的内容,可以参考笔者的Java Concurrent文章。

 

3.5 从游戏屏幕返回到主菜单屏幕时主菜单显示不正确
    这个问题在Motorola手机上比较常见。我认为造成这个问题的原因是在其它线程(例如负责响应按键事件的系统线程)在主线程的游戏屏幕重画过程中调用了Display.getDisplay(sInstance).setCurrent(Display disp);方法。如果在主线程的游戏屏幕重画间隔中调用setCurrent方法就不会出现这个问题(由此可以引申出一个更具体的策略,就是不要按键响应线程中修改程序的状态,尽量只是设置一些标志让主线程查询)。解决这个问题的方法有两种:1、在需要显示菜单的时候,也就是在按键响应代码中设置一个标志,让主线程在重画之前查询这个标志,以决定是否需要显示主菜单屏幕(这通常需要修改程序的结构);2、如果需要在按键响应代码中完成Display的切换,那么就要确定是在游戏屏幕的重画间隔中完成(在我开发的四款游戏中采用的是方法2,但是我更倾向于使用方法1)。

3.6从主菜单屏幕进入到游戏屏幕之前屏幕出现一段时间白屏
    通常在主菜单屏幕中选择“开始游戏”选项后出现这个问题。产生这个问题的原因通常是过多的初始化操作减慢了进入游戏屏幕后的第一次绘图操作。如果在开始游戏时候需要做较多的初始化操作,推荐显示一个载入中的屏幕,提示用户等待。具体做法时在用户选择了“开始游戏”选项后,不作任何初始化操作,首先在屏幕上绘制一个载入中画面,从而迅速的结束第一次重画操作。在此之后再做初始化操作。

3.7 在经过剪裁(使用Graphics.setClip方法)的屏幕上绘图后,显示效果不正确
    Motorola T720机器上存在这个问题。产生这个问题的原因是传入Graphics.setClip(int x, int y, int width ,int height)的参数x或者y为负值。解决方法是对参数x或者y进行检查,如果为负值,就将它们改为0。通常还需要同时调整width或者height的值。在目前的Nokia手机上不需要做这个检查,系统会正确显示。

3.8 在内存图片上绘图的结果不正确
    Nokia 7650机器上存在这个问题。在雷鸟号游戏中,背景图采用了双缓冲的技术。在背景图的绘制过程中,在Nokia7210上正确的代码在Nokia 7650上的运行结果不正确。以下是Nokia7210中运行正确的示例代码:

Java代码
for(int i=0;i<m;i++)   
      for(int j=0;j<n;j++)   
         sImgTile.getGraphics().drawImage(image,…,…,…);  

for(int i=0;i<m;i++)
      for(int j=0;j<n;j++)
         sImgTile.getGraphics().drawImage(image,…,…,…);    经过分析后发现,在每次循环中调用Image接口的getGraphics ()方法是造成在Nokia 7650上运行结果不正确的原因。以下是在Nokia 7650上采用的代码:

Java代码
Graphics g=sImgTile.getGraphics();   
for(int i=0;i<m;i++)   
      for(int j=0;j<n;j++)   
         g.drawImage(image,…,…,…);   
g=null;   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值