2006年04月12日

原文链接:http://www.newwz.com/zblog/post/134.html


    从10月在0110的大S论坛里看到Flash Lite的资料以来,到现在已经有好几个月的时间了,也许当时已经掀起了一股研究Flash Lite的高潮,可惜那段时间我很忙,没留意这个方面….等到12月底,我因为有做Flash Lite软件的创意(也就是现在做出来的MyBattery),开始真正动手研究Flash Lite,才发现这个东西是如此精彩~ 我如痴如醉的寻找相关的资料,高手做的Flash Lite主题或者软件的源文件,沉醉于其中~并努力去更新自己的小软件MyBattery….每次版本的更新,伴随着软件功能的增加和完善,我对于Flash Lite的熟悉也与日俱增…但是随着我搜寻范围的扩大,我也发现,国内对Flash Lite所给予的热情还太少,尤其在于开发方面~ 本来由于Flash Lite是一个新鲜事物,知道的人不怎么多,用的人要少一些,而做的人就更少的可怜了!我想借这篇文章,能够让大家对Flash Lite有所了解,看的更全面一些,更希望能够带来玩Flash Lite的第二高潮!

    有些人可能会说,Flash Lite能做什么?不就是全屏主题吗?我只要会用就行了…还有些人可能一直以为Flash Lite是用来播放电脑上的flash动画的,所以一直没有关注…在这里,我要颠覆大家的这种错误观念!

    Flash Lite能做什么?Flash Lite可以分为两个部分来看,一个部分是手机端的播放器,也就是Flash Player,但是受限于手机的处理能力和硬件特点,这个播放器和电脑上的有很大不同,因此有与之对应的创作工具,实际上从flash MX 2004开始就已经提供了对Flash Lite的支持.按照Flash Lite的标准做出来的flash,就能够在手机端的Flash Player里打开并且播放.不知道大家对电脑上的flash是怎么样的一个印象?估计很多人听到flash,首先浮现在脑海中的会是一些经典的flash动画(比如大话三国系列,小小的动作系列等),那么,我可以告诉你,你的观念落后了!实际上,flash早就已经逐步转变为一种强大的开发工具,它不仅仅可以制作流畅的动画,而且可以用来开发各种功能强大的软件,游戏!而手机上的Flash Lite虽然功能弱了一些,但是还是很强大的,当然,同样可以开发软件和游戏!

那么,让我们来看看几个精彩的东西吧!


类似QQ的IM软件



漂亮的计时器



电视节目预告



发短信的工具


有些朋友可能会说,这些Flash Lite软件不是有SIS的类似软件吗?也许他们还更加方便和好用~那么,Flash Lite的优势在哪里呢?这里请允许我摘录一下李易修做的几点小结,以及我对这些优势的看法:
1.比原有开发流程快3-5倍
我不知道这里所指的原有开发流程是什么,估计是S60软件的开发流程.我同样不知道S60软件是如何被开发出来的,但是我用flash做过不少课件,它们当中的某些功能也很强大,和用其他程序开发工具创作的软件不相上下,那么,我想,Flash Lite开发的流程也不会有多么复杂.至少,入门肯定比做S60的软件要容易一些~

2.一致性的体验
由于Flash Lite的标准是由MacroMedia公司(现在已经被Adobe公司收购)所制定的,在统一的标准下,而且是同样的播放器,那么所获得的效果也是一致的.这样就是说,你对于开发者而言,只要开发一次,就能够在所有支持Flash Lite的手机上运行,而不必针对手机型号进行多次开发.而对于用户来说,你只要确保你的手机支持Flash Lite,并且你下载的是Flash Lite标准编写(创作)的swf文件,那么就可以确保它们在你的手机上正确的运行,而不必去寻找对应手机型号的版本!这种一致性,无论对于开发者还是用户所带来的便捷,是勿庸置疑的!

3.简易的开发工具
我不知道有多少人有电脑上的flash开发经验,也许我可以算是其中一个.那么对于这类人来说,在同样一个创作平台下的开发,难度会降低不少,你所需要做的,仅仅是一个适应的过程!从原有电脑上的鼠标键盘交互,到现在手机上仅仅0-9,*,#和方向键的交互.还有也许你已经用习惯的点语法,它们在Flash Lite中不被支持!不管怎么说,这些总比从头开始学习要容易的多了~ 而对于从未接触过flash开发的朋友,如果你只想做一些简单的东西,比如主题,那么也会很容易上手的~ 稍后也许我会抽空写一个教程~

事物总是有正反两个方面,Flash Lite也有它不可避免的劣势
目前来说,我觉得比较严重的,比较影响我做开发的,有这么几点:
1.在Flash Lite1.1中,对中文的支持不怎么好.主要体现在动态文本如果是中文的话,会是乱码!真是头疼,我只能尽量把文本打散,而保留其中的数字和英文部分.这样同时会增加最终输出文件的大小~
2.能够载入的外部图片资料有限制!据说是50K,总之这样的限制很不舒服,也决定了在主题中动态载入背景是多么容易引起程序崩溃的一种操作!
3.熟悉的点语法不能用了!尤其对于我这种习惯使用点语法的,现在又要回归flash 4的写法,真是很麻烦的一件事,但是又无可奈何.
4.Flash Lite仍然不适合作复杂的应用,包括商务和娱乐方面,从安全机制,存储能力,网络连接等层面,Flash Lite都比较薄弱,而且可能难以改进.幸好我现在的水平还无法支持我做这种程度的开发 -_-!

以上就Flash Lite,谈了一些我自己个人的见解,当然其中还引用了部分前辈的内容
撰写此文的目的,就在于希望能够吸引更多的人来关注,来研究Flash Lite,让它在我们的手机上大放光彩!

【背景资料】
什么是Flash lite?
Flash Lite 是专门为移动电话开发的 Macromedia Flash 配置文件。Flash Lite 已获得日本消费者和开发人员广泛采用,而且现在正在快速为日本以外地区运营商和制造商所采用。

此增长由功能强大的 Flash 再现引擎所推动,此引擎在不同操作系统、处理器和屏幕大小之间提供一致的体验。这受到世界各地强大的 Flash 开发人员群体支持。最初的反馈很清楚:Flash 显著加快用于移动电话的精彩内容和界面的开发。

Flash Lite 为手机提供了以下功能:

■ 基于 HTTP 的数据连接
■ SVG 支持*(播放)
■ 对电话功能的扩展访问
■ 新的内容开发套件 (CDK)
■ 向量图形表现
■ 位图图像
■ 条纹背景
■ 声音-事件音效
■ 静态文本(字体嵌入在 SWF 文件中)
■ 输入和动态文本(字体嵌入在 SWF 文件中)
■ 输入和动态设备文本
■ 基于帧的动画
■ 变形动画-动作和形状的改变
■ ActionScript(脚本)-支持 Flash 4 脚本
■ 键盘导航

Flash lite1.1和Flash lite2.0
在浏览器中搜索Flash Lite,可以发现大量的Flash Lite2.0的新闻报道,随着2.0的正式推出,闹的真是风风火火,让我们来看看2.0有哪些新的改变:

摘录luar的一段文字

1.Unicode:动态文字可以展示中文,在我们华文地区来说,非常重要!
2.Mobile Shared Object (MSO):即PC上LSO,可以储存资料到手机,不用再为游戏储存等问题寻求其它Fscommand Flash2File的解决方法。注意:MSO只在Standalone Mode支持,如果Flash内容是在Browser Mode、Messaging Application或者Nokia File Manager,MSO是不支持的。
3.可以LoadMovie加载图像档案,不用再寻求JPG2SWF等工具在Server先转换,由于Decode能力借助手机支持,所以可以加载JPG/GIF/PNG,不支持Animated GIF/PNG Animation,由于要知道手机是否支持该格式,所以有System.capabilities的ActionScript来侦测。
4.同样道理,loadSound支持动态加载MP3/MIDI,也是靠手机支持。
5.同样道理,Video可以播放3gp,其实也是靠手机支持,3gp影像一定盖在Flash内容上面,至于Embed Video in SWF也可以,不过File Size这么大,可行吗?
6.Stage Object可以知道SWF画面大小,onResize也支持,是否代表可以用一个SWF通吃不同手机画面大小?
7.SetInputTextType,可以限制Input TextField为Numeric, Alpha, Alphanumeric, Latin, NonLatin and NoRestriction。
8.ExtendBacklightDuration,控制手机光亮时间(如果手机支持的话),感动!

尤其让我喜欢的是动态文字可以使用中文而不必担心乱码和MSO,可以保存设置了~ 另外2.0又可以使用点语法了~

可惜需要告诉大家的是,大S中能够使用的Flash lite是1.1的.我发现2.0的需要Symbian 7.0以上的才能安装,目前没有大S能用的版本~ 非常遗憾~

Flash Lite

Flash Lite Macromedia公司为了行动电话开发的 Flash Player。于20032月发布,Flash Lite 1.0支持 Flash 5的对象和Flash 4 ActionScript, 也能够播放MIDI音乐。 2004年初Flash Lite 升级到1.1版,加强了网络连接、数据交换能力,支持的声音格式包含MP3 PCMADPCM SMAF,使其更适合发展多媒体应用程序。

    由于受到手机运算能力、内存、屏幕的限制,现有的Flash Lite player对于网络上流行的flash动画文件很多都不支持,但是Flash Lite的主要功能并不是要让我们可以浏览动画,而是是一个类似J2ME一样新的手机开发平台。

Flash Lite 在手机上情况

2003年日本NTT DoCoMo 公司推出的的505i是第一款支持Flash Lite的行动电 话。之后的每一支 i-mode 手机都内建了 Flash Lite 设定档,可以 浏览内嵌了Flash内容的 i-mode服务、以 Flash动画作为待机图案。目前已经有 超过 1000 家的公司提供了以 Flash 为基础的 i-mode 服务,内容包含游戏、交通 信息、气象、互动地图、动画铃声等等。

2004Macromedia公司推出了可以在Symbian或是MS Smartphone手机上可以使用的flash lite1.1播放器,同时,也和三星电子以及NOKIA达成了软件授权协议,以后这两家公司生产的手机将会内置flash lite播放器。现有的一些支持Flash Lite的手机

更多支持Flash Lite的手机大家可以通过这个网址查看:http://www.macromedia.com/mobile/supported_devices/

 

让我们使用flash lite来制作一个简单的手机应用程序

    使用flash lite很大的一个好处就是相对JAVA来说入门简单,怎么简单呢?下边我们就通过一个例子来向大家说明,一个非专业人员也可以轻松的来开发一个基于手机的小应用程序。

大家都知道N-gageN-gage QD76503650S60手机都是不支持主题功能的,看着别人S70手机上那些酷酷的主题,是不是很郁闷?别急,有了flash lite,让我们为自己的N-gageN-gage QD加入令人心动的主题功能,而且要更玄。

这个主题的基本原理就是通过flash lite里的按钮,来调用手机里对应的程序。就像电脑里的快捷方式一样。那接下来我们进入正题吧!(本文需要您对flash 2004以及一些基本的图像处理软件具有一定的基础)

准备工作
开始制作FlashLite内容之前,要先准备好下面这些东西:
• Flash MX 2004 Professional
• FlashLite 1.1 CDK
或者
Macromedia Flash MX 2004 7.2更新

FlashLite 1.1 CDK可以在Macromedia 网站上免费下载。http://www.macromedia.com/devnet/devices/development_kits.html

Macromedia Flash MX 2004 7.2更新也可以在Macromedia 网站上免费下载。http://www.macromedia.com/cn/software/flash/special/7_2updater/

