最近一段时间发现身边有一本买了一年多的J2ME的书没看,于是抱起来大概看了一下,我的E72是Symbian系统,支持J2ME,所以做了几个简单的小玩意。在学习J2ME开发的过程中有一些简单的心得和体会,现在写下来备忘,如果有朋友也正在学习J2ME开发,觉得有帮助的话,那是最好不过的事情了。
 
1.开发环境安装配置
目前开发J2ME应用的环境有多种,有基于NetBean的,不过仍以Eclipse为主流。所以本篇以在Eclipse下开发J2ME来介绍。
1.1下载JDK
因为这些软件本身是用Java开发的,所以首先需要去Java的官方网站下载JDK了,下载地址是:http://www.oracle.com/technetwork/java/javase/downloads/index.html,当然也可以在国内正规网站下载,这样可以获得比较快的下载速度。
1.2下载Eclipse
最开始Eclipse仅仅是针对Java的,现在有不同开发场景下的插件,如JavaScript、PHP、C\C++、Java SE、Java EE及Java ME等,地址是:http://www.eclipse.org/downloads/,这里选择“Pulsar for Mobile Developers”,其下载地址是:http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/helios/R/eclipse-pulsar-helios-win32.zip。这是一个Zip文件,下载完成后如果安装了JRE或者JDK即可双击Eclipse.exe使用。
 
2.插件安装配置
在进行J2ME开发中,个人觉得比较有用的插件如下:
properties文件的Eclipse插件:http://propedit.sourceforge.jp/eclipse/updates
Coffee Bytes Java Folding Eclipse插件:http://eclipse.realjenius.com/update-site 
ExploreFS插件: http://www.junginger.biz/eclipse/
MTJ插件: http://download.eclipse.org/mtj/updates/1.1.2/stable
安装插件的步骤如下(以安装在Eclipse for J2SE为例,在此基础上添加J2ME插件):找到Eclipse界面上的Help->Install new software…->Add,在”name”处输入” JavaME”,在处输入” http://www.eclipseme.org/updates/ ”,如下图:

J2ME开发的一般步骤_SDK

点击”OK”之后出现如下界面:
 

J2ME开发的一般步骤_ProGuard_02

选择要安装的部分,再点击”Finished”就可以自动安装相关插件。注意,视插件大小的不同,安装的时间也会不同,不过一般时间都相对较长。
为了避免中文乱码的问题,最好了解一下SDK对文件编码的支持程度,一般来说为了简单起见,都是将文件编码设置为“UTF-8”,如下所示:
 

J2ME开发的一般步骤_ProGuard_03

 
3.开发包安装配置
目前比较流行的开发包有WTK(Wireless Toolkits)及各支持J2ME的手机厂商自己推出的SDK。如果针对某一类型的手机做开发,可以直接去其官方网站下载SDK,像周公使用的是Nokia E72,它的SDK可以在Nokia的官方网站下载S60 Platform SDKs for Symbian OS, for Java™的开发包,下载地址是:http://www.forum.nokia.com/info/sw.nokia.com/id/6e772b17-604b-4081-999c-31f1f0dc2dbb/S60_Platform_SDKs_for_Symbian_OS_for_Java.html。
注:关于插件的安装及配置在周公的另一篇文章《WinXP下搭建适合Nokia开发的J2ME环境》中有比较详细的说明,网址是http://blog.csdn.net/zhoufoxcn/archive/2010/07/12/5728224.aspx
 
4.编码及测试
这里用来举例的是一个简单的例子,这是一个数字时钟,为此我们需要准备12个图片文件,分别用于时钟绘制、表盘及作为应用程序的图标。
项目体系结构如下:
 

J2ME开发的一般步骤_ProGuard_04

