Thursday, December 21, 2006

Blogger 不再 Beta

昨天 Blogger Buzz 更新消息说 Blogger 推出了正式版本,不再是 beta 状态了。

看了下更新列表,没什么特别的新东西。都是最近长期 beta 以来一步步更新的内容。只是发现新版本 Blogger 的域名从 beta.blogger.com 变成了 www2.blogger.com,不过这个并不影响原来 blog 的网页。访问任何 beta.blogger.com 下的页面,会直接跳转到 www2.blogger.com。

趁着这次更新发布,又修改了一下我的模版。主要是把 Calendar 部分换成了可以在一个页面上来回翻月份,而不会在翻动的时候跳动到对应的存档页,这样可以提高 Calendar 导航的效率。

另外,把树状 Archive 目录提前了一些,并在存档页和帖子页会自动展开对应的月份,这同样也是为了加强导航的作用。

之前发现在页内的评论表单上提交评论后,HTML 页面不会被 Blogger 重建,现在欣喜的发现,这个问题 Blogger 似乎已经在最近的某次更新中修正过了。目前直接在页内提交评论,页面会马上开始重建了。只不过由于新 Blogger 每次要更新的页面比较多(包含所有的 Label 页面),因此和以往老的 Blogger 页面重建相比,速度还是要稍慢点,通常 5 分钟之内应该是可以完成的。

接下来打算在有时间的时候把页面左栏的最新评论改成用 JSON 实现,这样就成了真正意义上的“最新评论列表”,而不是目前的“出现在引导页上的帖子相关评论列表”。本来是想把页面上所有可以由 Bogger 的 Feed 实现的数据都用 JSON 来提供,但是每次请求返回的内容多的时候高达 60 多 K,可能会导致页面变慢,而访问上,不会感觉到太大的改变,因此决定先放一放再说。目前还是通过一个 JS 文件来静态存储所有帖子的时间和标题等信息。只不过这个文件仍然可以写服务端程序通过调用 Blogger 的 GData API 来自动更新。

Tuesday, December 05, 2006

用微软雅黑代替宋体开启 ClearType UI

用 Microsoft Update 升级到了 Internet Exloporer 7,发现它默认开启了 ClearType,一时之间,觉得两眼昏花,半天才适应。不过由于中文的默认字体仍然是宋体/新宋体,而这个字条不支持 ClearType,因此会发现网页中西文部分是 Anti-Aliasing 的,但是中文部分仍然是默认状态。

其实很早之前就很羡慕 Mac 系统下简明漂亮的细黑中文字体,Windows 系统下的默认宋体的中文字体还算过得去,但是它的西文实在是无法入眼,因此通常我安装了中文版的 Windows 的第一件事情就是把系统界面的字体换成 Tahoma。

现在微软在新的 Windows Vista 中用“微软雅黑”字体代替了原来的宋体/新宋体,以便更好的支持中文的 ClearType 显示。将这个字体安装到 Windows XP 上,也就可以同样利用它实现中文的 ClearType 了。以下是应用前后的对比:

未使用 ClearType

使用 ClearType

使用 ClearType 后的 Google 搜索页

实现方法如下:

安装字体
下载“微软雅黑 (ver 5.00)”,解压后得到 msyh.ttf 和 msyhbd.ttf 两个文件,拷贝到 C:\Windows\Fonts 目录,字体即被安装。

应用在网页上
从 IE 中的菜单选项打开 Tools -> Internet Options -> General -> Apperance -> Fonts 对话框,在左边的 Webpage font 选择框中选 “微软雅黑”即可。Internet Options 对话框同样可以在控制面板中打开。

应用在系统 UI 上
在桌面单击右键,选择“属性 / Properties”,打开第四个标签“Appearance -> Effects...”,在打开的对话框第二个下拉菜单中选 ClearType,然后点 OK;同样是在 “Appearance” 标签,点右下角的“Advanced”,在弹出的对话框中,将 Item 下拉框中的所有项目全部过一遍,将所有可以改变字体的项目的字体改成“微软雅黑”(字号推荐 8pt),然后点 OK 退出即可。

微软雅黑默认的西文字体还算不错,只是感觉稍欠紧凑,不过比宋体/新宋体的默认西文字体好多了。

调节 ClearType
如果觉得显示的 ClearType 不够清晰,可以到微软网站 ClearType Tuner 在线调整,也可以下载安装 ClearType Tuner 的 Windows XP PowerToy 在控制面板中调整。

关闭 ClearType
如果觉得眼睛仍然无法适应 ClearType,在显示属性的对话设置中还原成 “Standard” 即可。对于 IE7,可以打开 Internet Options -> Advanced 标签,将 Multimedia 下的 “Always use ClearType for HTML” 前的复选框去掉即可在 IE 中关闭 ClearType。

Saturday, November 18, 2006

通过 GData API 提交评论

前两天有提到升级到 Blogger Beta 后发布评论的问题。今天从 Google Groups 上的 Google Data API 讨论组上看到一个新的发布评论的办法,就是用 GData API 通过单贴评论的 Feed 地址
http://beta.blogger.com/feeds/blogID/postID/comments/default

发布。

这个方法与向 http://beta.blogger.com/comment.do 直接 POST 数据的方法比起来,要科学得多。经过测试,这个方法确实可行,评论发布后,页面会被正常地重建。然而,仍然有问题:评论对应的发布者被设置成了 Blogger 的主人,也就是在向 feed POST 数据之前提供给 Google 的身份验证信息对应的用户。

据讨论组上 Blogger 的研发人员回复说,官方并不支持这种方法提交评论,尽管它确实是可行的。我想之所以会有上面的问题,大概是因为这个方法原本是用来发布帖子用的,在这种情况下,作者信息自然就应该从通过身份验证的 Blogger 账户中取得。

已经在这个有关 GData API 提交评论的讨论串中提出建议修正这个问题,不过不知道 Blogger 会不会理我。在这个问题被修正之前,这个方法实际仍然无法投入实用,毕竟不会有人会让所有评论的作者信息丢失掉。

Wednesday, November 15, 2006

Feeds of Blogger Beta

今天爬 Blogger Beta 的资料,发现 Blogger Beta 除了可以提供整站帖子、整站评论以及分帖子评论的 feed 以外,甚至可以按照单个的 label 来提供 feed,格式如下:
http://beta.blogger.com/feeds/blogID/posts/default/-/labelname

URL 中的 labelname 就是要订阅的 label,注意中间的“-”符号不能少!

另外,把其它的订阅地址的格式也一并写在这里:

订阅整站帖子全文:
http://beta.blogger.com/feeds/blogID/posts/full

订阅整站帖子默认,即按照 Blogger 的 Site Feed 设置选项:
http://beta.blogger.com/feeds/blogID/posts/default

订阅整站评论:
http://beta.blogger.com/feeds/blogID/comments/default

订阅单贴评论:
http://beta.blogger.com/feeds/blogID/postID/comments/default


Update on 2006/11/17:
对于 label 的 Feed,发现如果 labelname 中含有空格,则无法正确获取 Feed 内容,会出现 Blogger 的“We're sorry, but we were unable to complete your request.”报错页面,例如:
http://beta.blogger.com/feeds/blogID/posts/default/-/Knowledge Base

无论中间的空格是否被 urlencode 成 %20,都会出现这个情况。已经向 Blogger GData API 讨论组 提交了这个问题,不过至今仍未有任何回复。

