尽管支持移动信息设备规范MIDP 1.0的手机大量地出现在市面上,不管是Nokia未来发售的各款手机,还是Motorola T720、V60i,Sony Ericsson P800等手机,每一款都支持MIDP 1.0。但是,Sun也没忘记继续和各家手机厂商制定下一代Java手机的规格—MIDP 2.0。
何谓应用程序管理员应用程序管理员在规格中也称作Application Management Software。这是一个用来执行J2ME应用程序的程序,它负责管理该装置上所有的J2ME应用程序。
应用程序管理员的设计方式会随着平台的不同而不同,但是大致上可以分成两种方式:
1. 在背后运作,使用者不知道应用程序管理员的存在。这种类型的应用程序管理员概念如图1所示。
图1 背后运作的应用程序管理员背后运作的应用程序管理员的设计方式,使得一般的J2ME程序看起来和应用程序管理员一样,即使实际上应用程序管理员在背后运作,使用者也很难感受到。这种设计可以在MIDP for Palm之中看到,Java HQ就是这样的东西,如图2所示。
图2 Java HQ但是,如果是程序开发者一旦安装了Developer.prc,仍然可以透过Java HQ之中的Developer Preference里的MIDlets按钮来观察整个系统之中所安装的每一个Java应用程序。
2. 一个单一的进入点,使用者必须先进入应用程序管理员,然后才能启动个别的Java应用程序。这种类型的应用程序管理员概念如图3所示。
图3 单一进入点的应用程序管理员这种应用程序管理员设计可以在Motorola A388、Nokia 9210、Nokia 7650的手机上看到。
JAD与JAR一个完整的MIDP应用程序是由一个JAD文件(纯文字文件)与JAR(ZIP压缩档)所组成。JAD与JAR之间的关系可以用图4简单描述。之所以有这样的设计,主要为了下面两个原因:
图4 JAD与JAR关系图1. 网络传输费用;
2. 安全性。
MIDP 2.0之后,为了保护JAR不受窜改,同时也让安装程序的人可以确定MIDlet的来源,所以特别增加了安全设计如图5。
图5 安全设计JAD图其中,MIDlet-Jar-RAS-SHA1属性值为经过base64编码的执行文件数字签章。而MIDlet-Certificate-
<n>-<m>属性值为安全证明。在MIDlet安装前,应用程序管理员会使用安全证明来验证公开金钥的可靠性,然后再使用此公开金钥解开数字签章,确认此执行档的来源并确定没有受到非法窜改。
最后请注意,并非每种装置在安装时都要求同时有JAD与JAR,有些装置只需要JAR即可。不过,有JAD和没有JAD的J2ME应用程序在安全性上是有差异的。
MIDP执行环境根据MIDP规格,所谓MIDP执行环境(MIDP Execution Environment)指的是下面几项所构成的集合:
1. 实作CLDC中所定义的类别函数库的类别档(以Java撰写)及原生程序(Native code,以C撰写)。MIDlet Suite里不能有与CLDC类别函数库同样名称的类别;
2. 实作MIDP中所定义的类别函数库的类别档(以Java撰写)及原生程序(Native code,以C撰写)。MIDlet Suite里不能有与MIDP类别函数库同样名称的类别;
3. 所有来自同一个JAR档中的类别档。包括设计者自己所撰写的类别、其它的JSR(例如Profile或Optional Package),或其它开放的函数库(例如kXML或kSOAP);
4. 所有来自同一个JAR档之中的非类别档(即资源文件),另外,记录管理系统(RMS,MIDP版的数据库管理系统)也是可共享的资源之一;
5. 权限确认与连结外部资源;
6. 描述文件与清单文件的内容。
以上这几点构成所谓的MIDP执行环境。应用程序管理员会保证这些资源都可以在执行时期供MIDlet存取。而且,位于同一个MIDlet Suite的MIDlet会共享同一组MIDP执行环境,并且可以彼此互动。MIDlet可以调用CLDC的类别函数库,也可以调用MIDP的类别函数库,如图6所示。
图6 MIDlet执行环境图只有存取标准CLDC与MIDP函数库的MIDlet Suite才可以跨平台。通常手机厂商会针对自己的装置开发专属的API,例如Nokia的UI API、SMS API、Camera API。一旦程序使用了这些专属API,那么除非其它厂商也在其虚拟机器之中实作这些API,否则很难达到跨平台的目的。
功能与资源位于JAR档之中的类别档可以被同一个JAR档之中的MIDlet所使用。同一个JAR档里的资源档可以透过java.lang.Class.getResourceAsStream()来存取。描述档的内容则可以透过javax.microedition.mdilet. MIDlet.getAppProperty()取得,如图7所示。
图7 JAR功能资源图
使用getResourceAsStream()时,必须给定一个URL,由于我们要存取JAR内部的资源,如果使用「/」作为开头,代表绝对路径,「/」代表JAR文件之中的根目录。如果没有使用「/」,则视为相对路径,要看调用getResourceAsStream()类别的所在路径而定,这样容易造成混淆。所以请尽量使用「/」作为开头的绝对路径。
MIDlet的程序结构
如果曾经撰写过Java Applet或Java Servlet,就知道制作Java Applet,必须继承自java.applet.Applet这个类别;制作Java Servlet,则程序必须继承自javax.servlet. http.HttpServlet这个类别。同理,要撰写能够在手机上执行的Java MIDlet必须要继承自javax.microedition.midlet. MIDlet类别,如图8所示。
图8 MIDlet继承体系图
javax.microedition.midlet.MIDlet类别中定义了三个抽象方法,它们分别是:startApp() ==> 至运作状态;pauseApp() ==> 至停止状态;destoryApp() ==> 至消灭状态。应用程序管理员就是透过这三个抽象方法来控制MIDlet的生命周期。因此,所有的MIDlet都必须实现这三个方法,才保证能正常运作。因此,一个MIDlet的程序外壳至少要如下:
import javax.microedition.midlet.*;
public class HelloMIDlet extends MIDlet
{
public HelloMIDlet()
{
//建构式
}
public void startApp()
{
}
public void pauseApp()
{
}
public void destroyApp(boolean unconditional)
{
}
}
|
根据MIDP规格,MIDlet中不应该有“public static void main(Straing[] args)”这个方法。如果有则应用程序管理员会忽略不管。
一旦MIDlet被JAM载入之后,首先会先呼叫MIDlet中没有参数的建构式以进行初始化的工作。如果熟悉Java语法一定知道Java语言的一些特性,就是如果没有在程序中加入任何建构式,编译器会自动帮助加入一个预设建构式。但是如果已经撰写了自己的建构式(有任何参数),那么编译器将不会自动帮助加上预设建构式。因此,如果有必要撰写有参数的建构式,别忘了再手动加上一个没有参数的建构式,否则MIDlet将无法正确地初始化。
MIDlet的起始行为
当MIDlet被应用程序管理员产生之后,MIDlet就开始运作,程序设计师应该做的事情如图9所示。
图9 MIDlet起始行为图
我们会使用Display.getDisplay(this)来取得代表该MIDlet显示屏的Display对象。从应用程序管理员呼叫startApp()到MIDlet结束运作这段时间之内,不管何时呼叫Display.getDisplay(this),取得的都是同一份Display对象的参考。
要设定显示在屏幕上的画面,会使用Display对象的参考,并调用其setCurrent()方法,即display.setCurrent( Displayable类别的子类别实体)。因此一个可以运作的MIDlet程序,至少如HelloMIDlet.java,代码为:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class HelloMIDlet extends MIDlet
{
private Display display;
public HelloMIDlet()
{
display = Display.getDisplay(this);
}
public void startApp()
{
Form t = new Form("画面");
display.setCurrent(t);
}
public void pauseApp()
{
}
public void destroyApp(boolean unconditional)
{
}
}
|
注意:根据规格MIDlet只能由应用程序管理员产生,我们不能自己在程序里生成其它的MIDlet,并呼叫其生命周期函数,这样做将引发SecurityException异常。
MIDlet的生命周期
前面简单地叙述了应该如何撰写一个MIDlet的轮廓。但事实上,MIDlet的运作稍微复杂一点。所以接下来要仔细探讨MIDlet的运作细节,也就是MIDlet的生命周期。
当MIDlet被应用程序管理员成功地初始化之后,就开始展开了它的生命周期。图10描述了一个MIDlet的生命周期。MIDlet的生命周期完全由应用程序管理员控制,也就是说,当MIDlet要从一个状态变成另外一个状态时,应用程序管理员会呼叫对应的回呼函数(Call Back,也就是MIDlet类别定义的那三个抽象方法)。MIDlet基本上有三种状态,分别是停止状态(Paused)、启动状态(Active)及毁灭状态(Destroyed)。MIDlet开始时一定是先进入停止状态,然后应用程序管理员再将它转换成启动状态,然后调用startApp(),见图10。只有当应用程序管理员认为MIDlet的状态必须改变时,才会呼叫图中的相关函数。
图10 MIDlet生命周期图
以Active状态来说,MIDlet先进入运作状态,然后才调用startApp(),而MIDlet会先调用pauseApp()或destroyApp(),然后再进入停止状态和毁灭状态,这就是之所以Active没有被动式(字尾没有加ed),而Paused和Destroyed都是被动式(字尾有加ed)的真正涵义。
如果MIDlet自己调用这些函数,通常不会发生错误(除非程序本身有逻辑上的错误),但是也不会造成状态的转换,只是一个单纯的函数呼叫而已。如果MIDlet在状态转换回呼函数执行时发生错误,那么就应该丢出MIDletStateChange Exception异常,让应用程序管理员知道该如何处理。
请使用下列程序HelloMIDlet.java代码来验证MIDlet的生命周期:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class HelloMIDlet extends MIDlet
{
private Display display;
public HelloMIDlet()
{
System.out.println("Constructor") ;
display = Display.getDisplay(this);
}
public void startApp()
{
System.out.println("startApp Called") ;
Form t = new Form("画面");
display.setCurrent(t);
}
public void pauseApp()
{
System.out.println("pauseApp Called") ;
}
public void destroyApp(boolean unconditional)
{
System.out.println("destroyApp Called :" + unconditional) ;
}
}
|
执行结果时我们会在讯息显示窗口看到:
Constructor
startApp Called
|
此结果表示建构式先被呼叫,然后startApp()才会呼叫。
这时如果我们按下强制关闭应用程序的按钮 ,屏幕上就会出现:
这代表当我们使用装置上的按钮强制关闭MIDlet时,应用程序管理员会调用destroyApp(),并传入true作为参数。因此,撰写程序时,可以假定destroyApp()传入true时,是硬件强制关闭MIDlet的情形。
从图10中可以看出,startApp()很可能不只是被呼叫一次,而是每次从停止状态重新回到运作状态的时候都会被应用程序管理员调用,所以只需要被初始化一次的动作就不适合放在startApp()之中,请改用建构式做初始化动作。如果startApp()丢出MIDletStateChangeException或RuntimeException或两者的子类别,那么会立刻进入毁灭状态,而且系统会自动调用destroyApp(true)。
由于规格告诉我们,startApp()的执行时间应该尽可能短。如果程序在执行时,发生的错误是可以稍候就解决的(很可能是系统资源暂时不足),那么程序设计师就该直接丢出MIDletStateChangeException。拦截之后,再调用notifyPaused(),稍待一会再藉由异步事件呼叫resumeRequest(),重新试看看。如果发生错误即使稍待一会也无法解决,那么程序设计师就应该直接调用notifyDestroyed()来结束程序。参见如下代码:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class HelloMIDlet extends MIDlet
{
private Display display;
public HelloMIDlet()
{
System.out.println("Constructor") ;
display = Display.getDisplay(this);
}
public void startApp()
{
System.out.println("startApp Called") ;
Form t = new Form("画面");
display.setCurrent(t);
throw new RuntimeException() ;
}
/*
public void startApp() throws MIDletStateChangeException
{
System.out.println("startApp Called") ;
Form t = new Form("画面");
display.setCurrent(t);
throw new MIDletStateChangeException() ;
}
*/
public void pauseApp()
{
System.out.println("pauseApp Called") ;
}
public void destroyApp(boolean unconditional)
{
System.out.println("destroyApp Called :" + unconditional) ;
}
}
|
执行结果如下:
Constructor
startApp Called
startApp threw an Exception
java.lang.RuntimeException
java.lang.RuntimeException
at HelloMIDlet.startApp(+33)
at javax.microedition.midlet.MIDletProxy.startApp(+7)
at com.sun.midp.midlet.Scheduler.schedule(+266)
at com.sun.midp.main.Main.runLocalClass(+28)
at com.sun.midp.main.Main.main(+88)
destroyApp Called :true
|
一般来说,应用程序管理员会因为某些状况必须请MIDlet停止运作,例如手机突然来电、闹铃响了或者使用者切换到其它程序执行。在这些情况下,为了避免MIDlet占用太多系统资源,应用程序管理员就会调用该MIDlet的pauseApp()。这样程序设计师应该在pauseApp()之中适时释放一些非必需的资源,等到回到运作状态时,应用程序管理员会重新呼叫startApp(),这时再将这些之前被pauseApp()释放的资源重新找回来。
当MIDlet进入停止状态时,不应该使用任何资源。如果应用程序管理员调用pauseApp()时产生例外情形,MIDlet就应该立刻进入毁灭状态。
同样的情况也发生在destroyApp()。通常此方法被调用的时候,代表MIDlet要被关闭了,所以程序设计师应该在这里释放自己所配置的资源。只要MIDlet进入了毁灭状态,就无法再回头。如果是系统自己调用destroyApp(),那么在destroyApp()执行时万一发生例外,这些例外将被忽略,MIDlet一样会被关闭。根据规格,我们不能在MIDlet之中直接呼叫System.exit()或Runtime.exit()来结束程序的执行。如果这样做,就会引发java.lang.SecurityException异常。
MIDlet管理自己的生命周期
除了由应用程序管理员来控制MIDlet的生命周期之外,MIDlet本身也可以决定自己的状态,但不是自己改变状态,而是MIDlet先呼叫上述相对应的状态改变函数。这些函数会发出讯息通知应用程序管理员,请它来帮助改变MIDlet的状态,但是决定权在应用程序管理员,不保证一定可行。状态改变函数如图11所示:
图11 MIDlet状态改变函式图
对照以上这两张与状态有关的图,举个例子大家可能会比较清楚:假设今天如果是MIDlet主动要将MIDlet的状态由运作状态变成停止状态,那么直接呼叫pauseApp()函数只会执行pauseApp()之中的程序代码,而无法改变MIDlet的状态。MIDlet必须呼叫notifyPaused()以通知应用程序管理员,应用程序管理员收到通知之后,才会让MIDlet进入停止状态。
不过,由MIDlet调用notifyPaused()与应用程序管理员主动要求停止,两者之间是有所差别的。它们主要在于应用程序管理员主动要求停止时,pauseApp()会被呼叫,而由MIDlet调用notifyPaused(),pauseApp()不会被调用。但是两者都会让MIDlet进入停止状态。所以在MIDlet调用notifyPaused()之前,最好自己也先调用pauseApp()比较适当。
同样的情况也发生在notifyDestroyed()与destroyApp()。除非是系统强制关闭MIDlet,否则最好MIDlet先呼叫destroyApp(),然后再呼叫notifyDestroyed()。请应用程序管理员帮助将MIDlet转换到毁灭状态,最后结束MIDlet的运作。单单MIDlet自己呼叫destroyApp()是没有用的。
destroyApp()有个布尔值作为参数,根据MIDP的规格,如果传入true,那么MIDlet不管如何应该无条件释放所有资源,然后让应用程序管理员结束MIDlet的运作,这是系统或硬件强制关闭MIDlet的情形。如果使用者调用notifyDestroyed()来结束MIDlet,那么在调用destroyApp()时,最好传入false,代表这并非系统或硬件强制关闭,这时如果MIDlet不希望结束执行,那么它可以藉由丢出MIDletStateChangeException异常告知呼叫它的人:“我还不想被消灭,请待会再来。”
这里可以看出startApp()、pauseApp()及destroyApp()并非控制MIDlet生命周期的函数,它们只是一个提供初始化资源、释放资源的地方而已。
根据MIDP 规格书中所说,即使MIDlet处于停止状态,它仍然可以处理异步事件(Asynchronous Event),比方说Timer的事件或是其它回呼函数(Callback)。这将涉及到resumeRequest()的使用。resumeRequest()会将MIDlet从停止状态回到运作状态,并调用startApp()。由于当时MIDlet处于停止状态,所以必须依靠异步事件使MIDlet重新回到运作状态。
在一些范例程序中,我们常常只呼叫notifyDestroyed(),而没有呼叫destroyApp(),这是因为范例通常没有释放资源的需求,所以可以不用呼叫。但是如果是正规的程序,建议记得呼叫destroyApp(false)会比较好。
总结
通过本文的讨论,相信大家都可以发现MIDP更成熟了,功能更强了,但是也更复杂了。
附注:本文中出现的Java程序代码已在Java 2 SDK 1.4.x与J2ME Wireless Toolkit 2.0的Win32版本上完成测试。本文所有操作皆在Windows 2000 Professional与Windows XP Professional中文版操作系统上经过测试。本文的范例程序代码请至Http://www.javatwo.net/experts/moli/publish/下载。
名词解释
MIDP: Java API中面向移动终端的集合。通过与J2ME中的面向移动终端产品配置CLDC配合使用,就能够提供J2ME应用程序所需的运行环境。主要用于手机、PDA及双向寻呼机等低价位移动信息终端。
MIDlet: 即一个可以执行的J2ME/MIDP应用程序基本单位。除了继承自javax.microedition.midlet.MIDlet之外,还包括让此类别可以顺利执行的所有其它类别和资源档(只要是非类别档都称作资源档)所构成的集合,所以一般又称做MIDlet应用程序(MIDlet Application)。
MIDlet Suite: 许多MIDlet所构成的集合一般又叫做MIDlet应用程序套件。MIDlet Suite和MIDlet的关系,就好像Office与Word、Excel、PowerPoint、Access的关系。
JAR档(.jar档): 实际上包含MIDlet Suite的档案,属于ZIP档格式。
描述档(.jad档): 用来描述一个MIDlet Suite基本数据,以及该MIDlet Suite内含的MIDlet内含的MIDlet相关信息(类别名称、图标、程序名)的外部档案(不在JAR档内部)。
应用程序管理员(Java Application Manager): 负责将MIDlet Suite安装到机器上执行,以及负责管理MIDlet生命周期的机制(或软件)总称。应用程序管理员会根据使用者的需求安装、启动、停止或移除相对应的MIDlet。