MainMIDlet.java的代码:
 
  1. package com.netskycn;  
  2. import javax.microedition.lcdui.Command;  
  3. import javax.microedition.lcdui.CommandListener;  
  4. import javax.microedition.lcdui.Display;  
  5. import javax.microedition.lcdui.Displayable;  
  6. import javax.microedition.midlet.MIDlet;  
  7. import javax.microedition.midlet.MIDletStateChangeException;  
  8. public class MainMIDlet extends MIDlet implements CommandListener{  
  9. private Display display;  
  10. private ImageCanvas canvas;  
  11. private Command cmdExit;  
  12. public MainMIDlet() {  
  13. super();  
  14. canvas=new ImageCanvas();  
  15. canvas.setTitle("周公数字时钟");  
  16. cmdExit=new Command("退出", Command.EXIT, 0);  
  17. canvas.addCommand(cmdExit);  
  18. canvas.setCommandListener(this);  
  19. }  
  20. protected void destroyApp(boolean arg0) throws MIDletStateChangeException {  
  21. // TODO Auto-generated method stub  
  22. }  
  23. protected void pauseApp() {  
  24. // TODO Auto-generated method stub  
  25. }  
  26. protected void startApp() throws MIDletStateChangeException {  
  27. display=Display.getDisplay(this);  
  28. display.setCurrent(canvas);  
  29. }  
  30. public void commandAction(Command cmd,Displayable d){  
  31. if (cmd==cmdExit) {  
  32. notifyDestroyed();  
  33. }  
  34. }  
ImageCanvas.java的代码:
 
  1. package com.netskycn;  
  2. import java.util.Calendar;  
  3. import javax.microedition.lcdui.Canvas;  
  4. import javax.microedition.lcdui.Graphics;  
  5. import javax.microedition.lcdui.Image;  
  6. public class ImageCanvas extends Canvas implements Runnable {  
  7. private Image[] p_w_picpaths;  
  8. private final int ImageCount=12;  
  9. private Calendar calendar=null;  
  10. //private byte hour,minute,second;  
  11. private int startX,startY;  
  12. private byte[] times=new byte[3];  
  13. public ImageCanvas(){  
  14. p_w_picpaths=new Image[ImageCount];  
  15. try {  
  16. for(int i=0;i<11;i++){  
  17. p_w_picpaths[i]=ZoomImage(Image.createImage("/p_w_picpaths/"+i+".JPG"),0.8d);  
  18. }  
  19. p_w_picpaths[11]=ZoomImage(Image.createImage("/p_w_picpaths/CLOCK.JPG"),0.8d);  
  20. startX=(getWidth()-p_w_picpaths[0].getWidth()*6-p_w_picpaths[10].getWidth()*2)/2;  
  21. startY=(getHeight()-p_w_picpaths[0].getHeight())/2;  
  22. catch (Exception e) {  
  23. System.out.println(e);  
  24. }  
  25. new Thread(this).start();  
  26. }  
  27. public void run() {  
  28. while (true) {  
  29. try{  
  30. repaint();  
  31. Thread.sleep(900);  
  32. }  
  33. catch (Exception e) {  
  34. System.out.println(e);  
  35. }  
  36. }  
  37. }  
  38. protected void paint(Graphics g) {  
  39. calendar=Calendar.getInstance();  
  40. // hour=(byte)calendar.get(Calendar.HOUR_OF_DAY);  
  41. // minute=(byte)calendar.get(Calendar.MINUTE);  
  42. // second=(byte)calendar.get(Calendar.SECOND);  
  43. times[0]=(byte)calendar.get(Calendar.HOUR_OF_DAY);//hour  
  44. times[1]=(byte)calendar.get(Calendar.MINUTE);//minute  
  45. times[2]=(byte)calendar.get(Calendar.SECOND);  
  46. g.drawImage(p_w_picpaths[11], (getWidth()-p_w_picpaths[11].getWidth())/2, (getHeight()-p_w_picpaths[11].getHeight())/2, Graphics.TOP|Graphics.LEFT);  
  47. int tenUnit,unit;  
  48. for(int i=0;i<times.length;i++){  
  49. tenUnit=times[i]/10;  
  50. unit=times[i]%10;  
  51. if(i==0){  
  52. g.drawImage(p_w_picpaths[tenUnit], startX, startY, Graphics.TOP|Graphics.LEFT);  
  53. g.drawImage(p_w_picpaths[unit], startX+p_w_picpaths[0].getWidth(), startY, Graphics.TOP|Graphics.LEFT);  
  54. g.drawImage(p_w_picpaths[10], startX+p_w_picpaths[0].getWidth()*2, startY+10, Graphics.TOP|Graphics.LEFT);  
  55. }  
  56. else if (i==1) {  
  57. g.drawImage(p_w_picpaths[tenUnit], startX+p_w_picpaths[0].getWidth()*2+p_w_picpaths[10].getWidth(), startY, Graphics.TOP|Graphics.LEFT);  
  58. g.drawImage(p_w_picpaths[unit], startX+p_w_picpaths[0].getWidth()*3+p_w_picpaths[10].getWidth(), startY, Graphics.TOP|Graphics.LEFT);  
  59. g.drawImage(p_w_picpaths[10], startX+p_w_picpaths[0].getWidth()*4+p_w_picpaths[10].getWidth(), startY+10, Graphics.TOP|Graphics.LEFT);  
  60. }  
  61. else {  
  62. g.drawImage(p_w_picpaths[tenUnit], startX+p_w_picpaths[0].getWidth()*4+p_w_picpaths[10].getWidth()*2, startY, Graphics.TOP|Graphics.LEFT);  
  63. g.drawImage(p_w_picpaths[unit], startX+p_w_picpaths[0].getWidth()*5+p_w_picpaths[10].getWidth()*2, startY, Graphics.TOP|Graphics.LEFT);  
  64. }  
  65. //g.drawImage(p_w_picpaths[tenUnit], startX, startY, Graphics.TOP|Graphics.LEFT);  
  66. }  
  67. //System.out.println("second="+times[2]);  
  68. }  
  69. /**  
  70.  * 图像缩放算法  
  71.  * @param src 原始图像  
  72.  * @param rate 图像的缩放比率,缩放后的图像高度和宽度都依照这个比率  
  73.  * @return  
  74.  */ 
  75. public static Image ZoomImage(Image src,double rate){  
  76. int destWidth=(int)(src.getWidth()*rate);  
  77. int destHeight=(int)(src.getHeight()*rate);  
  78. return ZoomImage(src, destWidth, destHeight);  
  79. }  
  80. /**  
  81.  * 图像缩放算法  
  82.  * @param src 原始图像  
  83.  * @param destW 缩放后的图像宽度  
  84.  * @param destH 缩放后的图像高度  
  85.  * @return  
  86.  */ 
  87. public static Image ZoomImage(Image src, int destW, int destH) {  
  88.     Image desImg = null;  
  89.     int srcW = src.getWidth();   
  90.     int srcH = src.getHeight();   
  91.     int[] srcBuf = new int[srcW * srcH];   
  92.     src.getRGB(srcBuf, 0, srcW, 00, srcW, srcH);  
  93.     int[] tabY = new int[destH];  
  94.     int[] tabX = new int[destW];  
  95.     int sb = 0;  
  96.     int db = 0;  
  97.     int tems = 0;  
  98.     int temd = 0;  
  99.       
  100.     int distance = srcH > destH ? srcH : destH;   
  101.       
  102.     for (int i = 0; i <= distance; i++) {   
  103.      tabY[db] = sb;  
  104.      tems += srcH;   
  105.      temd += destH;    
  106.      if (tems > distance) {  
  107.       tems -= distance;  
  108.       sb++;  
  109.      }  
  110.      if (temd > distance) {  
  111.       temd -= distance;  
  112.       db++;  
  113.      }  
  114.     }  
  115.     sb = 0;  
  116.     db = 0;  
  117.     tems = 0;  
  118.     temd = 0;  
  119.     distance = srcW > destW ? srcW : destW;   
  120.       
  121.     for (int i = 0; i <= distance; i++) {  
  122.      tabX[db] = (short) sb;  
  123.      tems += srcW;  
  124.      temd += destW;  
  125.      if (tems > distance) {  
  126.       tems -= distance;  
  127.       sb++;  
  128.      }  
  129.      if (temd > distance) {  
  130.       temd -= distance;  
  131.       db++;  
  132.      }  
  133.     }  
  134.     int[] desBuf = new int[destW * destH];  
  135.     int dx = 0;  
  136.     int dy = 0;  
  137.     int sy = 0;  
  138.     int oldy = -1;  
  139.     for (int i = 0; i < destH; i++) {  
  140.      if (oldy == tabY[i]) {  
  141.       System.arraycopy(desBuf, dy - destW, desBuf, dy, destW);  
  142.      } else {  
  143.       dx = 0;  
  144.       for (int j = 0; j < destW; j++) {  
  145.        desBuf[dy + dx] = srcBuf[sy + tabX[j]];  
  146.        dx++;  
  147.       }  
  148.       sy += (tabY[i] - oldy) * srcW;  
  149.      }  
  150.      oldy = tabY[i];  
  151.      dy += destW;  
  152.     }  
  153.     desImg = Image.createRGBImage(desBuf, destW, destH, false);  
  154.     return desImg;  
  155.  }  
由于今天要讲述的重点不是如何编写J2ME应用,而是这种讲述整个流程,所以这里不对代码做过多解释。
 
5.更改有关应用程序的信息
上面的代码能编译通过并且能在Symbian系列手机上运行,不过如果你就此将生成的jar包拷贝到手机上安装运行,会看到类似下面的效果:
 

J2ME开发的一般步骤_Eclipse_05

这是因为关于这个J2ME应用很多还是采用了默认设置,为了让我们的应用与众不同,我们应该做一些特有的设置,比如设置应用的logo及应用名称等,这些可以在Eclipse中很方便地设置。打开项目中的“Application Descriptor”文件,然后打开“Application Descriptor”选项卡,如下:
 

J2ME开发的一般步骤_ProGuard_06

此时这个文件的内容为:
MIDlet-Version: 1.0.0
MIDlet-Vendor: MIDlet Suite Vendor
MIDlet-Jar-URL: DigitalClock.jar
MicroEdition-Configuration: CLDC-1.1
MIDlet-1: MainMIDlet,,com.netskycn.MainMIDlet
MicroEdition-Profile: MIDP-2.0
MIDlet-Name: Zhou
我们把它改为:
MIDlet-1: 卡通时钟,/p_w_picpaths/logo.png,com.netskycn.MainMIDlet
MIDlet-Jar-URL: DigitalClock.jar
MIDlet-Icon: /p_w_picpaths/logo.png 
MicroEdition-Configuration: CLDC-1.1
MIDlet-Version: 1.0.0
MIDlet-Name: 卡通时钟
MIDlet-Description: 卡通时钟
MIDlet-Vendor: 周公
MicroEdition-Profile: MIDP-2.0
在上面的文本中红色加粗部分是新增的,仅仅标识为红色的是在原有基础上做的修改。这样一来我们的应用程序安装之后就可以显示我们指定的名称和图标了,如下图所示:
 

J2ME开发的一般步骤_ProGuard_07

6.代码混淆
上面的代码确实能够很好的运行,但是由于运行在虚拟机之上的语言都有一个特点,那就是因为比较高级的原因所以很容易被反编译,如果你的点子很好,人家可以通过反编译工具查看你的代码,“借鉴”或者篡改程序代码以达到不可告人的目的(这一点国内的*坝及一些所谓开源的操作系统和国产数据库做得很好,“借鉴”得相当成功,据说有的都进入“核高基”了)。作为公司和个人来说自然不希望出现这样的情况,避免这种情况有几种办法:加壳、加密和混淆。相对来说,混淆是副作用最小的一种,因而也被普遍接受。ProGuard是一款不错的开源混淆工具,它的下载地址是:http://proguard.sourceforge.net/。
将下载的文件解压到一个文件下,然后在Ecplise中做如下配置:
 

J2ME开发的一般步骤_Eclipse_08

7.应用程序打包
刚刚我们已经设置了ProGuard,的根路径,下面我们来将如何使用。首先选中要打包的项目,然后鼠标右键,选择“Export...”,这时候出现如下界面:
 

J2ME开发的一般步骤_ProGuard_09

选中“Export Midlet Package”然后点击“Next”,出现如下界面:
 

J2ME开发的一般步骤_J2ME_10

在上图中选中“Obfuscate the code”后,打包后的应用程序代码就被混淆了,混淆代码除了是代码更难懂之外,还可以一定程度上减少打包文件的体积。
总结:这是一个周公这几个星期来学习J2ME开发的一个总结,偏重于流程的介绍,关于如何设置应用程序安装后的问题和图标问题周公是费了很大心思才弄明白的。作为备忘,周公写了这篇文章,如果你当前也在学习J2ME开发的初级阶段,或许也能有一点参考价值。周公正在琢磨基于微博的应用,如果有兴趣,请在新浪微博上围观,地址是:http://weibo.com/zhoufoxcn
周公
2011-06-12