Update on 2007/01/12:
这个问题在 Blogger GData API 的另一个讨论串中被再次提到,并被认定为一个 bug,可以通过这里来追踪。希望这个问题能够早日得到解决。

Monday, November 13, 2006

升级后如何才能方便的发布评论?

升级到 Blogger Beta后,由于大家无法访问 beta.blogger.com 域名,因此出现了无法发布评论的问题,在上次的帖子中有所讨论。

为了解决这个问题,我自己写了个简单的 Servlet,来接受来自页面上的评论发布,并将这个请求转到 Blogger Beta 上处理评论发布的地址 https://beta.blogger.com/comment.do,因为我可以修改服务器上的 host 文件使其可以访问 beta.blogger.com,这样可以起到一个类似代理的作用,代替需要发布评论的朋友们访问这个不能直接访问的域名。

理论上是没有问题的,实践上我也几乎取得了成功。出现的问题是:评论被正常发布后,和评论相关的帖子页面没有重建!

我们知道,用 FTP 的方式发布 Blogger,任何对帖子的更改,都会导致 Blogger 自动重建页面并自动发布到 FTP 相关的目录下,以使得 blog 网站上的页面能够反应出最近的信息,这些操作包括新增/修改/删除帖子,以及添加/删除评论。正常情况下,一旦有评论发布,这个重建的过程就会发生,这样评论能够在最多几分钟的时间内出现在页面上。

可是现在,评论的内容都出现在 Blogger Beta 自己的评论页面上了,但是却没有触发页面重建。不过,如果直接把页面上评论表单的内容提交到 https://beta.blogger.com/comment.do,则一切正确;但是当然这样做没有意义,因为这要求发布评论的朋友能够直接访问 beta.blogger.com 域名。

现在出现的现象会是,大家发布的评论不会立即显示在帖子页面上,但是实际上是已经被正确的保存了。我看到电子邮件的提示后,就会尽快重建页面使得这些评论能够被显示出来。相信这个问题只是暂时的,总归是应该得到解决的。

这个问题琢磨了一晚上也没有得到结果,Google 上也搜不出什么有用的东西来。已经把这个问题发布到了 Google 上的 Blogger Data API 讨论组求助,希望有人遇到过类似的问题从而给我一些提示。

Updated on 2006/11/17:
有一个更科学的方法来提交评论,那就是用 GData API 向单贴评论的 Feed 地址提交,不过仍然有些问题。详见“通过 GData API 提交评论”。

Updated on 2006/12/21:
Blogger 的某次更新似乎已经解决了评论提交后页面不会重建的问题。现在在页内的评论表单中提交评论,页面会被立即重建,只是由于需要重建的页面和以往的老 Blogger 比起来要多一些,因此会慢一点。一般 5 分钟之内评论就会出现在页面上了。

Saturday, November 11, 2006

Firefox 的 JavaScript 问题两则

今天发现页面上新加上的左列 Tag 在 Firefox 上显示不正确,在细察之下,发现 Firefox 在 JavaScript / CSS 上和 IE 不同之处:

问题 1. 类似 obj.style.height = imgObj.height 的语句无效。

即将一个 image 对象的高度值赋给另一个对象,用来修改其样式高度,这样做无效。

分析

要理解这个问题,首先要纠正思想上的一个误区。以上这个操作,其实并非是通常编程概念上的将一个 int 值赋给另一个 int 变量。这个语句的操作,实际上是把 imageObj.height 当作一个字符串,作为 obj 这个对象 CSS 中 height 的属性。

之前的一篇帖子说过,Firefox 对 CSS 的理解非常严格,任何表示大小的值,数字后面必须跟上单位,除非是 0。也就是说,height: 20 对于 Firefox 来讲不具有任何意义,必须写成 height: 20px 才会被接受。所以,在 JavaScript 中,为了让对 CSS 的 height 值的设置有效,所赋给的字符串也同样必须是数量加上单位。

解决

以上语句,应该写成:
obj.style.height = imgObj.height + 'px';


问题 2. Firefox 不支持 obj.innerText 属性。

如果把 obj.innerText alert() 出来,显示的值是 undefined

分析

Firefox 支持 innerHTML 属性却不支持 innerText,这一点实在是蹊跷。很多人建议用 innerHTML 来代替需要用 innerText 的场合,但是这显然并不总是适用。我们有时候只是要取得一个 Tag 中的文字信息而不需要 HTML 的标记。

解决

良好的替代办法是用 obj.textContent,这个属性的作用和 innerText 是相同的,名称不同而已。不过为了兼容性,我们还是需要在程序中区分一下当前环境支持哪种。以下方法可以帮助我们区分:
if (document.all) {
obj.innerText = "myText";
}
else {
obj.textContent = "myText";
}

以前我们讲过,Firefox 不支持 document.all 这个 Collection,我们需要用 getElementById() 方法来替代,所以通过判断是否有 document.all,就能区分当前环境。

Friday, November 10, 2006

升级到 Blogger Beta 后的问题