建议安装Macromedia Flash MX 2004 7.2更新,虽然容量大了一些,但是更新了很多内容,而且安装过程比较简单,这里就不介绍了。

详细说一下安装CDK所需要的步骤 (如果你已经安装过Flash 7.2 update就可以跳过这个步骤。)
1.
下载完后解开压缩档,把Flash_Lite_1.1_CDKFlash_Lite_authoring_updater目录下的FlashLite1_1.dll,拷贝到下面的位置。
• Windows:
C:\Program Files\Macromedia\Flash MX 2004\language\Configuration\Players
• Mac OS X:
Macintosh HD::Applications:Macromedia Flash MX 2004:Configuration:Players

2.拷贝FlashLite1_1.xml到下面的位置。
• Windows:
C:\Program Files\Macromedia\Flash MX 2004\language\Configuration\Players
• Mac OS X:
Macintosh HD::Applications:Macromedia Flash MX 2004:Configuration:Players

3.拷贝DeviceMsg.cfg到下面的位置。
• Windows 2000/ WindowsXP:
C:\Documents and Settings\user name\Local Settings\Application Data\Macromedia\ Flash MX 2004\language\Configuration\
• Windows 98(SE):
C:\Windows\Profiles\user name\Application Data\Macromedia\Flash MX 2004\language\Configuration\
• Macintosh:
Macintosh HD::Users:user name:Library:Application Support:Macromedia:Flash MX 2004:language:Configuration:

要在手机上测试,还需要以下这两样:
支援Flash Lite的手机一只,Symbian60及以上的都可以。包括6600n-gageQD7610等等。
• FlashLite Player1.1
FlashLite Player1.1
前一段时间是可以免费申请的,但是现在不可以了,只能在Macromedia的网络商店购买,价格是10美元。地址如下http://www.macromedia.com/cfusion/store/index.cfm?store=OLS-US#view=ols_prod&loc=en_us&store=OLS-US&categoryOID=1141004&distributionOID=103

1、  要做FlashLite文件,可以从模板建立Create from Template开始。Flash MX 2004在发布的时候,已经内建了一些手机的模板Template,不过种类有点少。有国外网友自己做了一些Symbian S60手机的Template,可以到http://www.flash-lite.de/downloads.html下载。
打开Flash,建立新文件fileànew,选择模板templates、选择移动设备mobile devices,然后随便选择nokia 3650 7650都可以。(和你的手机不一样没关系)

2、  删除最上边的一层,这层是用作模版御览用的,对我们的工作没有用处。

3、  选择Properties里的设置Setting按钮

