多线程技术是JAVA ME中的关键技术,应用十分频繁,尤其是在游戏中。但是对于新手来说,又容易忽略或错误的使用多线程,导致程序堵塞,而无法响应用户的输入请求。
由于笔者对于游戏开发不是十分了解,所以本文将仅就多线程技术在JAVA ME应用程序中的使用展开讨论。本文主要包含如下部分:
多线程与联网
手机中,所有的MIDlet程序都是由Application Manager Software(AMS)管理的。当MIDlet初始化后,AMS就会调用MIDlet的startApp()方法,此时MIDlet就进入了Acitive状态。在JAVA ME中有些操作可能会导致程序堵塞,比如连接网络等。如果这些操作与主程序在同一个主线程中完成,那么由于堵塞会造成程序长时间无法返回,也就无法响应用户的其他操作了。所以,如果我们在commandAction()中进行了联网的操作,则会造成如上所述的情况。
下面,将通过一个例子来演示如上的情况,并使用多线程最终解决此问题。这是一个“Echo Message”实例,手机端向服务器端发送一条消息,服务器得到此消息后直接返回给手机端。
首先,创建一个NetworkConnection类来封装联网相关的操作,这样,MIDlet中只需调用此类中的方法就可以完成联网的操作。代码如下:
以下是引用片段: /* * NetworkConnection.java * * Created on 2006年7月20日, 下午2:54 * */ package nju.hysteria.thread.connection; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import javax.microedition.io.Connector; import javax.microedition.io.HttpConnection; /** * * @author Magic */ public class NetworkConnection { private static final String URL = "http://localhost:8080/thread/"; private HttpConnection httpConnection; private String message; public NetworkConnection(String message) { this.message = message; connect(); } /** * Connect to web server. * */ public void connect(){ try { httpConnection = (HttpConnection) Connector.open(URL); httpConnection.setRequestMethod(HttpConnection.POST); } catch (IOException ex) { System.out.println("Can not open connection!"); ex.printStackTrace(); } } /** * Send message to server. * @throws java.io.IOException */ public void sendMessage() throws IOException{ DataOutputStream out = httpConnection.openDataOutputStream(); out.writeUTF(message); out.close(); } /** * Receive message from server. * @throws java.io.IOException * @return */ public String receiveMessage() throws IOException { DataInputStream in = httpConnection.openDataInputStream(); String message = in.readUTF(); in.close(); return message; } /** * Close connection. */ public void close(){ if(httpConnection!=null){ try { httpConnection.close(); } catch (IOException ex) { ex.printStackTrace(); } } } } |
以下是引用片段: /* * MalConnectionMidlet.java * * Created on 2006年7月20日, 下午2:53 */ package nju.hysteria.thread.connection; import java.io.IOException; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * * @author Magic * @version */ public class MalConnectionMidlet extends MIDlet implements CommandListener { private Display display; private TextBox text; private Command showCommand; public MalConnectionMidlet(){ display = Display.getDisplay(this); text = new TextBox("Message","请使用‘问候’命令发送消息",100,TextField.ANY); showCommand = new Command("问候",Command.SCREEN,1); text.addCommand(showCommand); text.setCommandListener(this); } public void startApp() { display.setCurrent(text); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command command, Displayable displayable) { if(command==showCommand){ /** * 在当前的线程中直接进行联网操作,造成程序堵塞。 */ String message = null; NetworkConnection connection = new NetworkConnection("Hello World"); try { connection.sendMessage(); message = connection.receiveMessage(); connection.close(); } catch (IOException ex) { ex.printStackTrace(); } text.setString(message); } } } |
图1
以下是引用片段: Warning: To avoid potential deadlock, operations that may block, such as networking, should be performed in a different thread than the commandAction() handler. |
这就是因为没有使用多线程造成的。下面,就来看看如何使用多线程来解决此问题。
[NextPage]
新建类NetworkThread,它继承在Thread,并将原先commandAction()中发送,接受消息的操作移到此类中完成。代码如下:
以下是引用片段: /* * NetworkThread.java * * Created on 2006年7月20日, 下午4:16 * */ package nju.hysteria.thread.connection; import java.io.IOException; import javax.microedition.lcdui.TextBox; /** * * @author Magic */ public class NetworkThread extends Thread { private NetworkConnection connection; private TextBox text; public NetworkThread(TextBox text) { super(); this.text = text; } public void run() { String message = null; connection = new NetworkConnection("Hello World"); try { connection.sendMessage(); message = connection.receiveMessage(); connection.close(); } catch (IOException ex) { ex.printStackTrace(); } text.setString(message); } } |
以下是引用片段: /* * ConnectionMidlet.java * * Created on 2006年7月20日, 下午2:53 */ package nju.hysteria.thread.connection; import java.io.IOException; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * * @author Magic * @version */ public class ConnectionMidlet extends MIDlet implements CommandListener { private Display display; private TextBox text; private Command showCommand; public ConnectionMidlet(){ display = Display.getDisplay(this); text = new TextBox("Message","请使用‘问候’命令发送消息",100,TextField.ANY); showCommand = new Command("问候",Command.SCREEN,1); text.addCommand(showCommand); text.setCommandListener(this); } public void startApp() { display.setCurrent(text); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command command, Displayable displayable) { if(command==showCommand){ /** * 创建新的线程完成联网操作 */ (new NetworkThread(text)).start(); } } } |
图2
多线程与拍照
同样,在移动多媒体API(JSR 135)的使用过程中也需要应用到多线程。利用手机摄像头拍照的操作也会引起的堵塞,因此当在commandAction()中调用拍照操作时,若未开辟新线程来处理,同样也会造成程序没有响应。让我们通过一个例子来观察一下吧。
这是一个很简单的摄像头程序,首先程序将会调用摄像头取景,然后当用户按下“拍照”命令后就捕捉当前的画面并显示给用户。图3是程序的UML类图。
图3
MalCameraMidlet和CameraMidlet分别是错误和正确的MIDlet,它们都创建一个CameraView(用于显示摄像头画面)的对象,唯一的不同在于前者创建MalCamera的对象,后者创建Camera的对象(MalCamera和Camera都是CameraView的子类)。SnapShot则是将摄像头捕捉到的图像显示给用户的类。
首先,我们还是来看一下未使用多线程,而造成程序没有响应的情况。如下是MalCameraMidlet类的代码,其中保存着一个指向CameraView的引用,并在startApp()中创建了MalCamera对象。
以下是引用片段: /** * MalCameraMidlet.java * */ package nju.hysteria.thread.camera; import javax.microedition.lcdui.Display; import javax.microedition.midlet.MIDlet; /** * This MIDlet create the mal camera view, so the program * will block after calling Capture command. * @author Magic * */ public class MalCameraMidlet extends MIDlet { protected Display display; private CameraView camera; public MalCameraMidlet() { super(); display = Display.getDisplay(this); } protected void startApp() { camera = new MalCamera(this); display.setCurrent(camera); } /** * Show current camera. */ public void showCamera(){ display.setCurrent(camera); } protected void pauseApp() { } protected void destroyApp(boolean arg0) { } } |
以下是引用片段: /** * CameraView.java */ package nju.hysteria.thread.camera; import java.io.IOException; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Item; import javax.microedition.media.Manager; import javax.microedition.media.MediaException; import javax.microedition.media.Player; import javax.microedition.media.control.VideoControl; import javax.microedition.midlet.MIDlet; /** * This is an abstract class, which display camera to user. * @author Magic * */ public abstract class CameraView extends Form implements CommandListener { protected MIDlet midlet; protected Player player; protected VideoControl vc; protected Command exitCommand; protected Command captureCommand; protected CameraView(MIDlet midlet){ super("照相"); this.midlet = midlet; exitCommand = new Command("退出",Command.EXIT,0); captureCommand = new Command("拍照",Command.SCREEN,1); addCommand(exitCommand); addCommand(captureCommand); setCommandListener(this); /** * Create camera player and control. */ try { player = Manager.createPlayer("capture://video"); player.realize(); vc = (VideoControl)player.getControl("VideoControl"); append((Item)vc.initDisplayMode(VideoControl.USE_GUI_PRIMITIVE,null)); player.start(); } catch (IOException e) { e.printStackTrace(); } catch (MediaException e) { e.printStackTrace(); } } public abstract void commandAction(Command cmd,Displayable displayable); } |
[NextPage]
MalCamera和Camera都继承了CameraView类,并分别实现了commandAction()方法。先来看一下MalCamera:
以下是引用片段: /** * MalCamera.java */ package nju.hysteria.thread.camera; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.Displayable; import javax.microedition.media.MediaException; import javax.microedition.midlet.MIDlet; /** * This class display the mal camera. In commandAction(), * for capture command, just get the data without create a * new thread, and thus cause program blocked. * @author Magic * */ public class MalCamera extends CameraView { public MalCamera(MIDlet midlet){ super(midlet); } public void commandAction(Command cmd, Displayable displayable) { if(cmd==exitCommand){ try { player.stop(); } catch (MediaException e) { e.printStackTrace(); } player.close(); ((MalCameraMidlet)midlet).destroyApp(false); midlet.notifyDestroyed(); }else if(cmd==captureCommand){ // Do not handle in a new thread. try { byte[] data = vc.getSnapshot(null); new SnapShot(midlet,data); } catch (MediaException e) { e.printStackTrace(); } } } } |
图4
查看控制台窗口,得到如下提示:
以下是引用片段: Warning: To avoid potential deadlock, operations that may block, such as networking, should be performed in a different thread than the commandAction() handler. |
同样,还是由于没有创建新的线程进行处理的原因。下面,我们就把处理的代码放到新的线程中来完成,看看情况如何。如下是修改过的MalCamera代码,Camera.java:
以下是引用片段: /** * Camera.java */ package nju.hysteria.thread.camera; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.Displayable; import javax.microedition.media.MediaException; import javax.microedition.midlet.MIDlet; /** * This class displays camera. And do the right thing * for capture command, putting code in a new thread. * @author Magic * */ public class Camera extends CameraView { public Camera(MIDlet midlet){ super(midlet); } public void commandAction(Command cmd, Displayable displayable) { if(cmd==exitCommand){ try { player.stop(); } catch (MediaException e) { e.printStackTrace(); } player.close(); ((CameraMidlet)midlet).destroyApp(false); midlet.notifyDestroyed(); }else if(cmd==captureCommand){ // Handle in a new thread. new Thread(){ public void run(){ try { byte[] data = vc.getSnapshot(null); new SnapShot(midlet,data); } catch (MediaException e) { e.printStackTrace(); } } }.start(); } } } |
以下是引用片段: /** * CameraMidlet.java */ package nju.hysteria.thread.camera; import javax.microedition.lcdui.Display; import javax.microedition.midlet.MIDlet; /** * The correct MIDlet. * @author Magic * */ public class CameraMidlet extends MIDlet { protected Display display; private CameraView camera; public CameraMidlet() { super(); display = Display.getDisplay(this); } protected void startApp() { camera = new Camera(this); display.setCurrent(camera); } /** * Show current camera. */ public void showCamera(){ display.setCurrent(camera); } protected void pauseApp() { } protected void destroyApp(boolean arg0) { } } |
图5
[NextPage]
Timer与TimerTask
联网和拍照这两种情况都需要程序员创建新的线程来完成任务,并且这种做法对于程序员来说是显式的,即通过直接使用Thread类或Runnable接口来直接创建新线程。在MIDP的API中同样提供了隐式的方式来创建新线程,以方便程序员的编程。这就是TimerTask类,它实现了Runnable接口,用户只需创建一个继承它的类,并且实现run()方法,以此来创建新线程,而无需显示的继承Thread或Runnable。
当然,TimerTask的优点不仅于此,从它的名字来看,可以认为它是一个在特定时间执行的任务。run()方法中代码就是这任务,那么怎么控制其在特定时间执行呢?这就需要Timer这个类的帮助了。顾名思义,Timer是一个定时器,通过调用它的多个schedule(...)方法中的一个,可以控制在特定的时间,或每隔一定时间执行TimerTask。具体的方法介绍请看JDK文档。
TimerTask和Timer经常一起使用,比如在显示时间,倒计时和显示欢迎界面时会经常用到。下面,就通过一个实例来介绍这两个的用法。
这是一个计时的程序,程序从0秒开始,用户可以随时暂停或继续计时。程序是通过Timer和TimerTask来完成的,包含三个类:ClockMidlet,ClockCanvas和Clock。首先来看一下本程序最主要的类ClockCanvas:
以下是引用片段: /** * ClockCanvas.java */ package nju.hysteria.thread.clock; import java.util.Timer; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; /** * This class display time to user, and also start the timer * to update time each second. * @author Magic * */ public class ClockCanvas extends Canvas implements CommandListener { private ClockMidlet midlet; private Command exitCommand; private Command stopCommand; private Command resumeCommand; private long second; private int x; private int y; // Timer private Timer timer; public ClockCanvas(ClockMidlet midlet){ this.midlet = midlet; exitCommand = new Command("退出",Command.EXIT,0); stopCommand = new Command("停止",Command.SCREEN,1); resumeCommand = new Command("继续",Command.SCREEN,1); addCommand(exitCommand); addCommand(stopCommand); setCommandListener(this); second = 0; x = getWidth()/2 - 10; y = getHeight()/2 - 10; // Create timer and start it. timer = new Timer(); timer.schedule(new Clock(this),0,1000); } /** * Add one second. * */ public void addSecond(){ second++; } protected void paint(Graphics g) { g.setColor(0,0,0); g.fillRect(0,0,getWidth(),getHeight()); g.setColor(255,0,0); g.setFont(Font.getFont(Font.FACE_SYSTEM,Font.STYLE_PLAIN,Font.SIZE_LARGE)); // Draw second. g.drawString(String.valueOf(second),x,y,Graphics.LEFT|Graphics.TOP); } public void commandAction(Command cmd, Displayable displayable) { if(cmd==exitCommand){ timer.cancel(); midlet.destroyApp(false); midlet.notifyDestroyed(); }else if (cmd==stopCommand){ timer.cancel(); removeCommand(stopCommand); addCommand(resumeCommand); }else if (cmd==resumeCommand){ timer = null; timer = new Timer(); timer.schedule(new Clock(this),0,1000); removeCommand(resumeCommand); addCommand(stopCommand); } } } |
以下是引用片段: /** * Clock.java */ package nju.hysteria.thread.clock; import java.util.TimerTask; /** * Update the time. * @author Magic * */ public class Clock extends TimerTask { private ClockCanvas canvas; public Clock(ClockCanvas canvas){ this.canvas = canvas; } public void run() { canvas.addSecond(); canvas.repaint(); } } |
非常简单,在run()方法中就是调用了ClockCanvas类中addSencond()方法来增加时间,同时调用repaint()来更新界面。从表面上看几乎没有多线程的痕迹,其实创建Timer的时候就相当于在后台创建了一个新的线程,它控制着TimerTask的执行,因此对于秒数的增加是在另一个线程的完成的,而主线程只负责更新显示。
在加上下面的ClockMidlet就可以运行程序了。
以下是引用片段: /** * ClockMidlet.java */ package nju.hysteria.thread.clock; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Display; import javax.microedition.midlet.MIDlet; /** * Clock MIDlet. * @author Magic * */ public class ClockMidlet extends MIDlet { private Display display; private Canvas clockCanvas; public ClockMidlet() { super(); display = Display.getDisplay(this); } protected void startApp(){ clockCanvas = new ClockCanvas(this); display.setCurrent(clockCanvas); } protected void pauseApp() { } protected void destroyApp(boolean arg0) { } } |
图6
总 结
以上介绍了多线程技术在联网,拍照中的应用,以及MIDP自带的TimerTask和Timer的使用方法。在实际编程过程中会遇到更多的使用多线程的情况,比如播放背景音乐等,但基本方法都是如此,这里不再赘述了。
由于笔者水平有限,以上论述只是蜻蜓点水,未能深入讨论多线程的使用,特别是防止死锁,以及wait()/notify()的使用。