昨天收到 Blogger 的升级邀请,将我的 Blogger 帐号升级为了新推出的 Blogger Beta。由于升级的过程中,没有对原有模版中的内容进行更新,也没有自动重新重建页面,因此今天在重新修正了模版文件并将整个站点重建。在这个过程中还是发现了一些问题:
  • 模版中的中文内容变成了乱码
  • 在转换到 Blogger Beta 过后,发现原模版中所有的中文内容全部变成了乱码。看来正如 Blogger Beta 的 Known Issues 所说,Blogger Beta 目前仅支持英文

    幸亏模版有备份,阿门……

  • 模版标签 <$BlogOwnerAboutMe$> 失效
  • 发现原有模版中的 <$BlogOwnerAboutMe$> 在构建页面时被替换成了空字符串,也就是说现在在模版上使用这个标签显示不出任何内容。

    不过与此同时,<<$BlogMemberProfile$> 标签输出的内容中仍然正确包含了 About Me 的信息。我想这应该是个 bug,而且已经通过 Blogger 的反馈系统提交了。只是没有回应…… 没有办反,暂时只有用这个标签代替。其输出的内容与我自己写的几乎完全相同,我之所以原来没有用,是因为我不需要 About Me 这几个字,而是用图片替换掉了。现在只好在页面上用 JavaScript 来取掉这几个字。

  • 发布时不再提供 rss.xml 文件
  • 老版本的 Blogger 支持 Atom 0.3 和 RSS 2.0 两种格式的 Feed,在发布时分别对应 FTP 发布根目录下的 atom.xml 和 rss.xml,现在看来仅发布了 atom.xml 而没了 rss.xml。

    与此同时,Atom Feed 的版本升级为 1.0。我是通过 FeedBurner 来发布我的 Feed 地址的。不过,升级过后,通过 Maxthon 无论直接通过 atom.xml 还是 FeedBurner 的地址,都会发生“无法解析 Feed 的格式”错误。除此之外,Google Reader、FireFox 2.0 的订阅都没有问题。看样子这个只能怪 Maxthon 了。
    关于新版的 Site Feed 设置,比较奇怪的一点是 Blogger 除了提供对 Post 的订阅以外,还提供了对所有 Comments 以及单篇 Post 的 Comments 的订阅,然而发布到 FTP 上的所有文件中,和 Site Feed 相关的只有 atom.xml 这么一个文件,所以无从得知如何实现对 Comments 的订阅。或许是要使用 BlogSpot 的用户才有这个功能?

    Update on 2006/11/14:
    Blogger Help 上的一个条目说:RSS 格式的 Feed 只在 Blogger Pro 提供,如果非 Pro 用户想要使用 RSS 格式的 Feed,请使用 FeedBurner

    另外,现在知道了订阅整站 Comment 和分单帖子 Comment 的地址,它们分别是
    http://beta.blogger.com/feeds/blogID/comments/default

    http://beta.blogger.com/feeds/blogID/postID/comments/default

    Update on 2006/11/16:
    似乎 rss.xml 的发布又恢复了。现在发布新帖的时候,发布文件列表中有这个文件。Known Issues for New Bloggers 上面说明了该问题的情况。

  • 无法特别为标签页面定制模版内容
  • 老版本的 Blogger 模版标签中有几个条件标签可以分别对 IndexPage、ItemPage 和 ArchivePage 进行内容的定制,因为这几个页面的元素显然是不同的。现在多了个 Label 页面,不过却没有特别提供一个 LabelPage 的标签来方便模版条件输出。

    之所以提这个功能,是因为它最大的好处是可以在 Label Page 上显示当前这个页面的内容属于哪个 Label,这样不致于让读者产生困惑。现在的 Label Page 和 Index Page 除了帖子不同外,其它格式基本上完全一样。

  • 可能出现读者访问速度非常缓慢且无法发表评论
  • 当然这个不能怪 Blogger。不过最近大多数人都无法访问 beta.blogger.com 这个域名,对升级后的 Blogger 确实会产生很大的影响。

    首先模版中有多处需要引用 beta.blogger.com 站点上的资源,而且这些资源通常写在网页头中。读者必须要等待浏览器判断这些资源访问超时后才会继续显示后面正常的网页内容,看到的现象就是,网页除了窗口标题出现以外,内容全白,大约等半分钟后网页内容出现。即使 CSS 和模版图片全部用自己的,也无法避免需要引用 beta.blogger.com 上的 JavaScript 来实现 Backlinks 以及一些 Post Control 的功能。除非你完全不用这两个功能,则可以基本上避免引用 beta.blogger.com 站点而导致页面打开缓慢。

    另外,众所周知对 Blogger 的帖子发表评论需要连接到 Blogger 站点上发布。这个过程实际上是 Blogger 在收到新的内容后对相关页面的重建过程。Blogger Beta 的 Blog 站点,读者自然要到 beta.blogger.com 上去发表评论,这个站点打不开,所以不能评论。

    虽然我们可以通过在 C:\Windows\System32\Drivers\etc\host 文件中添加
    72.14.219.190 beta.blogger.com

    这样一行内容来实现访问该站点,但是这仅能方便自己而已,你无法要求自己 blog 所有的读者在发表评论之前都这么做。因此,请所有 Blogger 的用户在升级之前三思!

Thursday, November 09, 2006

转到 Blogger Beta

前几天 Blogger Beta 终于支持了发布到 S/FTP,今天我的帐号终于收到了转到 Blogger Beta 的邀请,这一历史性的时刻终于来到了。这里是 Blogger Buzz 上的公示消息


兴冲冲的赶快按下了“Switch to the new version”,才发现后面的过程需要输入 Google Account 并且到 beta.blogger.com 验证。由于目前公司的网络访问不了 www.google.com,而 beta.blogger.com 目前也无法访问,因此第一次失败……

开了 Tor,用 FireFox 重新打开页面,并且修改了 host 文件以便能够访问 beta.blogger.com,这次所有验证都对了,最后显示了一个“Switching to the new Blogger”,大意是说我的 blogger 正在转移到新的 Blogger Beta,有些 blog 根据情况可能需要时间比较久,因此请耐心等待。等转移完成以后,将会往我的 Gmail 邮箱发邮件通知。

从上午开始申请转移,到午后一直是这个样子。我严重怀疑是不是由于第一次操作失败导致了什么问题,因为我觉得我那两个 blog 150 来篇帖子不至于需要这么久。不放心之下,Google 上查了一下,发现其他人也遇到过类似的问题,说大概是遇到了 IRoC (Initial Rush of Connection)。嗯,也难怪,Google 一开放个服务,开始几天总是要出现点负荷问题。

大概下午三点钟的时候,情况终于发生了变化。Blogger Beta 登录后,显示的内容变为:


看来是 Blogger 正在做转移操作,为了避免这个过程中发生问题,已经屏蔽了用户对帐号进行操作。不过这个过程仍然很久,一直到下午下班前都没有完成。

大约晚上八点,再上线时,发现 Blogger 转移终于完成。Gmail 也收到了 Blogger 的通知邮件。

登录到 Blogger Beta,发现罗列了三个blog,有两个是从先前老版本 Blogger 上转来的,另一个则是上次为了测试创建的。登录老版本的 Blogger,发现已经无法回头了:


不管怎么说,等了好几个月的升级终于完成了。即使是这个转移本身,也花了一天的时间,对人的耐心真是一个不小的考验啊。

Wednesday, November 08, 2006

让 TortoiseSVN 记住密码

用 TortoiseSVN 维护工程,每天都要输入无数次密码,有时候一次操作就要输入数次密码,实在是烦人。多方搜寻让 TortoiseSVN 记住密码的办法,但是对我目前的情况都无效,我想大概是因为用了 svn+ssh 协议的原因。

不过虽然不能让 TortoiseSVN 记住密码,但是却能够让它自动输入密码,一样可以避免频繁输入密码的麻烦,方法如下:

1. 在 Explorer 右键菜单中:TortoiseSVN -> Settings.
2. 在弹出对话框的左侧面板中选择 Network,在右边界面下方的 SSH Client 中填写:
[TortoiseSVN Install Path]\bin\TortoisePlink.exe -pw [Your Password]

例如,你的 TortoiseSVN 安装在默认目录 C:\Program Files\TortoiseSVN 下,密码为 abc123,那么以上的内容就应该为:
C:\Program Files\TortoiseSVN\bin\TortoisePlink.exe -pw abc123

3. 点击 OK.

然后,以后在进行 SVN 操作的时候,就不再弹出要求输入密码的窗口了。

Friday, November 03, 2006

Blogger Beta 终于完工了

今天 BloggerBlogger Buzz 上公布:Blogger Beta: Feature Complete! 文中声称现在 Blogger Beta 可以支持通过 S/FTP 发布了,相比 Classic 的 FTP 发布,保留了 Label 的功能!这是之前我一直对 Blogger 感到不爽而且想自己增加的功能,后来 Blogger Beta 自己增加了 label 功能后,我自己的开发也就停止了,不过由于是通过动态网页技术实现的,当时只限使用 blogspot 的 Blogger 用户使用。现在终于也支持 FTP 发布的用户了,对于我来说,对 Google Blogger 服务的支持无疑又进了一步。

只不过,美中不足的是,通过 FTP 发布的 Blogger,不支持新的 Layout 模版编辑和拖拽功能。不过对于喜欢自己编写模版定制 Blogger 的人(比如我)来讲,这个是无关紧要的。页面上编辑 Layout 再智能再强大,也比不过我自己写 HTML 和 CSS 吧! :-P

这里是原先通过 blogspot 发布的一个测试用 Beta-Blogger,现在移到用 FTP 发布,基本上没有任何问题。观察其发布的文件,会发现它为每个 label 建立了一个 HTML 文件,只是不知道当某个 label 下帖子多了以后会怎么办。