4、  在发布选项内把版本Version选择为Flash lite 1.1(默认为Flash lite 1.0,其他无需改动

5、  在时间轴中建立3个层,actions层(用来放置程序)、按钮层(用来放置按钮)、背景层(用来放置主题的背景)。

6、  接下来我们准备一张漂亮的图片作为主题的背景,大小要求宽176像素、高208像素,正好和我们的手机屏幕分辨率一样,格式要求是JPG或是PNG的(图片可以用你LPGF的,也许会有意外惊喜呀),以上可以采用一些图像处理软件来完成,在这里我准备了一张红色的中国龙的图片。

7、  选择背景层的第一帧,然后选择文件fileà导入importà导入到场景import to stage
然后选择已经做好的那张图片,确定,正确的话应该会是这个样子。这就是放到手机上的样子

8、  下一步,我们开始制作上边的按钮,我们可以使用一些现成的图标,像是XP本身的一些图标,您也可以去网上找到一些更好看的图标,然后使用图像处理软件,将其制作为背景色透明的PNG格式文件(推荐使用Firework软件)。为了知道当前是选择了哪一个按钮,要再给每一个图标制作一个略有不同的图片,可以采用一下外发光或阴影滤镜都可以。下边是我们做好的三个按钮。上边是普通状态,下边是该按钮被选中的样子。

9、  接下来我们回到flash中,选择插入Insertànew symbal,然后选择按钮button,名字就叫“联系人”就好。

10、              在新建立的“联系人”按钮中的UP这一帧上,我们选择文件fileà导入importà导入到场景import to stage,然后选择我们刚才创建的联系人的普通图标,完成后应该是这个样子。

11、              然后选择Over这一帧,按“F7”键,这样可以在Over这一帧建立一个新的关键帧,然后重复上边的文件fileà导入importà导入到场景import to stage,选择我们刚才创建的联系人的被选中图标,这样当“联系人”这个按钮被选中是就会显示刚才制作的被选中图标。

12、              现在我们已经完成了一个按钮,回到主场景。选择左上方的Scene 1

13、              我们刚才制作的按钮哪里去了?不要着急,它已经在我们的库文件里了。打开库文件的工具栏Windowsàlibrary,看看是不是有一个按钮库文件叫联系人?

14、              先选中时间轴中的按钮那一层,然后在库文件中把它选中,拖到主场景中,位置你可以随便放置。

15、              好,我们的第一个按钮就已经都做好了,现在我们为它加上功能,写一段非常简单的程序。先选中这个按钮,然后打开动作actions面板。写上如下内容:
on (release) {
getURL("file:///Z:/System/Apps/Phonebook/Phonebook.app");
}
意思也就是当这个按钮被按下然后抬起的时候,执行手机上Z:/System/Apps/Phonebook/Phonebook.app这个程序,对
Symbian手机比较了解得朋友可能已经看出来了,这就是手机上的联系人程序。

16、              按照上边的步骤,我们再添加两个按钮,分别是“电话”和“短消息”,程序代码分别是:
电话:on (release) {
getURL("file:///Z:/System/Apps/Phone/Phone.app");
}
短消息:on (release) {
getURL("file:///Z:/System/Apps/mce/mce.app");
}

17、              然后选择时间轴上的actions层,打开Actions面板,加入如下程序(//后为注释,可以不写):
fscommand2("FullScreen", true);//
当打开的时候自动全屏幕播放
fscommand2("SetQuality", "low");//
播放时细节设置为高级
_focusrect = false;//
不显示当前焦点框,不写的话当按钮被选中就会有一个黄色的框框,不好看。
stop();//
停止

18、              好,我们自己的第一个手机程序就要搞定了,先保存一下,然后同时按下”Ctrl”+”Enter”键,我们就可以看到主题运行的样子(此时使用Flash内建的Flash Lite模拟器来运行,有些情况下可能和在手机上运行有不同),然后选择文件fileà导出Exportà导出为动画Export Movie,找一个目录,起一个自己喜欢的名字。确认,然后弹出版本选择,因为我们前边已经作了选择,这里直接点确认OK就好了。

19、              接下来的事对于大家来说就简单了,和其他应用程序一样,把我们刚才制作的扩展名为swf的文件传到我们的手机上边,放在e:\Documents\flash目录下(flash lite安装后会自动建立)。然后运行flash lite player1.1,选择我们制作的文件,看,效果如何?你还可以加入更多的按钮和功能。

20、              Flash lite本身还有一些和手机的接口函数,利用这些函数可以得到手机的很多信息,包括当前电量、信号置、支持什么铃声等等,利用这些,就可以做出更多更好东西,笔者在这个主题的基础上制作了一个能显示手机电量、信号、以及时间的主题。在这里限于篇幅,就不做更多的介绍了,感兴趣的朋友可以自己查阅flash lite的相关文档,都在flash lite CDK中,也可以到我的网站上来一起讨论。

 

制作和测试Flash lite需要注意的一些问题

FlashLite模拟器和手机上播放同样档案的结果很可能不同,比如手机上的播放速度可能会较慢,或是图片出不来。所以最好边做边测,才不会白费功夫。有时候在手机上播放Flash的时候,会出现错误讯息例如:Problem with content: 6,这代表JPEG图档太大了。以下是错误讯息的意义。
1. Out of memory
(内存不够,是你的flash文件过大,现在flash lite对文件的大小限制是1M以下)
2. Stack limit reached
(堆栈超过限制)
3. Corrupt SWF data
(错误的swf数据)
4. ActionScript stuck
(代码错误)
5. Infinite AS loop
(代码有死循环)
6. Bad JPEG data
(错误的JPEG数据)
7. Bad sound data
(错误的Sound数据)
8. Cannot find host
(没有找到主机)
9. ActionScript error
(代码错误)
10. URL too long
(网址过长)

图形处理

毕竟手机的运算效能还不够强,尽量让图形简单一点,少用点渐层、半透明,必要时把向量图转为位图,才能确保播放的效果和你的预期相同。

Flash lite 的优势和前景

虽然现在来说Flash Lite功能相比J2ME还过于单薄。支持的手机还不是很多,但是它还是存在一些优势的:

开发人员数目庞大

全球有750000Flash开发人员,国内的“闪客”也绝对是为数不少

标准统一

    Flash Lite Player,任何安装了Flash Lite Player的设备都可以播放Flash Lite文件而不需要加以编译修改。而J2ME的基础规范虽然比较统一,但是各手机厂家的兼容性又有不同,经常可以看到NOKIA可以用的程序在索爱上就用不了。

开发简单、周期短

会用Flash的人只要经过短期的学习甚至不需要学习就可以直接制作开发基于手机的各种Flash动画、应用软件等等。

支持矢量格式,在设计制作动画、用户界面方面拥有得天独厚的优势

大家都知道Flash是一种矢量格式,也就是可以和屏幕的分辨率无关,而Flash Lite也是矢量格式,同时还支持SVG(也是一种矢量格式),在现在各种移动设备屏幕大小不一、分辨率多种多样的情况下,他的这种优势非常明显。而且对于制作动画方面,Flash要比逐桢绘图的J2ME拥有更多的优势。

FlashCast

FlashCast 是推播 Flash Lite内容的服务平台。20047 T-Mobile在英国、德国和奥地利推出以FlashLite为基础的News Express服务。News Express服务每天推播两次新闻、娱乐、气象、运动信息等多媒体内容到用户的手机上。

总体说来,Flash LiteJ2ME两者都具备自身的优势和劣势,这种优势和劣势也导致了两者必将在不同的领域发挥作用。

    下边是使用Flash Lite编写的一些手机应用程序,更多的Flash Lite应用大家可以通过这个网址来下载:
http://www.macromedia.com/cfusion/exchange/index.cfm#loc=en_us&view=sn310&viewName=Flash%20Lite%20Exchange

结语

由于Macromedia Flash是目前网络上最广泛使用的技术之一,进入门坎相对较低。开发效率也较其它技术快3 5倍。已经熟析Flash的开发人员,不需要学习对特定装置的技术,就能够把开发的内容部署在手机、PDA等不同的移动设备上。非常适合作为多媒体应用程序的行动平台。

Flash在手机方面的应用目前仍是处于刚起步的阶段,手机上Flash Lite的占有率远低于JavaFlash Lite的服务范围现在还仅限于日本、德国、英国、奥地利等地。但随着Macromedia和三星、NOKIA等手机厂商的签约,以及和一些内容服务商的合作或许在不久的将来,就能在中国看到Flash Lite 提供的各种服务。如果Flash Lite在手机上的的占有率能够提升到一定的比例、内建的 ActionScript 能够升级、功能与效能提升,Flash Lite应该能够成为一种新兴的手机应用平台。

相关信息

作者网站:www.flashfuture.net

MM公司手机网站:mobile.macromedia.com

  Flash Lite 是专门用于为移动电话和消费电子设备进行开发的Flash技术。现在 Flash Lite 2 已经发布了。让我们来看看它都有些什么新特色、新功能:

  1、Flash Player 7 支持

  Flash Lite 2 基于 Flash 7 内容创作标准。这意味着在最新 Flash 创作环境中已开发的内容,能够为移动设备和消费电子设备进行更动(re-purposed )。

Flash Lite 2 基于 Flash 7 内容创作标准

  2、动态 XML 数据

  Flash Lite 2 支持在 Flash 内容中,使用与 Flash Player 7 相同的处理方法加载和解析外部XML数据。

支持动态 XML 数据

  3、数据传递(Persistent Data)

  Flash Lite 2 支持本地存储和返回相应的,特定应用程序信息,例如参数设置,最高分,用户名等等。这将提供一个更加强大的开发环境。

数据传递

  4、强大和动态的媒体

  Flash Lite 2 基于设备所支持的可用编码器,允许动态加载多媒体内容,例如图像、声音和视频。这包括加载和处理XML数据和SWF内容。Flash Lite 2 提供视频支持和外部多媒体支持。

视频回放基于设备可用的编码解码器

  5、文本增强

  Flash Lite 2 允许用户在程序运行时修改文本颜色、大小以及其他属性,提供改进的显示和文字处理。OEM商也能够选择嵌入矢量字体到他们用于渲染文本的应用程序中。Flash Lite 2还支持对象阿拉伯语这样从右到左的语言进行文本渲染。

增强的文本显示和处理功能

  6、ActionScript 绘图 API

  Flash Lite 2允许开发者在程序运行时使用ActionScript,简便的创建矢量图像和动态形状。

ActionScript 绘图 API

  7、ActionScript 2.0 支持

  Flash Lite 2 支持基于ECMA 262 的标准 ActionScript 2.0。Flash Lite 内容能够使用现代事件模型(影片剪辑和对象事件)、tab index control、绘图API以及更好的SWF压缩器完成开发创作。

  8、同步设备声音

  Flash Lite 2 允许内容开发者使用设备中特定的音频格式例如MIDI, SMAF等音频数据同步动画。

  9、压缩SWF文件

  Flash Lite 2 能够渲染被内容开发者使用Flash创作工具压缩过的 SWF 文件。在开始处理和渲染数据前,Flash Lite 2 将会解压 SWF 文件。

2006年04月10日

 

  世界上的各地区都有本地的语言。地区差异直接导致了语言环境的差异。在开发一个国际化程序的过程中,处理语言问题就显得很重要了。

  这是一个世界范围内都存在的问题,所以,Java提供了世界性的解决方法。本文描述的方法是用于处理中文的,但是,推而广之,对于处理世界上其它国家和地区的语言同样适用。

  汉字是双字节的。所谓双字节是指一个双字要占用两个BYTE的位置(即16位),分别称为高位和低位。中国规定的汉字编码为GB2312,这是强制性的,目前几乎所有的能处理中文的应用程序都支持GB2312。GB2312包括了一二级汉字和9区符号,高位从0xa1到0xfe,低位也是从0xa1到0xfe,其中,汉字的编码范围为0xb0a1到0xf7fe。

  另外有一种编码,叫做GBK,但这是一份规范,不是强制的。GBK提供了20902个汉字,它兼容GB2312,编码范围为0×8140到0xfefe。GBK中的所有字符都可以一一映射到Unicode 2.0。

  在不久的将来,中国会颁布另一种标准:GB18030-2000(GBK2K)。它收录了藏、蒙等少数民族的字型,从根本上解决了字位不足的问题。注意:它不再是定长的。其二字节部份与GBK兼容,四字节部分是扩充的字符、字形。它的首字节和第三字节从0×81到0xfe,二字节和第四字节从0×30到0×39。

  本文不打算介绍Unicode,有兴趣的可以浏览“http://www.unicode.org/”查看更多的信息。Unicode有一个特性:它包括了世界上所有的字符字形。所以,各个地区的语言都可以建立与Unicode的映射关系,而Java正是利用了这一点以达到异种语言之间的转换。

  在JDK中,与中文相关的编码有:

  表1 JDK中与中文相关的编码列表


编码名称 说明
ASCII 7位,与ascii7相同
ISO8859-1 8-位,与 8859_1,ISO-8859-1,ISO_8859-1,latin1…等相同
GB2312-80 16位,与gb2312,gb2312-1980,EUC_CN,euccn,1381,Cp1381, 1383, Cp1383, ISO2022CN,ISO2022CN_GB…等相同
GBK 与MS936相同,注意:区分大小写
UTF8 与UTF-8相同
GB18030 与cp1392、1392相同,目前支持的JDK很少


  在实际编程时,接触得比较多的是GB2312(GBK)和ISO8859-1。

  为什么会有“?”号

  上文说过,异种语言之间的转换是通过Unicode来完成的。假设有两种不同的语言A和B,转换的步骤为:先把A转化为Unicode,再把Unicode转化为B。

  举例说明。有GB2312中有一个汉字“李”,其编码为“C0EE”,欲转化为ISO8859-1编码。步骤为:先把“李”字转化为Unicode,得到“674E”,再把“674E”转化为ISO8859-1字符。当然,这个映射不会成功,因为ISO8859-1中根本就没有与“674E”对应的字符。

  当映射不成功时,问题就发生了!当从某语言向Unicode转化时,如果在某语言中没有该字符,得到的将是Unicode的代码“\uffffd”(“\u”表示是Unicode编码,)。而从Unicode向某语言转化时,如果某语言没有对应的字符,则得到的是“0×3f”(“?”)。这就是“?”的由来。

  例如:把字符流buf =“0×80 0×40 0xb0 0xa1”进行new String(buf, "gb2312")操作,得到的结果是“\ufffd\u554a”,再println出来,得到的结果将是“?啊”,因为“0×80 0×40”是GBK中的字符,在GB2312中没有。

  再如,把字符串String="\u00d6\u00ec\u00e9\u0046\u00bb\u00f9"进行new String (buf.getBytes("GBK"))操作,得到的结果是“3fa8aca8a6463fa8b4”,其中,“\u00d6”在“GBK”中没有对应的字符,得到“3f”,“\u00ec”对应着“a8ac”,“\u00e9”对应着“a8a6”,“0046”对应着“46”(因为这是ASCII字符),“\u00bb”没找到,得到“3f”,最后,“\u00f9”对应着“a8b4”。把这个字符串println一下,得到的结果是“?ìéF?ù”。看到没?这里并不全是问号,因为GBK与Unicode映射的内容中除了汉字外还有字符,本例就是最好的明证。

  所以,在汉字转码时,如果发生错乱,得到的不一定都是问号噢!不过,错了终究是错了,50步和100步并没有质的差别。

  或者会问:如果源字符集中有,而Unicode中没有,结果会如何?回答是不知道。因为我手头没有能做这个测试的源字符集。但有一点是肯定的,那就是源字符集不够规范。在Java中,如果发生这种情况,是会抛出异常的。

什么是UTF

  UTF,是Unicode Text Format的缩写,意为Unicode文本格式。对于UTF,是这样定义的:

  (1)如果Unicode的16位字符的头9位是0,则用一个字节表示,这个字节的首位是“0”,剩下的7位与原字符中的后7位相同,如“\u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(与源Unicode字符是相同的);

  (2)如果Unicode的16位字符的头5位是0,则用2个字节表示,首字节是“110”开头,后面的5位与源字符中除去头5个零后的最高5位相同;第二个字节以“10”开头,后面的6位与源字符中的低6位相同。如“\u025d”(0000 0010 0101 1101),转化后为“c99d”(1100 1001 1001 1101);

  (3)如果不符合上述两个规则,则用三个字节表示。第一个字节以“1110”开头,后四位为源字符的高四位;第二个字节以“10”开头,后六位为源字符中间的六位;第三个字节以“10”开头,后六位为源字符的低六位;如“\u9da7”(1001 1101 1010 0111),转化为“e9b6a7”(1110 1001 1011 0110 1010 0111);

  可以这么描述JAVA程序中Unicode与UTF的关系,虽然不绝对:字符串在内存中运行时,表现为Unicode代码,而当要保存到文件或其它介质中去时,用的是UTF。这个转化过程是由writeUTF和readUTF来完成的。

  好了,基础性的论述差不多了,下面进入正题。

  先把这个问题想成是一个黑匣子。先看黑匣子的一级表示:

input(charsetA)->process(Unicode)->output(charsetB)

  简单,这就是一个IPO模型,即输入、处理和输出。同样的内容要经过“从charsetA到unicode再到charsetB”的转化。

  再看二级表示:

SourceFile(jsp,java)->class->output

  在这个图中,可以看出,输入的是jsp和java源文件,在处理过程中,以Class文件为载体,然后输出。再细化到三级表示:

jsp->temp file->class->browser,os console,db

app,servlet->class->browser,os console,db

  这个图就更明白了。Jsp文件先生成中间的Java文件,再生成Class。而Servlet和普通App则直接编译生成Class。然后,从Class再输出到浏览器、控制台或数据库等。

  JSP:从源文件到Class的过程

  Jsp的源文件是以“.jsp”结尾的文本文件。在本节中,将阐述JSP文件的解释和编译过程,并跟踪其中的中文变化。

  1、JSP/Servlet引擎提供的JSP转换工具(jspc)搜索JSP文件中用<%@ page contentType ="text/html; charset=<Jsp-charset>"%>中指定的charset。如果在JSP文件中未指定<Jsp-charset>,则取JVM中的默认设置file.encoding,一般情况下,这个值是ISO8859-1;

  2、jspc用相当于“javac –encoding <Jsp-charset>”的命令解释JSP文件中出现的所有字符,包括中文字符和ASCII字符,然后把这些字符转换成Unicode字符,再转化成UTF格式,存为JAVA文件。ASCII码字符转化为Unicode字符时只是简单地在前面加“00”,如“A”,转化为“\u0041”(不需要理由,Unicode的码表就是这么编的)。然后,经过到UTF的转换,又变回“41”了!这也就是可以使用普通文本编辑器查看由JSP生成的JAVA文件的原因;

  3、引擎用相当于“javac –encoding UNICODE”的命令,把JAVA文件编译成CLASS文件;

  先看一下这些过程中中文字符的转换情况。有如下源代码:


<%@ page contentType="text/html; charset=gb2312"%>
<html><body>
<%
 String a="中文";
 out.println(a);
%>
</body></html>


  这段代码是在UltraEdit for Windows上编写的。保存后,“中文”两个字的16进制编码为“D6 D0 CE C4”(GB2312编码)。经查表,“中文”两字的Unicode编码为“\u4E2D\u6587”,用 UTF表示就是“E4 B8 AD E6 96 87”。打开引擎生成的由JSP文件转变而成的JAVA文件,发现其中的“中文”两个字确实被“E4 B8 AD E6 96 87”替代了,再查看由JAVA文件编译生成的CLASS文件,发现结果与JAVA文件中的完全一样。

  再看JSP中指定的CharSet为ISO-8859-1的情况。


<%@ page contentType="text/html; charset=ISO-8859-1"%>
<html><body>
<%
 String a="中文";
 out.println(a);
%>
</body></html>


  同样,该文件是用UltraEdit编写的,“中文”这两个字也是存为GB2312编码“D6 D0 CE C4”。先模拟一下生成的JAVA文件和CLASS文件的过程:jspc用ISO-8859-1来解释“中文”,并把它映射到Unicode。由于ISO-8859-1是8位的,且是拉丁语系,其映射规则就是在每个字节前加“00”,所以,映射后的Unicode编码应为“\u00D6\u00D0\u00CE\u00C4”,转化成UTF后应该是“C3 96 C3 90 C3 8E C3 84”。好,打开文件看一下,JAVA文件和CLASS文件中,“中文”果然都表示为“C3 96 C3 90 C3 8E C3 84”。

  如果上述代码中不指定<Jsp-charset>,即把第一行写成“<%@ page contentType="text/html" %>”,JSPC会使用file.encoding的设置来解释JSP文件。在RedHat 6.2上,其处理结果与指定为ISO-8859-1是完全相同的。

  到现在为止,已经解释了从JSP文件到CLASS文件的转变过程中中文字符的映射过程。一句话:从“JspCharSet到Unicode再到UTF”。下表总结了这个过程:

  表2 “中文”从JSP到CLASS的转化过程


Jsp-CharSet JSP文件中 JAVA文件中 CLASS文件中
GB2312 D6 D0 CE C4(GB2312) 从\u4E2D\u6587(Unicode)到E4 B8 AD E6 96 87 (UTF) E4 B8 AD E6 96 87 (UTF)
ISO-8859-1 D6 D0 CE C4
(GB2312)
从\u00D6\u00D0\u00CE\u00C4 (Unicode)到C3 96 C3 90 C3 8E C3 84 (UTF) C3 96 C3 90 C3 8E C3 84 (UTF)
无(默认=file.encoding) 同ISO-8859-1 同ISO-8859-1 同ISO-8859-1


  下节先讨论Servlet从JAVA文件到CLASS文件的转化过程,然后再解释从CLASS文件如何输出到客户端。之所以这样安排,是因为JSP和Servlet在输出时处理方法是一样的。

Servlet:从源文件到Class的过程

  Servlet源文件是以“.java”结尾的文本文件。本节将讨论Servlet的编译过程并跟踪其中的中文变化。

  用“javac”编译Servlet源文件。javac可以带“-encoding <Compile-charset>”参数,意思是“用< Compile-charset >中指定的编码来解释Serlvet源文件”。

  源文件在编译时,用<Compile-charset>来解释所有字符,包括中文字符和ASCII字符。然后把字符常量转变成Unicode字符,最后,把Unicode转变成UTF。

  在Servlet中,还有一个地方设置输出流的CharSet。通常在输出结果前,调用HttpServletResponse的setContentType方法来达到与在JSP中设置<Jsp-charset>一样的效果,称之为<Servlet-charset>。

  注意,文中一共提到了三个变量:<Jsp-charset>、<Compile-charset>和<Servlet-charset>。其中,JSP文件只与<Jsp-charset>有关,而<Compile-charset>和<Servlet-charset>只与Servlet有关。

  看下例:


import javax.servlet.*;

import javax.servlet.http.*;

class testServlet extends HttpServlet
{
 public void doGet(HttpServletRequest req,HttpServletResponse resp)
 throws ServletException,java.io.IOException
 {
  resp.setContentType("text/html; charset=GB2312");
  java.io.PrintWriter out=resp.getWriter();
  out.println("<html>");
  out.println("#中文#");
  out.println("</html>");
 }
}


  该文件也是用UltraEdit for Windows编写的,其中的“中文”两个字保存为“D6 D0 CE C4”(GB2312编码)。

  开始编译。下表是<Compile-charset>不同时,CLASS文件中“中文”两字的十六进制码。在编译过程中,<Servlet-charset>不起任何作用。<Servlet-charset>只对CLASS文件的输出产生影响,实际上是<Servlet-charset>和<Compile-charset>一起,达到与JSP文件中的<Jsp-charset>相同的效果,因为<Jsp-charset>对编译和CLASS文件的输出都会产生影响。

  表3 “中文”从Servlet源文件到Class的转变过程


Compile-charset Servlet源文件中 Class文件中 等效的Unicode码
GB2312 D6 D0 CE C4
(GB2312)
E4 B8 AD E6 96 87 (UTF) \u4E2D\u6587 (在Unicode中=“中文”)
ISO-8859-1 D6 D0 CE C4
(GB2312)
C3 96 C3 90 C3 8E C3 84 (UTF) \u00D6 \u00D0 \u00CE \u00C4 (在D6 D0 CE C4前面各加了一个00)
无(默认) D6 D0 CE C4 (GB2312) 同ISO-8859-1 同ISO-8859-1


  普通Java程序的编译过程与Servlet完全一样。

  CLASS文件中的中文表示法是不是昭然若揭了?OK,接下来看看CLASS又是怎样输出中文的呢?

  Class:输出字符串

  上文说过,字符串在内存中表现为Unicode编码。至于这种Unicode编码表示了什么,那要看它是从哪种字符集映射过来的,也就是说要看它的祖先。这好比在托运行李时,外观都是纸箱子,里面装了什么就要看寄邮件的人实际邮了什么东西。

  看看上面的例子,如果给一串Unicode编码“00D6 00D0 00CE 00C4”,如果不作转换,直接用Unicode码表来对照它时,是四个字符(而且是特殊字符);假如把它与“ISO8859-1”进行映射,则直接去掉前面的“00”即可得到“D6 D0 CE C4”,这是ASCII码表中的四个字符;而假如把它当作GB2312来进行映射,得到的结果很可能是一大堆乱码,因为在GB2312中有可能没有(也有可能有)字符与00D6等字符对应(如果对应不上,将得到0×3f,也就是问号,如果对应上了,由于00D6等字符太靠前,估计也是一些特殊符号,真正的汉字在Unicode中的编码从4E00开始)。

  各位看到了,同样的Unicode字符,可以解释成不同的样子。当然,这其中有一种是我们期望的结果。以上例而论,“D6 D0 CE C4”应该是我们所想要的,当把“D6 D0 CE C4”输出到IE中时,用“简体中文”方式查看,就能看到清楚的“中文”两个字了。(当然了,如果你一定要用“西欧字符”来看,那也没办法,你将得不到任何有何时何地的东西)为什么呢?因为“00D6 00D0 00CE 00C4”本来就是由ISO8859-1转化过去的。

给出如下结论:

  在Class输出字符串前,会将Unicode的字符串按照某一种内码重新生成字节流,然后把字节流输入,相当于进行了一步“String.getBytes(???)”操作。???代表某一种字符集。

  如果是Servlet,那么,这种内码就是在HttpServletResponse.setContentType()方法中指定的内码,也就是上文定义的<Servlet-charset>。

  如果是JSP,那么,这种内码就是在<%@ page contentType=""%>中指定的内码,也就是上文定义的<Jsp-charset>。

  如果是Java程序,那么,这种内码就是file.encoding中指定的内码,默认为ISO8859-1。

  当输出对象是浏览器时

  以流行的浏览器IE为例。IE支持多种内码。假如IE接收到了一个字节流“D6 D0 CE C4”,你可以尝试用各种内码去查看。你会发现用“简体中文”时能得到正确的结果。因为“D6 D0 CE C4”本来就是简体中文中“中文”两个字的编码。

  OK,完整地看一遍。

  JSP:源文件为GB2312格式的文本文件,且JSP源文件中有“中文”这两个汉字

  如果指定了<Jsp-charset>为GB2312,转化过程如下表。

  表4 Jsp-charset = GB2312时的变化过程


序号 步骤说明 结果
1 编写JSP源文件,且存为GB2312格式 D6 D0 CE C4
(D6D0=中 CEC4=文)
2 jspc把JSP源文件转化为临时JAVA文件,并把字符串按照GB2312映射到Unicode,并用UTF格式写入JAVA文件中 E4 B8 AD E6 96 87
3 把临时JAVA文件编译成CLASS文件 E4 B8 AD E6 96 87
4 运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码 4E 2D 65 87(在Unicode中4E2D=中 6587=文)
5 根据Jsp-charset=GB2312把Unicode转化为字节流 D6 D0 CE C4
6 把字节流输出到IE中,并设置IE的编码为GB2312(作者按:这个信息隐藏在HTTP头中) D6 D0 CE C4
7 IE用“简体中文”查看结果 “中文”(正确显示)


  如果指定了<Jsp-charset>为ISO8859-1,转化过程如下表。

  表5 Jsp-charset = ISO8859-1时的变化过程


序号 步骤说明 结果
1 编写JSP源文件,且存为GB2312格式 D6 D0 CE C4
(D6D0=中 CEC4=文)
2 jspc把JSP源文件转化为临时JAVA文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式写入JAVA文件中 C3 96 C3 90 C3 8E C3 84
3 把临时JAVA文件编译成CLASS文件 C3 96 C3 90 C3 8E C3 84
4 运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码 00 D6 00 D0 00 CE 00 C4
(啥都不是!!!)
5 根据Jsp-charset=ISO8859-1把Unicode转化为字节流 D6 D0 CE C4
6 把字节流输出到IE中,并设置IE的编码为ISO8859-1(作者按:这个信息隐藏在HTTP头中) D6 D0 CE C4
7 IE用“西欧字符”查看结果 乱码,其实是四个ASCII字符,但由于大于128,所以显示出来的怪模怪样
8 改变IE的页面编码为“简体中文” “中文”(正确显示)


  奇怪了!为什么把<Jsp-charset>设成GB2312和ISO8859-1是一个样的,都能正确显示?因为表4表5中的第2步和第5步互逆,是相互“抵消”的。只不过当指定为ISO8859-1时,要增加第8步操作,殊为不便。

  再看看不指定<Jsp-charset> 时的情况。

  表6 未指定Jsp-charset 时的变化过程


序号 步骤说明 结果
1 编写JSP源文件,且存为GB2312格式 D6 D0 CE C4
(D6D0=中 CEC4=文)
2 jspc把JSP源文件转化为临时JAVA文件,并把字符串按照ISO8859-1映射到Unicode,并用UTF格式写入JAVA文件中 C3 96 C3 90 C3 8E C3 84
3 把临时JAVA文件编译成CLASS文件 C3 96 C3 90 C3 8E C3 84
4 运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码 00 D6 00 D0 00 CE 00 C4
5 根据Jsp-charset=ISO8859-1把Unicode转化为字节流 D6 D0 CE C4
6 把字节流输出到IE中 D6 D0 CE C4
7 IE用发出请求时的页面的编码查看结果 视情况而定。如果是简体中文,则能正确显示,否则,需执行表5中的第8步


  Servlet:源文件为JAVA文件,格式是GB2312,源文件中含有“中文”这两个汉字

  如果<Compile-charset>=GB2312,<Servlet-charset>=GB2312

  表7 Compile-charset=Servlet-charset=GB2312 时的变化过程


序号 步骤说明 结果
1 编写Servlet源文件,且存为GB2312格式 D6 D0 CE C4
(D6D0=中 CEC4=文)
2 用javac –encoding GB2312把JAVA源文件编译成CLASS文件 E4 B8 AD E6 96 87 (UTF)
3 运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码 4E 2D 65 87 (Unicode)
4 根据Servlet-charset=GB2312把Unicode转化为字节流 D6 D0 CE C4 (GB2312)
5 把字节流输出到IE中并设置IE的编码属性为Servlet-charset=GB2312 D6 D0 CE C4 (GB2312)
6 IE用“简体中文”查看结果 “中文”(正确显示)


  如果<Compile-charset>=ISO8859-1,<Servlet-charset>=ISO8859-1

  表8 Compile-charset=Servlet-charset=ISO8859-1时的变化过程


序号 步骤说明 结果
1 编写Servlet源文件,且存为GB2312格式 D6 D0 CE C4
(D6D0=中 CEC4=文)
2 用javac –encoding ISO8859-1把JAVA源文件编译成CLASS文件 C3 96 C3 90 C3 8E C3 84 (UTF)
3 运行时,先从CLASS文件中用readUTF读出字符串,在内存中的是Unicode编码 00 D6 00 D0 00 CE 00 C4
4 根据Servlet-charset=ISO8859-1把Unicode转化为字节流 D6 D0 CE C4
5 把字节流输出到IE中并设置IE的编码属性为Servlet-charset=ISO8859-1 D6 D0 CE C4 (GB2312)
6 IE用“西欧字符”查看结果 乱码(原因同表5)
7 改变IE的页面编码为“简体中文” “中文”(正确显示)


  如果不指定Compile-charset或Servlet-charset,其默认值均为ISO8859-1。

  当Compile-charset=Servlet-charset时,第2步和第4步能互逆,“抵消”,显示结果均能正确。读者可试着写一下Compile-charset<>Servlet-charset时的情况,肯定是不正确的。

  当输出对象是数据库时

  输出到数据库时,原理与输出到浏览器也是一样的。本节只是Servlet为例,JSP的情况请读者自行推导。

  假设有一个Servlet,它能接收来自客户端(IE,简体中文)的汉字字符串,然后把它写入到内码为ISO8859-1的数据库中,然后再从数据库中取出这个字符串,显示到客户端。

  表9 输出对象是数据库时的变化过程(1)


序号 步骤说明 结果
1 在IE中输入“中文” D6 D0 CE C4 IE
2 IE把字符串转变成UTF,并送入传输流中 E4 B8 AD E6 96 87
3 Servlet接收到输入流,用readUTF读取 4E 2D 65 87(unicode) Servlet
4 编程者在Servlet中必须把字符串根据GB2312还原为字节流 D6 D0 CE C4
5 编程者根据数据库内码ISO8859-1生成新的字符串 00 D6 00 D0 00 CE 00 C4
6 把新生成的字符串提交给JDBC 00 D6 00 D0 00 CE 00 C4
7 JDBC检测到数据库内码为ISO8859-1 00 D6 00 D0 00 CE 00 C4 JDBC
8 JDBC把接收到的字符串按照ISO8859-1生成字节流 D6 D0 CE C4
9 JDBC把字节流写入数据库中 D6 D0 CE C4
10 完成数据存储工作 D6 D0 CE C4 数据库
以下是从数据库中取出数的过程
11 JDBC从数据库中取出字节流 D6 D0 CE C4 JDBC
12 JDBC按照数据库的字符集ISO8859-1生成字符串,并提交给Servlet 00 D6 00 D0 00 CE 00 C4 (Unicode)  
13 Servlet获得字符串 00 D6 00 D0 00 CE 00 C4 (Unicode) Servlet
14 编程者必须根据数据库的内码ISO8859-1还原成原始字节流 D6 D0 CE C4  
15 编程者必须根据客户端字符集GB2312生成新的字符串 4E 2D 65 87
(Unicode)
 
Servlet准备把字符串输出到客户端
16 Servlet根据<Servlet-charset>生成字节流 D6D0 CE C4 Servlet
17 Servlet把字节流输出到IE中,如果已指定<Servlet-charset>,还会设置IE的编码为<Servlet-charset> D6 D0 CE C4
18 IE根据指定的编码或默认编码查看结果 “中文”(正确显示) IE


  解释一下,表中第4第5步和第15第16步是用红色标记的,表示要由编码者来作转换。第4、5两步其实就是一句话:“new String(source.getBytes("GB2312"), "ISO8859-1")”。第15、16两步也是一句话:“new String(source.getBytes("ISO8859-1"), "GB2312")”。亲爱的读者,你在这样编写代码时是否意识到了其中的每一个细节呢?

  至于客户端内码和数据库内码为其它值时的流程,和输出对象是系统控制台时的流程,请读者自己想吧。明白了上述流程的原理,相信你可以轻松地写出来。

  行文至此,已可告一段落了。终点又回到了起点,对于编程者而言,几乎是什么影响都没有。

  因为我们早就被告之要这么做了。

  以下给出一个结论,作为结尾。

  1、 在Jsp文件中,要指定contentType,其中,charset的值要与客户端浏览器所用的字符集一样;对于其中的字符串常量,不需做任何内码转换;对于字符串变量,要求能根据ContentType中指定的字符集还原成客户端能识别的字节流,简单地说,就是“字符串变量是基于<Jsp-charset>字符集的”;

  2、 在Servlet中,必须用HttpServletResponse.setContentType()设置charset,且设置成与客户端内码一致;对于其中的字符串常量,需要在Javac编译时指定encoding,这个encoding必须与编写源文件的平台的字符集一样,一般说来都是GB2312或GBK;对于字符串变量,与JSP一样,必须“是基于<Servlet-charset>字符集的”。

一、术语session
在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。

session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。有时候我们可以看到这样的话“在一个浏览器会话期间,…”,这里的会话一词用的就是其本义,是指从一个浏览器窗口打开到关闭这个期间①。最混乱的是“用户(客户端)在一次会话期间”这样一句话,它可能指用户的一系列动作(一般情况下是同某个具体目的相关的一系列动作,比如从登录到选购商品到结账登出这样一个网上购物的过程,有时候也被称为一个transaction),然而有时候也可能仅仅是指一次连接,也有可能是指含义①,其中的差别只能靠上下文来推断②。

然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义,“面向连接”指的是在通信双方在通信之前要先建立一个通信的渠道,比如打电话,直到对方接了电话通信才能开始,与此相对的是写信,在你把信发出去的时候你并不能确认对方的地址是否正确,通信渠道不一定能建立,但对发信人来说,通信已经开始了。“保持状态”则是指通信的一方能够把一系列的消息关联起来,使得消息之间可以互相依赖,比如一个服务员能够认出再次光临的老顾客并且记得上次这个顾客还欠店里一块钱。这一类的例子有“一个TCP session”或者“一个POP3 session”③。

而到了web服务器蓬勃发展的时代,session在web开发语境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候session也用来指这种解决方案的存储结构,如“把xxx保存在session里”⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持,所以在某种特定语言的语境下,session也被用来指代该语言的解决方案,比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。

鉴于这种混乱已不可改变,本文中session一词的运用也会根据上下文有不同的含义,请大家注意分辨。
在本文中,使用中文“浏览器会话期间”来表达含义①,使用“session机制”来表达含义④,使用“session”表达含义⑤,使用具体的“HttpSession”来表达含义⑥

二、HTTP协议与状态保持
HTTP协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。

然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。

让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案:
1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。
2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。
3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。

由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。

三、理解cookie机制
cookie机制的基本原理就如上面的例子一样简单,但是还有几个问题需要解决:“会员卡”如何分发;“会员卡”的内容;以及客户如何使用“会员卡”。

正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。

而cookie的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示,如果某家分店还发行了自己的会员卡,那么进这家店的时候除了要出示麦当劳的会员卡,还要出示这家店的会员卡。

cookie的内容主要包括:名字,值,过期时间,路径和域。
其中域可以指定某一个域比如.google.com,相当于总店招牌,比如宝洁公司,也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com,可以用飘柔来做比。
路径就是跟在域名后面的URL路径,比如/或者/foo等等,可以用某飘柔专柜做比。
路径与域合在一起就构成了cookie的作用范围。
如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。

存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。

下面就是一个goolge设置cookie的响应头的例子
HTTP/1.1 302 Found
Location: http://www.google.com/intl/zh-CN/
Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Content-Type: text/html


这是使用HTTPLook这个HTTP Sniffer软件来俘获的HTTP通讯纪录的一部分


浏览器在再次访问goolge的资源时自动向外发送cookie


使用Firefox可以很容易的观察现有的cookie的值
使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。


IE也可以设置在接受cookie前询问


这是一个询问接受cookie的对话框。

四、理解session机制
session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 – 称为session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。

保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是JSESSIONID。

由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://…../xxx;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
另一种是作为查询字符串附加在URL后面,表现形式为http://…../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。
为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。

另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如下面的表单





在被传递给客户端之前将被改写成







这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。
实际上这种技术可以简单的用对action应用URL重写来代替。

在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。

恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。

五、理解javax.servlet.http.HttpSession
HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。

首先,Weblogic Server提供了一系列的参数来控制它的HttpSession的实现,包括使用cookie的开关选项,使用URL重写的开关选项,session持久化的设置,session失效时间的设置,以及针对cookie的各种设置,比如设置cookie的名字、路径、域,cookie的生存时间等。

一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用,Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。

复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。

cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。

cookie的路径对于web应用程序来说是一个非常重要的选项,Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。

关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869

六、HttpSession常见问题
(在本小节中session的含义为⑤和⑥的混合)


1、session在何时被创建
一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。

由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。

2、session何时被删除
综合前面的讨论,session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;或c.服务器进程被停止(非持久session)

3、如何做到在浏览器关闭时删除session
严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。

4、有个HttpSessionListener是怎么回事
你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有HttpSessionBindingListener,HttpSessionActivationListener和HttpSessionAttributeListener。

5、存放在session中的对象必须是可序列化的吗
不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在Weblogic Server的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。

6、如何才能正确的应付客户端禁止cookie的可能性
对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL,具体做法参见[6]
http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770

7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session
参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。

8、如何防止用户打开两个浏览器窗口操作导致的session混乱
这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。

9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue
做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变,需要向其他服务器进程复制新的session值。

10、为什么session不见了
排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。
出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。

七、跨应用程序的session共享

常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。

然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。

首先来看一下Tomcat是如何实现web应用程序之间session的隔离的,从Tomcat设置的cookie路径来看,它对不同的应用程序设置的cookie路径是不同的,这样不同的应用程序所用的session id是不同的,因此即使在同一个浏览器窗口里访问不同的应用程序,发送给服务器的session id也可以是不同的。

根据这个特性,我们可以推测Tomcat中session的内存结构大致如下。

笔者以前用过的iPlanet也采用的是同样的方式,估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器,解决的思路很简单,实际实行起来也不难。要么让所有的应用程序共享一个session id,要么让应用程序能够获得其他应用程序的session id。

iPlanet中有一种很简单的方法来实现共享一个session id,那就是把各个应用程序的cookie路径都设为/(实际上应该是/NASApp,对于应用程序来讲它的作用相当于根)。

/NASApp

需要注意的是,操作共享的session应该遵循一些编程约定,比如在session attribute名字的前面加上应用程序的前缀,使得setAttribute("name", "neo")变成setAttribute("app1.name", "neo"),以防止命名空间冲突,导致互相覆盖。


在Tomcat中则没有这么方便的选择。在Tomcat版本3上,我们还可以有一些手段来共享session。对于版本4以上的Tomcat,目前笔者尚未发现简单的办法。只能借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段。

我们再看一下Weblogic Server是如何处理session的。

从截屏画面上可以看到Weblogic Server对所有的应用程序设置的cookie的路径都是/,这是不是意味着在Weblogic Server中默认的就可以共享session了呢?然而一个小实验即可证明即使不同的应用程序使用的是同一个session,各个应用程序仍然只能访问自己所设置的那些属性。这说明Weblogic Server中的session的内存结构可能如下

对于这样一种结构,在session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段,还有一种较为方便的做法,就是把一个应用程序的session放到ServletContext中,这样另外一个应用程序就可以从ServletContext中取得前一个应用程序的引用。示例代码如下,

应用程序A
context.setAttribute("appA", session);

应用程序B
contextA = context.getContext("/appA");
HttpSession sessionA = (HttpSession)contextA.getAttribute("appA");

值得注意的是这种用法不可移植,因为根据ServletContext的JavaDoc,应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值,以上做法在Weblogic Server 8.1中通过。

那么Weblogic Server为什么要把所有的应用程序的cookie路径都设为/呢?原来是为了SSO,凡是共享这个session的应用程序都可以共享认证的信息。一个简单的实验就可以证明这一点,修改首先登录的那个应用程序的描述符weblogic.xml,把cookie路径修改为/appA访问另外一个应用程序会重新要求登录,即使是反过来,先访问cookie路径为/的应用程序,再访问修改过路径的这个,虽然不再提示登录,但是登录的用户信息也会丢失。注意做这个实验时认证方式应该使用FORM,因为浏览器和web服务器对basic认证方式有其他的处理方式,第二次请求的认证不是通过session来实现的。具体请参看[7] secion 14.8 Authorization,你可以修改所附的示例程序来做这些试验。

八、总结
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。

关于作者:
郎云鹏(dev2dev ID: hippiewolf),软件工程师,从事J2EE开发
电子邮件:langyunpeng@yahoo.com.cn
地址:大连软件园路31号科技大厦A座大连博涵咨询服务有限公司

参考文档:
[1] Preliminary Specification http://wp.netscape.com/newsref/std/cookie_spec.html
[2] RFC2109 http://www.rfc-editor.org/rfc/rfc2109.txt
[3] RFC2965 http://www.rfc-editor.org/rfc/rfc2965.txt
[4] The Unofficial Cookie FAQ http://www.cookiecentral.com/faq/
[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869
[6] http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770
[7] RFC2616 http://www.rfc-editor.org/rfc/rfc2616.txt

代码下载:sampleApp.rar

 

常有人问及MD5算法为何有些程序片断返回完全数字型结果而有些返回数字与字母的混合字串。

其实两种返回结果只是因为加密结果的不同显示形式,Blog中已经有.Net的实现,在此附加JAVA实现,供参考。

JAVA的标准类库理论上功能也很强大,但由于虚拟机/运行时的实现太多,加之版本差异,有些代码在不同环境下运行会出现奇怪的异常结果,尤其以涉及字符集的操作为甚。

package com.bee.framework.common;

import java.security.MessageDigest;

public class MD5Encrypt {
public MD5Encrypt() {
}

private final static String[] hexDigits = {
"0", "1", "2", "3", "4", "5", "6", "7",
"8", "9", "a", "b", "c", "d", "e", "f"};

/**
* 转换字节数组为16进制字串
* @param b 字节数组
* @return 16进制字串
*/
public static String byteArrayToString(byte[] b) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++) {
//resultSb.append(byteToHexString(b[i]));//若使用本函数转换则可得到加密结果的16进制表示,即数字字母混合的形式
resultSb.append(byteToNumString(b[i]));//使用本函数则返回加密结果的10进制数字字串,即全数字形式
}
return resultSb.toString();
}

private static String byteToNumString(byte b) {

int _b = b;
if (_b < 0) {
_b = 256 + _b;
}

return String.valueOf(_b);
}

private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}

public static String MD5Encode(String origin) {
String resultString = null;

try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
resultString =
byteArrayToString(md.digest(resultString.getBytes()));
}
catch (Exception ex) {

}
return resultString;
}

