日期:2014-05-20  浏览次数:20814 次

Effective J2ME(4)

    本文节选于笔者在数年前开发手机游戏时总结的一份文档。一家之言,贻笑大方。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)。例如,在用户按住键一段时间后,如果在重画的间隔内松开了按键,那么在下次重画时会“多”移动一次(原因在于先移动,后判断是否松开按键)。具体代码如下:
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的作用在于保证在按下按键的第一次移动之后,如果用户松开按键,那么快速地响应松开按键的事件。具体代码如下:
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()方法的标准模式:
synchronized(obj) {
    while( condition checking) {
       obj.wait();
    }
    …// Other operations
}
?
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中运行正确的示例代码:
for(int i=0;i<m;i++)
      for(int j=0;j<n;j++)
         sImgTile.getGraphics().drawImage(image,…,…,…);

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