顺带想提一下的是 *.blogspot.com 域名现在又无法访问了,看来上次的解封也是空欢喜一场。目前更让人郁闷的是在公司无法访问 www.google.com (公司 IT 部门解释说是 ISP 的问题,貌似是网通,在家里的时候用电信 ADSL 拨号可以正常访问),beta.blogger.com 域名也无法访问。由于上面提到的测试用的 Beta Blogger 页面中需要调用 beta.blogger.com 下的一些资源,因此在打开页面的时候会比较慢,因为要等请求超时。好在缺少这些资源并不影响访问者观看 Blogger Beta 的网页。

什么时候才能自由访问 Google 的各项服务呢?抑或什么时候才能自由的访问互联网的公众信息?

Thursday, October 26, 2006

Blogger 网页显示空白的问题

之前一直有朋友声称我的 Blog 访问不了,打开来是空白。这个问题我自己也经常遇到,原因是网页编码被误判成 GB2312,而不是正确的 UTF-8,于是 IE 无法识别,所以显示成空白。这个问题据说在 Firefox 上并不存在,但是为什么会被误判我也一直不清楚。

现在原因应该清楚了,因为在文件头中,声明网页内容编码的那一行标记
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

被放到了 <title> ... </title> 之后,而刚好 title 中有中文,所以 IE 在没有获得指定的编码类型时,用了默认的 GB2312。

解决这个问题还是比较容易:编辑 Blogger 的模版,把“<$BlogMetaData$>”放到“<title><$BlogPageTitle$></title>”的前面,然后重建一下,就不会出现这个问题了。

Tuesday, September 26, 2006

奇怪的 TP-LINK 无线网卡驱动

由于新买的 ASUS A6Jc 笔记本没有自带无线网卡,于是我另外购买了一块 TP-LINK 的 TL-WN310G 无线网卡。安装以后出现一些问题,现总结如下:

现象
安装完驱动重新启动后,Windows XP 由其特有的欢迎登录界面变成经典的窗口登录模式。切在控制面板“用户设置”中无法改回欢迎界面登录模式,会提示“近期安装的应用程序屏蔽了欢迎界面和快速用户切换。您需要卸载该应用程序来恢复这项特性。以下名称或许能帮助您识别该应用程序:athgina.dll”。

原因
驱动程序安装时会用其自带的 athgina.dll 文件取代 Windows XP 自身的 msgina.dll 并写入注册表从而改变 GINA 的行为。

GINA 的全称为“Graphical Identification and Authentication”——图形化识别和验证。它是几个动态数据库文件,被 winlogon.exe 所调用,为其提供能够对用户身份进行识别和验证的函数,并将用户的帐号和密码反馈给 winlogon.exe。在登录过程中,“欢迎屏幕”和“登录对话框”就是 GINA 显示的。

无线网卡的驱动程序安装自己的动态链接库来控制 GINA,推测可能是为了获取用户登录的用户名密码以便在登录有身份验证的无线网络时使用。

解决办法
如果只是使用不加安全验证的无线网络,实际上是永不到这个动态链接库的。打开注册表,找到键值
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GinaDLL,其值应该就是 athgina.dll,将其删掉,就可以立即到控制面板里重新设置欢迎登录界面和用户快速切换。实际上,Windows 默认是没有这个键值的,而只要存在这个键值,欢迎登录界面和用户快速切换功能就会被屏蔽,哪怕其值是 Windows 自身的 msgina.dll。

如果需要使用 WPA + PEAP(MSCHAP v2) 安全验证的无线网络,似乎只有放弃 Windows XP 的欢迎登录机界面和用户快速切换功能了。去掉以上键值后,不管是用 Windows 自带的无线忘了连接向导还是 TP-LINK 驱动盘提供的无线网络连接程序都无法正确建立无线连接,而把这个键值重新加上即可。

Update on 2006/10/30:
今天又把笔记本带到公司,注册表中没有 GinaDLL 这一项,但是正常的连接到了公司的无线网络。看来这个东西并不是必要的。

Friday, August 18, 2006

模版更新

看了 Blogger Beta 新的 Archive 树状的列表方式,非常喜欢,于是花了点时间,把自己现在的模版也改成了这个样子。刚好做 Calender 的时候,已经有一份完整的时间、帖子标题和地址的对应数据,所以做这个树状的 Archive 列表,只是表现的问题而已。不过,尽管如此,还是花了我好几个小时的时间。

另外,打开了 Blogger 的 Backlink 功能。由于要改模版,Blogger 提供这个功能以后一直没有动,嫌麻烦。不过,把这个功能打开容易,要把样式调整的和整体一致,还是花了不少功夫。

弄好后,把自己比较得意的帖子都看了一下,除了 Throwing Tables Out the Window 那篇以外,别的帖子几乎没什么 backlinks,可能还是因为我写的东西都比较私人吧。

Wednesday, August 16, 2006

Blogger 新动向

这两天,久经抱怨的 Google Blogger 服务终于有了新动静。从一年多以前开始,Blogger 就承诺对其提供的 Blog 服务进行改进,但除了新增了评论发布的校验码以及评论内容评审以外,未见有什么大的动作。前天,Blogger Buzz 发布了最新推出的 Blogger in beta,针对长期以来用户的抱怨提供了很多新功能,具体包括:
  • 可使用标签对文章进行分类
  • 对 Blog 的访问进行权限控制
  • 用鼠标拖拽即可完成对 Blog 模版的修改和设定,无须进行 HTML 编辑

然而,该项服务目前仅对一部分老用户开放,Blogger 声称会对所有逐步开放。如果你登录后 Dashboard 上有相关连接,那就说明你的帐号已经支持新功能;否则,就要再耐心地等上一段时间。

如果等不急,也可以立即注册一个新的 Beta 帐号,或者用 Google 的帐号,或者原来的 Blogger 帐号登录后创建一个新的 Blog 即可。Blogger 稍后会提供功能,让用户把原来的 Blog 和 Beta 帐号下的新 Blog 合并起来,方便管理。

Blogger 还提供了一个 Tour 来介绍新的功能,包括灵活定制模版、对访问进行权限控制、更多的 Feed 选项、更快捷的发布等。

我也创建了一个新的 Blog 来测试 Blogger Beta 的新功能。前前后后把它的控制面板翻了一遍,发现功能并没开发完全,有些地方还是不尽如人意:
  • 发布选项中,目前仅支持发布到 *.blogspot.com,尚未提供对 FTP 和 SFTP 发布的支持。
  • 创建帖子的时候,表单下面多了一个地方可以设置标签,用来对帖子进行分类。分类的内容会自动发布到帖子后面,而连接指向 http://*.blogspot.com/search?label=label,而并非静态的 HTML 支持。当然,对于分类和标签支持,如何访问指定标签相关的文章,这是最麻烦的一部分,但如果对某一个标签的访问是通过 CGI 程序动态支持的话,不知道对于通过 FTP 发布到指定 host 的用户,Blogger 将如何提供这一功能。
  • 对帖子进行回复仍然要访问 Blogger,表单完全脱离模版。自定义 HTML 提供回复表单,仍然无法使用 Word Verification 功能。
  • 还不支持对模板直接进行 HTML 编辑,也无法去掉页面顶上的 Navbar;仅能够把目前的模板降级为旧版本的模版进行编辑。