public static void main(String[] args) {
MD5Encrypt md5encrypt = new MD5Encrypt();
System.out.println(MD5Encode("10000000"));
}
}


在无线通信领域有两项重大的技术应用,一个是宽带CDMA技术,一个是IP技术。前者使无线通信获得更多带宽,3G的目标是静态速率达到2Mbps,而现在的2.5G技术(如国内正在建设的GPRS和CDMA2000 1X)都可达到115kbps的速率;后者使无线通信和因特网融合起来,实际上当前WAP2.0技术就采用已经很成熟的TCP、TLS和HTTP协议,而移动IP和IPv6技术将促使更多设备接入互联网,如各种设备、家电等。

  就是在这样的环境下J2ME技术得以快速发展,J2ME用来为接入和即将接入互联网的设备提供应用开发平台和执行环境,本文则主要论述如何利用JBuilder进行快速的J2ME应用开发。

  1. J2ME分层结构及MIDP简表

  J2ME是SUN的JAVA2平台微型版(JAVA2平台3个版本之一,另两个是标准版J2SE和企业版J2EE),采用3层结构设计。最低层为配置层(Configuration),包括虚拟机(VM)和类库两部分,这一层与设备层(硬件及操作系统)关系紧密,由SUN提供参考规范和源代码,设备厂商根据设备特征进行相应的移植。当前J2ME提供有两个配置:连接设备配置(CDC)和有限连接设备配置(CLDC)。前者主要面向有较大内存和处理能力而只需有限功能的设备,如电视置顶盒、冰箱、汽车导航设备等,这些设备都有连接网络和相互交互的需求,其内存在2M以上;后者主要面向对内存和处理能力有较大限制的手持设备,如现在使用的手机、PDA等,现在及将来大多数这些设备都已经能够接入互联网,其内存范围160Kb(其中128Kb用于虚拟机及类库,至少32Kb用于CLDC规范所要求的应用程序堆栈空间)到2M。二者主要区别在于能提供给VM及应用程序的存储空间,CDC所用VM及类库实际是参考J2SE标准,去除了不需要的功能如AWT。CLDC所用VM(称作KVM)则有较大区别,包括类验证机制。对于下一代移动终端设备如智能电话、高端PDA,则是CDC所应用对象,因为它们将有超过2M的内存。

  设备层之上是简表层(Profile),再之上则是应用层(Application)。简表层扩展了配置层功能为上层应用提供API,如果说配置层面向设备,简表层则面向应用。可以根据需要在CDC或CLDC基础之上提供多种简表,一个配置层之上也可以有多个简表。当前CDC之上有基础简表(Foundation Profile)和基于FP的Personal Profile和RMI Profile。CLDC之上则主要提供有移动信息设备简表(MIDP),即用于手机、PDA等移动终端的设备简表,提供API以支持无线应用的开发。

  CLDC类库一部分来自J2SE,这部分类库是经过裁减的,去除了不必要的功能,主要包括java.lang包中的系统类、数据类型类、异常处理类,java.util包的集合类、时间类和附加工具类,java.io包的I/O处理类。CLDC专有类则主要是"通用连接框架(GCF)",为CLDC提供网络连接功能,这些网络接口都是Connection类的子类,由类Connector所提供的方法调用,这些接口或类位于包javax.microedition.io。

  CDC类库则是CLDC的超集,因此为CLDC开发的应用程序可以移植到CDC平台,由于CDC采用标准的J2SE VM,因此其开发与标准的J2SE开发一致,只是在用javac工具编译源代码时需要使用CDC的类库,即使用-bootclasspath参数指向CDC类库。

  MIDP扩展了CLDC的功能,它继承了GCF并在此基础上增加了类HttpConnection,用以提供HTTP连接功能(尽管从理论上CLDC/MIDP可以提供socket、数据报、文件、NFS等多种连接类型,但现在标准的CLDC/MIDP仅支持HTTP协议,一些设备实现则提供了socket和HTTPS协议的支持),MIDP类库

  总结如下:

  java.io、java.lang、java.util,属于MIDP的核心包,分别用来提供系统I/O、语言支持和工具支持。包中的类来自CLDC并稍有增加,但都来自J2SE。

  javax.microedition.midlet,定义了MIDP应用程序,以及应用程序和它所运行于环境之间的交互。

  javax.microedition.lcdui,为MIDP应用程序提供用户界面API。

  javax.microedition.rms,用来为MIDlet提供持久存储的机制,应用程序可以存储数据,在以后需要的时候获取这些数据。

  javax.microedition.io,提供了基于CLDC通用连接框架的网络支持。


2. MIDP应用程序开发

  这里讨论的J2ME无线应用开发主要是基于CLDC/MIDP的开发,其应用程序可运行于移植有KVM的手机、PDA等,这类设备由MIDP定义,即移动信息设备(MID),可看作一垂直应用市场。

  可在PC(Windows、UNIX或Linux平台)上开发MIDP应用程序,编译成类文件形式,下载到目标设备上,经过类文件的验证(验证是否有不符合KVM规范的方法调用等)后即可解释执行。

  生成的.java文件至少有一个是扩展了javax.microedition.midlet.MIDlet类的子类,并且实现几个规定的接口,比如下面的一个MIDlet程序 :


import javax.microedition.midlet.*; //应用程序生命周期,和J2SE一样,包java.lang.*是默认加载的
import javax.microedition.lcdui.*; //MIDP用户界面
public class FirstMIDlet extends MIDlet implements CommandListener {
private Display display; // 引用MIDlet的Display 对象
private TextBox textBox; // Textbox 显示一条消息
private Command cmdExit; // 设定按钮用于退出MIDlet
public FirstMIDlet() { // MIDlet构造程序
display = Display.getDisplay(this);
cmdExit = new Command("Exit", Command.SCREEN, 1);
textBox = new TextBox("My First MIDlet", "Hello, J2ME!", 50, 0);
textBox.addCommand(cmdExit);
textBox.setCommandListener(this);
}
public void startApp() { // 必须要实现的接口,被应用管理软件调用来启动MIDlet
display.setCurrent(textBox);
}
public void pauseApp() { } // 必须要实现的接口
public void destroyApp(boolean unconditional) { } //必须要实现的接口
public void commandAction(Command c, Displayable d) { //检查一下是否选择了退出命令
if (c == cmdExit) {
destroyApp(false);
notifyDestroyed();
}
}
}

  这是最简单且功能完整的MIDlet应用程序,其中接口startApp、pauseApp、destroyApp是必须要实现的,应用程序管理器(JAM)通过这些接口调用和控制应用程序,与Applet类似。除这个主类外还可以有其它辅助类,其要求和J2SE一致。