不过,我上周才决定要自己对 Blogger 进行 DIY,Blogger 这周就给出升级的消息,同时还包括标签和分类的功能,让我着实很郁闷。Blogger 马上就要自己支持的功能,我就完全没有必要自己 DIY 了阿。虽说是自己写点东西来玩,但也总要有用才有成就感啊。

在放出 Blogger 功能全面升级的消息后,昨天 Blogger Buzz 又放出了 Blogger API 更新的消息,这意味着:
  • Blogger/GData 文档中能够找到很多范例代码。
  • 提供 C# 和 Java 版本的客户端类库下载。
  • 对于账户校验,分别提供了针对桌面程序和 WEB 应用程序的详细文档。
  • GData 相关的代码将能够被很容易的重用于其它 GData 应用程序,如 Google 的 Calendar。

总的来说,这次 Blogger 算是给了支持者们一个值得期待的回应,虽然有很多地方还可以更好,不过也总比什么动静都没有好多了。

与此同时,Blogger 的默认托管站点域名 *.blogspot.com 最近可以在国内自由访问了。以上我建立的测试 Blog 就是用的这个域名。Google 上可以搜索到很多关于 Blogger 解封的信息,不过大家对 Blogger 是否真的解封众说纷纭,并没有确切的消息。这个域名被封锁长达三年之久,如今如果真的被解封了,当然是值得高兴,或许广大的 Google 和 Blogger 们暂时还无法相信这突如其来的幸福吧。

或许是 Google 为了在国内大力推广其 Blogger 服务,在着手升级 Blogger 功能的同时也大力加强公关工作让 *.blogspot.com 在国内真的解封;也有可能是 Blogger 升级让 *.blogspot.com 域名换了 IP 地址,从而暂时绕过了 GFW 的封锁,因为现在 *.blogspot.com 的域名和以前不同了。之前也发生过 Blogger 临时更换 IP 地址导致 Blogger 下站点在短时间内可以访问的事件。

虽然我自己没有用 *.blogspot.com,不会受封锁的影响,但是,解封也意味着可以看到更多 Blogger 上优秀的 blog。希望这次真的是解封了……

Tuesday, March 14, 2006

J2ME 游戏优化探密(三)

循环之外?

for() 循环内的代码,循环多少次,它们就被执行多少次。因此,为了改进性能,我们需要尽可能的把代码放在循环之外。我们在描述器中会发现我们的 paint() 方法被调用了 101 次,并且循环执行了 16 次。我们可以把哪些东西放在循环之外呢?让我们从所有的声明(declaration)开始。每次调用 paint() 方法,我们都声明了一个 Font,一个 String,一个 Image 以及一个 Graphics 对象。我们可以将这些从该方法中挪出来,放到类的顶部。
public static final Font font =
Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD Font.STYLE_ITALIC,
Font.SIZE_SMALL);
public static final int graphicAnchor =
Graphics.VCENTER Graphics.HCENTER;
public static final int textAnchor =
Graphics.TOP Graphics.LEFT;
private static final String MESSAGE = " ms per frame";
private String msMessage = "000" + MESSAGE;
private Image stringImage;
private Graphics imageGraphics;
private long oldFrameTime;

你会发现我把 Font 对象设置成为共有的常量。这在程序中通常很有用,比如这里你可以把常用到的字体在同一个地方集中定义。我发现锚(anchor)也是一样,因此对文字以及图形的锚也做了同样的处理。预先计算这些东西,保证了计算的结果,虽然微不足道,但是我们将它们从循环中挪了出来。

我同样把 MESSAGE 也设置成了常量。这是因为 Java 总是喜欢到处创建 String 对象。如果不控制好,这些字符串将消耗大量的内存。别不以为然,当你过多的消耗内存,将转而影响整体性能,尤其当垃圾收集器被频繁调用的时候。字符串制造垃圾,而垃圾是糟糕的;而使用字符串常量能缓解这个问题。稍后我们会讲到如何利用 StringBuffer 来完全解决由字符串泛滥带来的内存损耗。

现在我们将这些东西变成了实例变量(instance variables),我们需要在构造方法上添加如下代码:
stringImage = Image.createImage( font.stringWidth( msMessage ),
font.getBaselinePosition() );
imageGraphics = stringImage.getGraphics();
imageGraphics.setFont( font );

Graphics 对象提出来,另一点很爽的地方在于,我们只需要对字体设置一次,然后就可以完全不再去管它;而不用每次循环的时候都来设置它。不过仍然每次都需要调用 fillRect() 方法来擦去图像上的内容。编码狂人们可能会想要这样做,从同一个 Image 创建两个 Graphics 对象,然后将其中一个的颜色预设为 COLOR_BG 用来调用 fillRect(),另一个设置为 COLOR_FG 用来调用 drawString()。很遗憾,J2ME 中,对同一个 Image 多次调用 getGraphics() 方法的行为,定义的并不好,不同的平台会有不同的结果,因此你优化的结果可能对 Motorola 有效但 NOKIA 却不行。当无法确定的时候,不要做任何无根据假设。

还有另一个改进 paint() 方法的途径。动动我们的脑子我们会意识到,仅当 frameTime 的值发生改变的时候我们才需要重绘该字符串。这也就是我们引入新的变量 oldFrameTime 的原因。以下是新的方法:
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
if ( frameTime != oldFrameTime ) {
msMessage = frameTime + MESSAGE;
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
stringImage.getHeight() );
imageGraphics.setColor( COLOR_FG );
imageGraphics.drawString( msMessage, 0, 0, textAnchor );
}
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawImage( stringImage, getRandom( getWidth() ),
getRandom( getHeight() ), graphicAnchor );
}
oldFrameTime = frameTime;
}

描述器现在显示 OCanvaspaint() 方法所花费的时间占总时间的百分率已经下降到了 41.02%。相比以前由 drawString()fillRect() 引发的 101 次 paint() 来讲,现在只调用了 69 次。这是相当不错的改进,而且可以继续的空间不多了,这也是我们该认真起来的时候。优化总是越来越难。现在我们要刮掉循环中最后一点冗余的代码。我们可能只能再削减非常少的百分率,但是如果幸运的话,还是可以得到一些显著的改进。

让我们先从简单的开始。相比起调用 getHeight()getWidth() 方法,我们可以只调用它们一次,然后把结果缓存在循环之外。然后我们要停止使用 String,而用 StringBuffer 来手动地处理。我们可以通过调用 Graphics.setClip() 限制绘制区域来减少 drawImage() 调用所花费地时间。最后,我们要避免在循环中调用 java.util.Random.nextInt()

以下是新的变量……
private static final String MESSAGE = "ms per frame:";
private int iw, ih, dw, dh;
private StringBuffer stringBuffer;
private int messageLength;
private int stringLength;
private char[] stringChars;
private static final int RANDOMCOUNT = 256;
private int[] randomNumbersX = new int[RANDOMCOUNT];
private int[] randomNumbersY = new int[RANDOMCOUNT];
private int ri;

以下是新的构造方法……
iw = stringImage.getWidth();
ih = stringImage.getHeight();
dw = getWidth();
dh = getHeight();
for ( int i = 0 ; i < RANDOMCOUNT ; i++ ) {
randomNumbersX[i] = getRandom( dw );
randomNumbersY[i] = getRandom( dh );
}
ri = 0;
stringBuffer = new StringBuffer( MESSAGE+"000" );
messageLength = MESSAGE.length();
stringLength = stringBuffer.length();
stringChars = new char[stringLength];
stringBuffer.getChars( 0, stringLength, stringChars, 0 );