然后使用javac工具编译,javac工具来自J2SE,至少JDK 1.3版,编译时应该使用参数-bootclasspath并指向MIDP类库。之后使用preverify工具预验证,以保证生成的.class文件符合CLDC要求,这一步为每个类文件添加堆栈映射(stackmap,此属性为CLDC新定义)属性,增加类文件大小约5%。

  此时可以用midp工具(来自MIDP实现的可执行文件)模拟运行,之后即可打包,需要编写一清单文件(清单文件参考CLDC/MIDP规范),用jar工具(来自J2SE)把预验证后的类文件、资源文件、清单文件打包。

  要发布应用程序还需要编写一描述文件(JAD),JAD的要求同清单文件一致,可以自定义参数供应用程序调用。其中一个参数MIDlet-Jar-URL以URL方式指向JAR文件,移动终端设备通过JAM连接网络获取JAD,下载MIDlet-Jar-URL指向的JAR到设备中,通常JAM要先判断JAD与清单文件是否一致,应用程序是否有效,然后才决定下载。

  运行时执行环境、JAR包、JAD文件和应用程序生命周期,这些要素构成MIDlet套件,由JAM管理,每个MIDlet套件可包含一个或多个MIDlet。

  上面是一个MIDP应用程序开发的关键点,具体的开发方法及开发工具大致分为如下几种:

  Ⅰ. 基本开发工具

  最基本的开发方式是下载SUN提供的CLDC/MIDP参考实现及源代码,可根据需求编译生成一个CLDC/MIDP实现,并移植到设备上。配置好开发环境(CLDC/MIDP实现、JDK1.3)后即可采用上述方法和工具进行开发,多采用命令行方式,这是最原始的方法。

  Ⅱ. J2MEWTK开发工具

  这是SUN提供的便捷开发工具,用于Windows环境,同样需要先安装JDK1.3。源程序仍需要使用常规的文本编辑器,把编辑好的源文件及资源文件按一定要求放在规定目录下,J2MEWTK所提供的是菜单或按钮方式的命令。J2MEWTK中有相应的编译(和预验证一个步骤)、打包、模拟运行的菜单(或按钮),以及其它辅助工具。

  Ⅲ. IDE工具

  可以使用Forte For Java、JBuilder等IDE工具和J2MEWTK集成使用,它们除了有J2MEWTK的功能外,就是提供有可视化的开发工具。