你会发现我们预先计算了 Display 和图像的区域,还缓存了对 getRandom() 方法的 512 次调用,并用 StringBuffer 取代了 msMessage 字符串。当然,最大的目标还是 paint() 方法:
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, dw, dh );
if ( frameTime != oldFrameTime ) {
stringBuffer.delete( messageLength, stringLength );
stringBuffer.append( (int)frameTime );
stringLength = stringBuffer.length();
stringBuffer.getChars( messageLength,
stringLength,
stringChars,
messageLength );
iw = font.charsWidth( stringChars, 0, stringLength );
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, iw, ih );
imageGraphics.setColor( COLOR_FG );
imageGraphics.drawChars( stringChars, 0,
stringLength, 0, 0, textAnchor );
}
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.setClip( randomNumbersX[ri], randomNumbersY[ri], iw, ih );
g.drawImage( stringImage, randomNumbersX[ri],
randomNumbersY[ri], textAnchor );
ri = (ri+1) % RANDOMCOUNT;
}
oldFrameTime = frameTime;
}

我们现在用 StringBuffer 来绘制消息的字符。在 StringBuffer 结尾添加字符,比在开头插入要容易,因此我们调整了显示文字,frameTime 放到了消息的结尾,即“ms per frame:120”。这样我们每次只是更改 frameTime 的几个字符,而消息的文字则原封不动。像这样使用 StringBuffer,能够让系统不至于在每次循环的时候都要创建和销毁 String 对象。这是额外的工作,但是值得。注意我把 frame 强转成了 int,因为我发现使用 append(long) 会导致内存泄漏。我不知道为什么,不过这是很好的一个例子说明为什么在使用工具的时候要用眼睛盯好。

我们还用了 font.charsWidth() 来计算消息图像的宽度,这样我们能够尽量少做绘制的工作。我们使用了一个等宽的字体,因此图像“ms per frame:1”会比“ms per frame:888”小,而我们又在使用 Graphics.setClip(),因此我们不需要绘制多余的部分。这也意味着我们需要绘制一个足够大的矩形来清空我们需要的区域。当然,我们希望在绘图上节省的时间比因调用 font.charsWidth() 而多花的时间要多。

可能这里并不会有太大的意义,但是对于在游戏中在屏幕上绘制玩家的分数来讲,这是一项非常重要的技术。在这个情况下,绘制分数 0 和 150,000,000 是很不一样的。这里受到了 font.getBaselinePosition() 实现的错误返回值的阻碍,因为本来应该返回和 font.getHeight() 相同的值。唉~!

最后我们来看一下用两个数组存放的预计算的“随机”坐标,使得我们不用在循环中进行随机调用。注意这里用了一个取模操作来实现闭合的数组。同时注意我们现在使用 textAnchor 来绘制图像和字符串,这样 setClip() 才能正确工作。

根据这个版本的代码所产生的数字,我们目前处于一个比较尴尬的境地。描述器告诉我,现在 paint() 方法比没有做这些改变时,多花大约 7% 的时间。这可能得怪对 font.charsWidth() 的调用,因为它占用了 4.6%。(这并不太多,但是应该可以被减少。注意我们每次都在取得 MESSAGE 字符串的宽度,实际上我们可以在循环体之前就轻易的计算出,然后再加上 frameTime 的宽度。)同时,对 setClip() 的调用标示的是 0.85%,而花在 drawImage 上的时间看起来增加的比较显著(从 27.58% 到 33.94%)。

基于这一点,看起来当然是这些增加的代码把我们的程序变慢了,但是程序生成的值却和这个假设相矛盾。模拟器上的图表很不稳定,因此如果没有更多的测试我们无法得到确定的结论。然而,我的 i85s 却报告说,加上这些代码后,程序变快了一点点。只调用 setClip() 或者 charsWidth(),结果为 37130ms,而两个都调用得话,结果是 36540。在我耐心能够坚持的情况下,我把这个测试运行了很多遍,而结果是相同的。这也再次强调了不同的运行环境会有不同的结果。当你到了某一点无法确定是否做了改进的时候,你可能被迫在硬件上继续所有的测试,这需要对 JAR 文件进行很多安装和卸载工作。

那么看起来我们从常规的图形操作上挤出了不少的性能。现在是时候对我们的 work() 方法实施高端和低端优化的时候了。先让我们来复习一下这个方法:
public synchronized int work( int[] n ) {
r = 0;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
divisor = getDivisor(j);
r += workMore( n, i, divisor );
}
}
return r;
}

每次进入循环的时候,我们都会传入我们的数组。work() 方法中外层的循环计算我们的除数(divisor),然后调用 workMore() 来实施除法。可能已经发现,这里整件事情都有问题。开始的时候,编程的把对 getDivisor() 的调用放到了内层循环。由于 j 的值在内层循环过程中并不发生改变,因此除数也不发生变化,这完全应该放在内层循环之外。

如果我们再多考虑一点。这个调用本身也是完全没有必要的。以下代码可以做同样的事情……
public synchronized int work( int[] n ) {
r = 0;
divisor = 1;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
r += workMore( n, i, divisor );
}
divisor *= 2;
}
return r;
}

现在我们的描述器报告说我们的 run() 方法花费 23.72% 的时间,而我们做这些改进之前,这个值为 38.78%。在陷入低端优化把戏之前,请总是先用头脑来优化。不过,说到这里,还是让我们来看看这些小把戏。

(一) (二) (三) (四) (五)

Monday, March 13, 2006

J2ME 游戏优化探密(二)

哪些地方需要优化——九一原则

在性能较低的游戏中,90% 的时间被花费在运行 10% 的代码上。这 10% 的代码正是我们需要集中所有努力优化的地方。我们用一个描述器(profiler)来找出这 10% 的代码。要打开 J2ME Wireless Toolkit 中的 Profiler Utility,可以在 Edit 菜单中选择 Preference 项,打开 Preference 窗口,选择 Monitoring 标签,勾选标有“Enable Profiling”的复选框,然后点击“OK”按钮。没有任何事情发生?没关系——我们需要在模拟器中运行我们的程序,然后退出,这样 Profiler 窗口才会出现。现在就试试看吧!

Figure 1. 演示如何打开 Profiler Utility


我的模拟器(运行在 Windows XP 下,Intel P4 2.4GHz CPU)报告 100 次循环花费了 6,407ms,即 6 秒半种,每帧 61-63ms。在硬件(Motorala i85s)上,它运行慢许多。没帧的时间在 500ms 左右,而整个程序跑了 52460ms。在这篇文章种,我们将尝试改进这一现象。

当你退出程序时,描述器窗口将弹出来这样你就能看到一个类似于文件夹浏览器的东西,左边面板有一棵树,它分级显示了方法之间的关系。每个文加夹都是一个方法,打开一个方法的文件夹显示所有该方法调用的其它方法。在树中选择一个方法,将在右边窗口中显示该方法的描述信息以及所有其它被它调用的方法。注意每个元素附近显示有一个百分数。这个是该方法运行花费的时间占总运行时间的百分比。我们需要浏览这棵树,找出时间到花到哪里去了,然后尽可能地优化那些占有最高百分比的方法。

Figure 2. Profiler Utility 调用表单