3. 使用JBuilder MobileSet

  JBuilder是Java程序员常用工具之一,在于它强大的可视化编程工具,集成的编译、运行和调试环境。要为JBuilder提供J2ME无线应用开发支持,需要在JBuilder基础之上安装MobileSet,建议安装2.0版,当前支持CLDC/MIDP的1.0.3版本,而JBuilder的版本应该是6。JBuilder MobileSet 2可从下面网址免费下载:

  http://www.borland.com/jbuilder/mobileset/ 同时还要下载相应的MobileSet序列号和许可密钥(免费),根据提示安装MobileSet并注册后,运行JBuilder会找到"Help|MobileSet Guilde"菜单项。使用菜单"File->New…"打开"Object Gallery"对话框,会出现一个新的页面标签"Micro",此时图标"MIDlet"和"MIDP Displayable"是灰色的,只有生成一个MIDP项目后才能使用这两个图标。

  3.1 JBuilder MobileSet的特征

  JBuilder MobileSet是一个开放工具,能够与其它供应商提供的J2ME开发工具包集成在一起,当前提供了对下列厂商工具包的支持:

  Sun提供的J2ME无线开发工具包1.0.3版(J2MEWTK)

  诺基亚的J2ME开发套件(http://forum.nokia.com/)

  西门子的SMTK开发工具包(http://www.siemens-mobile.de/)

  其中J2MEWTK已经包含在MobileSet 2的安装文件中,如果要使用J2MEWTK提供的开发类库,并且还没有安装J2MEWTK,可以在安装MobileSet 2时选择完全安装。MobileSet 2提供了下列辅助开发的工具:
 
  用于CLDC/MIDP类的CodeInsight工具

  类/包浏览器

  JDK转换工具

  动态适配到任何J2ME简表,包括MIDP

  和开发其它Java应用程序一样,JBuilder通过MobileSet提供了快速开发模板,包括MIDP项目模板,MIDP Displayable模板,MIDlet模板。它提供了RAD(快速应用开发)的可视设计器,支持MIDP UI元素。通过MobileSet还支持MIDP应用程序打包和OTA配置(Over the Air,上载和下载文件和MIDlet套件,用来配置应用程序到设备上)。

  MobileSet能够用在JBuilder的个人版、专家版和企业版,但是一些功能不能用在个人版如JDK设置切换,打包器等。

  3.2 配置JDK

  前面已提到编译MIDP应用程序时需要设置特定的CLDC/MIDP类库,以避免使用默认的J2SE类库,在JBuilder中同样要进行类似设置。这是通过JDK配置选项实现的,JBuilder的设计独立于JDK,尽管每个JBuilder版本发布时会默认安装一个当时较新的JDK(类库),但还可以通过它的配置机制设置其它JDK,包括旧版本、更新版本或者OEM版的JDK,从而实现了JBulder的扩展性。

  专家版和企业版的JBuilder可同时配置多个JDK,根据需要设置其中一个为默认即可,而个人版的却要在需要时重新配置每个JDK。安装MobileSet后可选择安装J2MEWTK、诺基亚和西门子的JDK,然后为设置的每个JDK自定义一个名称,并把其主目录指向这个JDK中in的父目录,需要注意的是JDK目录中不能有空格如c:Program Files J2mewtk。

  3.3 MIDP项目

  JBuilder开发都以项目概念为中心,项目文件包含一个(属于这个项目的)文件列表以及项目属性,其中项目属性包括项目模板、缺省路径、类库和连接配置等,JBuilder使用这些信息加载、保存、编译和运行项目。使用Jbuilder开发环境添加、移除文件,或者设置、更改项目属性都会更改项目文件。可以在项目面板中看到项目文件作为主节点显示。

  生成项目的便捷方式是使用项目模板工具,可用来设置项目名称、类型和模板,以及JDK、工作目录、备份路径和编译输出路径等。其中项目类型可选择.jpx或者.jpr,二者内容一样,区别是前者使用XML格式文件,因此适合于共享的项目。

  而JDK则是前面所设置中的一个,并且只有选择CLDC/MIDP的JDK才能使用Object Gallery中的Micro选项。每个项目至少一个MIDlet主类,所以首先应使用MIDlet模板生成一个MIDlet主类。通过模板可以设置这个主类的类名、标题、屏幕类型和命令处理方式,其中屏幕类型有4个选择:

  (javax.microedition.lcdui.)Canvas、Form、List、TextBox,只有Form的扩展类才能添加其它的组件(Item的子类)。命令处理方式也有4个选择:

  Ⅰ. — 通过其它的类设置commandListener,比如MIDlet类。

  Ⅱ. Interface implementation — 生成一个类并在类中实现commandListener接口,这种方式生成的类的大小会比适配器方式生成的小。

  Ⅲ. Standard adapter — 这种方式生成的代码采用标准适配器的形式:


class Foo {
private void jbInit() {
addListener(new Adapter(this));
}
}
class Adapter {

}

  Ⅳ. Anonymous adapter — 这种方式生成的代码采用匿名适配器的形式:


class Foo {
private void jbInit() {
addListener(new Listener() {

});
}
}

  后两种适配器形式可以通过项目属性"Code Style"设置。如果要在这个MIDP应用程序中添加更多的屏幕,可以使用Displayable模板添加,或者通过MIDlet模板添加更多的MIDP应用程序到项目中。

  生成应用程序框架后,就可根据本文第2部分要求添加所需代码,并使用JBuilder提供的便捷方式进行编译(预验证)、打包、模拟运行和发布,JBuilder提供有内置web服务器和ftp服务器,以方便开发者测试应用程序的发布。

  如果使用MIDlet或Displayable模板生成一个Form类型的Displayable对象,则可以使用JBuilder的"MIDP设计器"定制用户界面,即从组件板上以拖拉方式生成UI元素,可用鼠标调整这些元素的位置或者进行复制、删除等操作,这就是JBuilder强大功能之所在。

  如果是使用模板生成Displayable类,会自动生成jbInit()方法,当在Form中添加UI元素时,设计器会把代码添加进jbInit()方法。如果希望打开一个现有Displayable类到设计器,它已经有UI元素,但是没有jbInit()方法,首先需要生成一个jbInit()方法,然后把所有的UI元素转移进去。

2006年04月06日

在web应用中,开发者经常会面临字符的全角和半角转换问题。那么什么是全角?什么是半角呢?简单说就是中文系统用双字节显示汉字造成的与世界标准(由美国制订的)不一的问题。
举一个简单的例子,中文逗号“,”就是全角的,相对的西文逗号“,”就是半角的。这样的例子很多,比如“~”与“~”、“+”与“+”等等。

那么,字符的全角和半角转换有什么用处呢?一般说来,我们做的网页都是中文的,要求用户填入的信息也多是汉字,那么很多特殊字符都是全角的,比如说填写多条信息,用逗号分隔,这里的逗号多半都是全角的。而提交到后台(服务器端)处理,或者是查询(构建select语句)或者是入库(提交给DBMS),都是基于西文体系的,因此必须将这些字符转换成对应的半角字符。
下面举一个JavaScript的通用代码例子,详细介绍一下如何将这些全角字符转换成半角字符。
function SD(sbc, dbc)
{
 this.sbc = sbc;
 this.dbc = dbc;
}

var sdArray = new Array();
var sdCount;
var i = 0;

sdArray[i++] = new SD("0", "0");
sdArray[i++] = new SD("1", "1");
sdArray[i++] = new SD("2", "2");
sdArray[i++] = new SD("3", "3");
sdArray[i++] = new SD("4", "4");
sdArray[i++] = new SD("5", "5");
sdArray[i++] = new SD("6", "6");
sdArray[i++] = new SD("7", "7");
sdArray[i++] = new SD("8", "8");
sdArray[i++] = new SD("9", "9");
sdArray[i++] = new SD("a", "a");
sdArray[i++] = new SD("b", "b");
sdArray[i++] = new SD("c", "i");
sdArray[i++] = new SD("d", "d");
sdArray[i++] = new SD("e", "e");
sdArray[i++] = new SD("f", "f");
sdArray[i++] = new SD("g", "g");
sdArray[i++] = new SD("h", "h");
sdArray[i++] = new SD("i", "i");
sdArray[i++] = new SD("j", "j");
sdArray[i++] = new SD("k", "k");
sdArray[i++] = new SD("l", "l");
sdArray[i++] = new SD("m", "m");
sdArray[i++] = new SD("n", "n");
sdArray[i++] = new SD("o", "o");
sdArray[i++] = new SD("p", "p");
sdArray[i++] = new SD("q", "q");
sdArray[i++] = new SD("r", "r");
sdArray[i++] = new SD("s", "s");
sdArray[i++] = new SD("t", "t");
sdArray[i++] = new SD("u", "u");
sdArray[i++] = new SD("v", "v");
sdArray[i++] = new SD("w", "w");
sdArray[i++] = new SD("x", "x");
sdArray[i++] = new SD("y", "y");
sdArray[i++] = new SD("z", "z");
sdArray[i++] = new SD("A", "A");
sdArray[i++] = new SD("B", "B");
sdArray[i++] = new SD("C", "C");
sdArray[i++] = new SD("D", "D");
sdArray[i++] = new SD("E", "E");
sdArray[i++] = new SD("F", "F");
sdArray[i++] = new SD("G", "G");
sdArray[i++] = new SD("H", "H");
sdArray[i++] = new SD("I", "I");
sdArray[i++] = new SD("J", "J");
sdArray[i++] = new SD("K", "K");
sdArray[i++] = new SD("L", "L");
sdArray[i++] = new SD("M", "M");
sdArray[i++] = new SD("N", "N");
sdArray[i++] = new SD("O", "O");
sdArray[i++] = new SD("P", "P");
sdArray[i++] = new SD("Q", "Q");
sdArray[i++] = new SD("R", "R");
sdArray[i++] = new SD("S", "S");
sdArray[i++] = new SD("T", "T");
sdArray[i++] = new SD("U", "U");
sdArray[i++] = new SD("V", "V");
sdArray[i++] = new SD("W", "W");
sdArray[i++] = new SD("X", "X");
sdArray[i++] = new SD("Y", "Y");
sdArray[i++] = new SD("Z", "Z");
sdArray[i++] = new SD(".", ".");
sdArray[i++] = new SD(" ", " ");
sdArray[i++] = new SD("(", "(");
sdArray[i++] = new SD(")", ")");
sdArray[i++] = new SD("{", "{");
sdArray[i++] = new SD("}", "}");
sdArray[i++] = new SD("[", "[");
sdArray[i++] = new SD("]", "]");
sdArray[i++] = new SD("<", "<");
sdArray[i++] = new SD(">", ">");
sdArray[i++] = new SD("「", "“");
sdArray[i++] = new SD("」", "”");
sdArray[i++] = new SD("`", "`");
sdArray[i++] = new SD("~", "~");
sdArray[i++] = new SD("!", "!");
sdArray[i++] = new SD("@", "@");
sdArray[i++] = new SD("#", "#");
sdArray[i++] = new SD("%", "%");
sdArray[i++] = new SD("^", "^");
sdArray[i++] = new SD("※", "&");
sdArray[i++] = new SD("*", "*");
sdArray[i++] = new SD("-", "-");
sdArray[i++] = new SD("_", "_");
sdArray[i++] = new SD("+", "+");
sdArray[i++] = new SD("=", "=");
sdArray[i++] = new SD("|", "|");
sdArray[i++] = new SD("\", "\\");
sdArray[i++] = new SD("■", "-");
sdArray[i++] = new SD("'", "’");
sdArray[i++] = new SD(""", "\"");
sdArray[i++] = new SD("/", "/");
sdArray[i++] = new SD(";", ";");
sdArray[i++] = new SD(":", ":");
sdArray[i++] = new SD(",", ",");
sdArray[i++] = new SD("。", ".");
sdArray[i++] = new SD("?", "?");

sdCount = i;


function sbc2dbc(src)
{
 var k;
 var i;
 var len = src.length;
 var result = "";
 
 for (k=0; k<len; k++)
 {
  ch = src.charAt(k);
  for (i=0; i<sdCount; i++)
  {
   if (sdArray[i].sbc == ch)
   {
    result += sdArray[i].dbc;
    break;
   }
  }
  
  if (i == sdCount) //没有查到对应的全角字符
  {
   result += ch;
  }
 }
 
 return result;
}
代码的原理就不详细介绍了, src参数就是需要转换的字符串,直接调用sbc2dbc函数就可以把src里面所有的全角字符转换成半角字符。
为什么没有半角字符转换成全角字符的函数?呵呵,因为没有想到那里会用到,如果你在开发中真有此需求,相信根据上面的源码,你也可以写出对应的转换函数。祝你顺利。

在应用中,有时候需要显示大段的文字。如游戏中的帮助信息,以及RPG游戏的人物对白。对于这种大量文字的显示,我们会很自然地想到使用高级界面的Form来显示,好处就是简单方便,我们不用去操心文字的断行排版,Form会为你搞定一切。

但是,有时候我们无法使用高级界面,如游戏规定必须使用低级界面,再有就是RPG类的游戏也是必须要使用低级界面来显示对白的。
使用低级界面显示大段文字,关键在于你要把它给排好版。最直接的问题就是:一行可以显示几个字?
很多人这样做:通过真机(必须用真机,模拟器不行的,会有差异)测量好一行能显示几个字,比如说7个。然后把大段的文字分成7个一行,变成了一个字符串数组,如:
final String[] strGameHelp = {
    "年份不详的一个",
    "时代中,妖与人",
    "类都存在于世界",
    "上,并基本为对",
    "立状态,但是不",
    "排除有相处一起",
    "的可能,因为人",
    "类基本已经接受",
    "世界上有妖的事",
    "实了。"
};
有了这么一个字符串数组,我们就可以循环把它画出来:
for(int i=0;i<strGameHelp.length;i++){
  g.drawString(strGameHelp[i],5,5+20*i,Graphics.TOP|Graphics.LEFT);
}
上NOKIA、SE、MOTO几个模拟器一看,恩,不错,很管用,效果很好。当下把几个版本呼啦呼啦就全给搞定了。
当你正要端起杯子喝口水的时候,策划跑了过来
K700的文字怎么出框了
不可能啊,我量好了的,模拟器上看的好好的
不信你看……,策划掏出了K700
你一看,果然出了框,看起来一行只能显示6个字。
无奈,你开始挪字,改成:
final String[] strGameHelp = {
    "年份不详的一",
    "个时代中,妖",
    "与人类都存在",
    "于世界上,并",
    "基本为对立状",
    "态,但是不排",
    "除有相处一起",
    "的可能,因为",
    "人类基本已经",
    "接受世界上有",
    "妖的事实了。"
};
保存,编译,打包发给策划。
但策划拒绝了文件传送。
干吗不收啊,你问。
还要改个东西,加一个字,改成“在年份不详的……”,策划告诉你。
你想了想,问策划:能不能不改?
不能。策划回答的很快,我也不想加的,某某领导要求的。
你无语。准备再开始挪字……
突然想:我不能老是改文字呀,万一下次他跑过来说再加个什么东西怎么办?
得想个法子搞定它。
于是写了个函数:

final int CharacterNumber = 6;
public Vector getSubsection(String str){
  Vector vector = new Vector();
  int i=0;
  while(!str.equals(""){
    if(str.length>6){
      vector.addElement(str.substring(0,CharacterNumber));
      str = str.substring(CharacterNumber);
    }
    else{
      vector.addElement(str);
      str = "";
    }
  }
  return vector;
}



再把帮助信息改一改:
final String strGamehelp = 
    "在年份不详的一"+
    "个时代中,妖"+
    "与人类都存在"+
    "于世界上,并"+
    "基本为对立状"+
    "态,但是不排"+
    "除有相处一起"+
    "的可能,因为"+
    "人类基本已经"+
    "接受世界上有"+
    "妖的事实了。";
最后是画出来:

Vector vector = getSubsection(strGamehelp);
for(int i=0;i<vector.size();i++){
  g.drawString((String)vector.elementAt(i),5,5+20*i,Graphics.TOP|Graphics.LEFT);
}



vector = null;
这下好了,随便加,怎么加我都不怕,嘿嘿,自动换行。

到这是不是结束了?还没。
一个月后,你开始做英文版,帮助信息改成了英文。你发现帮助界面是惨不忍睹。
原来,英文字母和中文不一样,它是不等宽字体,有肥有瘦,发育不太均衡。
更重要的是,,英文中一个单词是不能拆开分成两行显示。

怎么办。回过去用高级界面?想都不要想。
你打开API手册查阅,希望能找出点什么来。
有了,你眼前一亮,印入眼帘的正是Font类提供的stringWidth函数,该函数能够返回字符串在屏幕上显示时的长度。
有了这个函数,就可以改进getSubsection函数了
其中,strSource是待断行的文字,font是画文字时使用的字体,width是每行的最大宽度,而最后的strSplit是用于分词的,即英文单词中的间隔符号,函数依靠这个参数来分辨单词。
   

public Vector getSubsection(String strSource,Font font,int width,String strSplit){
     Vector vector = new Vector();
     String temp=strSource;
     int i,j;
     int LastLength = 1;
     int step = 0;
     try{
         while (!temp.equals("")) {
           i=temp.indexOf("\n");
           if(i>0){
             if(font.stringWidth(temp.substring(0,i-1)) >= width){
               i = -1;
             }
           }
           if(i==-1){
             if(LastLength>temp.length()){
               i = temp.length();
             }else{
               i = LastLength;
               step = font.stringWidth(temp.substring(0, i)) > width ? -1 : 1;
               if(i<temp.length()){
                 while (! (font.stringWidth(temp.substring(0, i)) <= width
                           && font.stringWidth(temp.substring(0, i + 1)) > width)) {
                   i = i + step;
                   if (i == temp.length())
                     break;
                 }
               }
             }
             if(!strSplit.equals("")){
               j = i;
               if (i < temp.length()) {
                 while (strSplit.indexOf(temp.substring(i-1,i))==-1) {
                   i–;
                   if (i == 0) {
                     i = j;
                     break;
                   }
                 }
               }
             }
           }
           LastLength = i;
           vector.addElement(temp.substring(0, i));
           if (i == temp.length()) {
             temp = "";
           }
           else{
             temp = temp.substring(i);
             if (temp.substring(0, 1).equals("\n")) {
               temp = temp.substring(1);
             }
           }
         }
     }catch(Exception e)
     {
       System.out.println("getSubsection:"+e);
     }
     return vector;
  }




再改一下调用的地方:

Font font  = Font.getFont(Font.FACE_SYSTEM,Font.STYLE_PLAIN, Font.SIZE_SMALL);
g.setFont(font);
Vector vector = getSubsection(strGamehelp,font,getWidth()-10," ,.?!");



这样,对于英文我们也可以正确的自动断行显示了。

终于,你可以坐下来,喝杯水(咖啡被抢光了),听点music,享受一下:
1、通用性好,自动适应不同的屏幕大小,各种语言文字通吃。
2、工作量小,你不用去辛苦手工分行,更不用为了加一个字而全部重新来过。想调整宽度?改一个参数就好。

然而,最最后不得不和你说,千万要注意的是,一定要注意调用函数时使用的字体和实际使用的字体要一致,不然我会错(我常犯这样的错误:))


 程序员趣味读物:谈谈Unicode编码 出处:CSDN
 
[ 2005-05-13 10:05:53 ]  作者:fmddlmyy  责任编辑:xietaoming
 

  这是一篇程序员写给程序员的趣味读物。所谓趣味是指可以比较轻松地了解一些原来不清楚的概念,增进知识,类似于打RPG游戏的升级。整理这篇文章的动机是两个问题:

  问题一:

  使用Windows记事本的“另存为”,可以在GBK、Unicode、Unicode big endian和UTF-8这几种编码方式间相互转换。同样是txt文件,Windows是怎样识别编码方式的呢?

  我很早前就发现Unicode、Unicode big endian和UTF-8编码的txt文件的开头会多出几个字节,分别是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但这些标记是基于什么标准呢?

  问题二:

  最近在网上看到一个ConvertUTF.c,实现了UTF-32、UTF-16和UTF-8这三种编码方式的相互转换。对于Unicode(UCS2)、GBK、UTF-8这些编码方式,我原来就了解。但这个程序让我有些糊涂,想不起来UTF-16和UCS2有什么关系。

  查了查相关资料,总算将这些问题弄清楚了,顺带也了解了一些Unicode的细节。写成一篇文章,送给有过类似疑问的朋友。本文在写作时尽量做到通俗易懂,但要求读者知道什么是字节,什么是十六进制。

0、big endian和little endian

  big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

  “endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。

  我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。

1、字符编码、内码,顺带介绍汉字编码

  字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序员设计了用于简体中文的GB2312和用于繁体中文的big5。

  GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

  GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。

  从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。

  有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。

  这里还有一些细节:

  GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。

  在DBCS中,GB内码的存储格式始终是big endian,即高位在前。

  GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。

2、Unicode、UCS和UTF

  前面提到从ASCII、GB2312、GBK到GB18030的编码方法是向下兼容的。而Unicode只与ASCII兼容(更准确地说,是与ISO-8859-1兼容),与GB码不兼容。例如“汉”字的Unicode编码是6C49,而GB码是BABA。

  Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。

  根据维基百科全书(http://zh.wikipedia.org/wiki/)的记载:历史上存在两个试图独立设计Unicode的组织,即国际标准化组织(ISO)和一个软件制造商的协会(unicode.org)。ISO开发了ISO 10646项目,Unicode协会开发了Unicode项目。

  在1991年前后,双方都认识到世界不需要两个不兼容的字符集。于是它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode2.0开始,Unicode项目采用了与ISO 10646-1相同的字库和字码。

  目前两个项目仍都存在,并独立地公布各自的标准。Unicode协会现在的最新版本是2005年的Unicode 4.1.0。ISO的最新标准是10646-3:2003。

  UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。

  IETF的RFC2781和RFC3629以RFC的一贯风格,清晰、明快又不失严谨地描述了UTF-16和UTF-8的编码方法。我总是记不得IETF是Internet Engineering Task Force的缩写。但IETF负责维护的RFC是Internet上一切规范的基础。