关于描述器,有些地方需要注意。首先,你所得到的百分比可能和我的会不同,但是方法类似——总是追踪那些最大的数字。我每次运行程序,这些数字都不同。在做测试时,请尽可能地保持环境统一,你可能需要关掉所有的后台应用程序,比如电子邮件客户端等等。同时,在使用描述器之前也不要采取任何代码保护措施(如某些让源代码变混乱以防被反编译的工具),否则你的方法会被莫明奇妙的命名为“b”、“a”或者“ff”之类。最后,描述器本身对性能没有任何帮助,不管你模拟什么设备。硬件本身是完全不同的东西。

打开具有最高百分率的文件夹,我们发现 66.8% 的运行时间被名为 com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run 的方法消耗,这对我们帮助不大。继续向下挖掘一两层具有类似奇怪名称的方法文件夹,你将追踪到大的百分比方法 serviceRepaints(),然后最终到我们的 OCanvas.paint() 方法。另外 30% 的时间被它消耗。这两个方法都存在于游戏的主循环终,这并不奇怪——我们不会花费时间去优化 MIDlet 类中的代码,同时你也不会去为优化游戏主循环之外的代码费心。只优化那些反复运行的部分。

我们例子中这些百分数的分配和真实游戏的情况不会差太远。你会发现,一个真实视频游戏的绝大部分运行时间的大部分都被花费在 paint() 方法上。和非图像处理的部分比起来,图像处理的常规操作会花费非常多的时间。遗憾的是,图像处理的常规操作早已封装在 J2ME API 之下,在这一点上,我们没有多少余地用来改善其性能。我们所能做的是,聪明地决定我们用它们中的哪一个,以及如何使用它们。

高端优化 VS. 低端优化

本文后面将讨论有关低端优化技术。你将发现它们很容易应用到已存在的代码中,虽然会降低代码的可读性,但是会带来性能的提升。在你使用这些技术之前,最好先改善你代码的设计以及规则(algorithms),也就是高端优化。

Michael Abrash,id software 力作 Quake 开发者之一,曾经写到,“最好的优化工具,在你的双耳之间”。一个问题总有不止一个解决办法,如果在行动之前花些时间来考虑正确的方法,则可以达到事半功倍的的效果。使用正确的(比如最快的)规则体系,对性能的帮助将远远大于用低端技术来改进二流的设计带来的效果。用低端技术你可能可以降低几个百分点,但是之前请从高端优化开始,多用你的脑子——它就在你的双耳之间。

那么,让我们来看看我们在 paint() 方法里面干了些什么。每次循环我们调用了 16 次 Graphics.drawString() 方法用来在屏幕上显示“n ms per frame”。我们并不知道 drawString 方法内部的工作原理,但是我们知道它被用了很多次,于是让我们尝试一下另一种途径。我们可以把字符串直接一次性画到一个 Image 对象中,然后将该 Image 绘制 16 次。
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
Font font = Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC,
Font.SIZE_SMALL );
String msMessage = frameTime + "ms per frame";
Image stringImage =
Image.createImage( font.stringWidth( msMessage ),
font.getBaselinePosition() );
Graphics imageGraphics = stringImage.getGraphics();
imageGraphics.setColor( COLOR_BG );
imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
stringImage.getHeight() );
imageGraphics.setColor( COLOR_FG );
imageGraphics.setFont( font );
imageGraphics.drawString( msMessage, 0, 0,
Graphics.TOP | Graphics.LEFT );
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawImage( stringImage, getRandom( getWidth() ),
getRandom( getHeight() ),
Graphics.VCENTER | Graphics.HCENTER );
}
}

当我们运行这个版本的程序时,我们会发现在 paint() 方法上花费的时间的百分率变小了一点点。更深入的观察我们会发现 drawString 方法仅被调用了 101 次,而 drawImage() 方法做了大部分的动作,被调用了 1616 次。尽管如此我们还是有成效,程序跑得更快了,因为我们使用的图形调用更快。

你可能会注意到将字符串绘制到图像会影响显示,因为 J2ME 并不支持图像的透明处理,因此很多背景被覆盖。对于前面所提到的,优化会导致你对应用的需求重新进行评估,这便是一个很好的例子。如果你真的需要文字的重叠,那么你可能就不得不去处理较慢的运行速度。

这端代码可能会稍好一些,但是仍然有很多优化的空间。接下来我们一起看看我们的第一个低端优化技术。

(一) (二) (三) (四) (五)

J2ME 游戏优化探密(一)

这是一篇翻译自 Mike Shivas 的文章,如果对原文感兴趣,可以访问英文原著

如果您对本译文有任何疑问或者意见,请直接对本 blog 文章发表评论,或者给我发送电子邮件


本文描述了优化在为移动设备编写游戏中所扮演的角色。笔者将通过范例说明,如何、何时以及为何要通过优化编码来“压榨” MIDP 相关手持设备上的最后“一滴”性能。我们将讨论为什么优化是必要的以及什么时候通常最好不要优化。笔者将解释高端和低端优化之间的区别,同时学会如何使用同 J2ME Wireless Toolkit 一起发布的 Profiler 工具。最后这篇文章将揭示很多方便您移植 MIDlets 的技术。

为什么要优化?

我们大致可以把视频游戏宽泛地分成两类:实时游戏和输入驱动游戏。输入驱动游戏显示游戏当前的状态,在继续工作之前,会无限制地等待,直到有用户输入发生。棋牌类游戏就属于此类,还有大部分迷题类、策略类以及文字型冒险游戏。实时游戏,有时候称为技巧或动作游戏,并不会等待游戏者,他们会一直工作,直到 Game Over。

技巧或动作游戏的特性,通常体现在屏幕上大量地移动(想象一下 Galaga [注]或者 Robotron [注])。刷新频率必须至少有 10 fps (frame per second,每秒帧数),并且要有足够的动作来保持游戏有挑战性。这些游戏需要游戏者有快速的反应能力以及良好的眼手协调能力,因此成功的 S&A 游戏同样要求对用户的输入有非常好的回馈。提供在高刷新率的图形运算的同时对按键进行快速的响应,正是为什么实时游戏需要高效率编码的原因。在使用 J2ME 进行开发的时候,这更是一项极大的挑战。

Java 2 Micro Edition (J2ME) 是一个被修剪过的 Java 版本,适合能力非常有限的小型设备,比如手机和 PDA。J2ME 设备有如下特征:
  • 受限的输入能力(没有键盘!)
  • 显示区域小
  • 有限的存储空间和堆
  • 较慢的 CPU

用 J2ME 平台来开发游戏,对于开发者来讲是一个挑战,因为他们编写的代码需要运行在远远慢于台式机的 CPU 之上。

什么时候不优化?

如果你不是在编写一个技巧或者动作游戏,那么可能没有必要进行优化。如果游戏者每几秒钟或几分钟才进行下一步操作,那么他可能并不会介意你的游戏在响应他的动作时多花了几百毫秒。这条准则的一个例外,是当游戏在决定下一步操作时,需要进行大量的运算,比如在上百万种可能的棋谱中进行搜索,那么你可能需要对代码进行优化,以确保下一步能在几秒钟内计算出来,而不是几分钟。

如果你在编写这类的游戏,优化的过程可谓艰辛。很多这样的技术通常伴随着其它的代价——它们不再是传统意义上的“好”程序,它们使得你的代码不易阅读。我们需要做出权衡,因为有的时候为了取得那么一点点性能上的改善,却需要我们显著地增大一个程序。J2ME 的开发者都太熟悉要尽可能地让 JAR 包变小。以下是更多不进行优化的理由:
  • 优化很容易引入 bug
  • 有些技术会降低代码的移植性
  • 你花费了很多努力却只有很小甚至没有结果
  • 优化真的很难做

最后一点需要澄清的是,优化是一个变化的目标,在 Java 平台上是这样,在 J2ME 平台上更是如此,因为运行环境千差万别。你优化过的代码在某个模拟器上可能更快,但是在真实设备上却更慢,反之亦然。在某一种手机上的优化可能实际上降低了在另一种上的性能。

但是希望还是有的。在优化的时候,我们有两种程度,高端的和低端的。前者类似于在所有的平台上提高运行性能,提高代码的整体质量;而后者可能更让你头痛,不过这些低端技术比较容易引入,并且在你不需要他们时也更容易忽略。至少,它们看起来非常有趣。

我们同样用系统时钟来描述在实际设备上的代码,这将帮助你估算在要部署的目标硬件上,这些技术到底多有效率。

最后,重要的一点——

优化充满了乐趣!

一个反面范例

现在让我们来看一个简单的例子,它有两个类,首先是 MIDlet——
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class OptimizeMe extends MIDlet implements CommandListener {
private static final boolean debug = false;
private Display display;
private OCanvas oCanvas;
private Form form;
private StringItem timeItem = new StringItem( "Time: ", "Unknown" );
private StringItem resultItem =
new StringItem( "Result: ", "No results" );
private Command cmdStart = new Command( "Start", Command.SCREEN, 1 );
private Command cmdExit = new Command( "Exit", Command.EXIT, 2 );
public boolean running = true;
public OptimizeMe() {
display = Display.getDisplay(this);
form = new Form( "Optimize" );
form.append( timeItem );
form.append( resultItem );
form.addCommand( cmdStart );
form.addCommand( cmdExit );
form.setCommandListener( this );
oCanvas = new OCanvas( this );
}
public void startApp() throws MIDletStateChangeException {
running = true;
display.setCurrent( form );
}
public void pauseApp() {
running = false;
}
public void exitCanvas(int status) {
debug( "exitCanvas - status = " + status );
switch (status) {
case OCanvas.USER_EXIT:
timeItem.setText( "Aborted" );
resultItem.setText( "Unknown" );
break;
case OCanvas.EXIT_DONE:
timeItem.setText( oCanvas.elapsed+"ms" );
resultItem.setText( String.valueOf( oCanvas.result ) );
break;
}
display.setCurrent( form );
}
public void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
oCanvas = null;
display.setCurrent ( null );
display = null;
}
public void commandAction(Command c, Displayable d) {
if ( c == cmdExit ) {
oCanvas = null;
display.setCurrent ( null );
display = null;
notifyDestroyed();
}
else {
running = true;
display.setCurrent( oCanvas );
oCanvas.start();
}
}
public static final void debug( String s ) {
if (debug) System.out.println( s );
}
}

然后,OCanvas 做了本例中大部分的工作——
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.Random;
public class OCanvas extends Canvas implements Runnable {
public static final int USER_EXIT = 1;
public static final int EXIT_DONE = 2;
public static final int LOOP_COUNT = 100;
public static final int DRAW_COUNT = 16;
public static final int NUMBER_COUNT = 64;
public static final int DIVISOR_COUNT = 8;
public static final int WAIT_TIME = 50;
public static final int COLOR_BG = 0x00FFFFFF;
public static final int COLOR_FG = 0x00000000;
public long elapsed = 0l;
public int exitStatus;
public int result;
private Thread animationThread;
private OptimizeMe midlet;
private boolean finished;
private long started;
private long frameStarted;
private long frameTime;
private int[] numbers;
private int loopCounter;
private Random random = new Random( System.currentTimeMillis() );
public OCanvas( OptimizeMe _o ) {
midlet = _o;
numbers = new int[ NUMBER_COUNT ];
for ( int i = 0 ; i < numbers.length ; i++ ) {
numbers[i] = i+1;
}
}
public synchronized void start() {
started = frameStarted = System.currentTimeMillis();
loopCounter = result = 0;
finished = false;
exitStatus = EXIT_DONE;
animationThread = new Thread( this );
animationThread.start();
}
public void run() {
Thread currentThread = Thread.currentThread();
try {
while ( animationThread == currentThread && midlet.running
&& !finished ) {
frameTime = System.currentTimeMillis() - frameStarted;
frameStarted = System.currentTimeMillis();
result += work( numbers );
repaint();
synchronized(this) {
wait( WAIT_TIME );
}
loopCounter++;
finished = ( loopCounter > LOOP_COUNT );
}
}
catch ( InterruptedException ie ) {
OptimizeMe.debug( "interrupted" );
}
elapsed = System.currentTimeMillis() - started;
midlet.exitCanvas( exitStatus );
}
public void paint(Graphics g) {
g.setColor( COLOR_BG );
g.fillRect( 0, 0, getWidth(), getHeight() );
g.setColor( COLOR_FG );
g.setFont( Font.getFont( Font.FACE_PROPORTIONAL,
Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ) );
for ( int i = 0 ; i < DRAW_COUNT ; i ++ ) {
g.drawString( frameTime + " ms per frame",
getRandom( getWidth() ),
getRandom( getHeight() ),
Graphics.TOP | Graphics.HCENTER );
}
}
private int divisor;
private int r;
public synchronized int work( int[] n ) {
r = 0;
for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
for ( int i = 0 ; i < n.length ; i++ ) {
divisor = getDivisor(j);
r += workMore( n, i, divisor );
}
}
return r;
}
private int a;
public synchronized int getDivisor( int n ) {
if ( n == 0 ) return 1;
a = 1;
for ( int i = 0 ; i < n ; i++ ) {
a *= 2;
}
return a;
}
public synchronized int workMore( int[] n, int _i, int _d ) {
return n[_i] * n[_i] / _d + n[_i];
}
public void keyReleased(int keyCode) {
if ( System.currentTimeMillis() - started > 1000l ) {
exitStatus = USER_EXIT;
midlet.running = false;
}
}
private int getRandom( int bound )
{ // return a random, positive integer less than bound
return Math.abs( random.nextInt() % bound );
}
}

本例程序是一个 MIDlet,模拟一个简单的游戏循环:
  • 计算
  • 画图
  • 处理输入
  • 反复

对于高速的游戏,这个循环应该尽可能的紧凑。我们的循环进行有限的次数 (LOOP_COUNT = 100),并且用一个系统时钟来计算整个过程花费了多少时间(毫秒),这样我们就能测量并且改进其性能。时间和结果显示在一个简单的表单中。用“开始”命令来启动测试,按任意键中止循环,用“退出”命令退出程序。

在大多数游戏中,游戏主循环的“计算”部分包含了对游戏世界状态的更新——移动主角,测试和响应碰撞,更改分数等。在这个例子中,我们并没有做任何有意义的操作,而只是简单的运行一个数组,对其中每个数字进行一些数学计算,并给出一个总的计算结果。

run() 方法同样计算出每次循环所花的执行时间。每一帧,OCanvas.paint() 方法都将在屏幕上 16 个位置的其中随机之一显示这个时间的毫秒数。正常情况下你可能会在这个方法中绘制游戏中的元素,这里我们的代码只是对这个过程提供一个合理的模拟。

不管这段代码看起来多么没有意义,它都将给我们提供很大的空间来改善运行性能。

(一) (二) (三) (四) (五)