“Mac OS X”目录存档

接受调试器

2010年08月18日,星期三

⋯⋯ 太多时候,用 printf 或者 log 就能发现问题。一些人于是发誓只用这种方法,而且如人所愿,只要时间充裕,力气下足,其实不用 debugger 也能干活。

—— Kernel Programming Guide

十年前刚毕业的时候,在公司里被分配了第一个像样的任务:用 Java 写一个 GUI editor 。准备开发环境的时候遇到一个问题: 对当时的我来说,和 Visual C++ 之类的工具相比,JDK 的 debugger 太难配置了。另一方面,System.out.println 倒是唾手可得的『利器』。最终我完全用后者调试完成了那个 5000 多行的 GUI editor 。之后的七年里我几乎再没有碰过任何 debugger 。对它的全部认识就是设置断点,运行程序 hit 断点之后单步(step in/over)。每次搭建开发环境的时候似乎都会因为因为这样那样的原因而放弃使用 debugger ,printfprintln 乃至 log 文件是不二的应对法宝。

直到三年前重新回到 Visual Studio 和 Xcode 的 IDE 环境之后,抱着反正不用费力气配置直接可以用的态度重新拾起 debugger 。下面是大致按照时间顺序的发现:

第一条:call stack

最初七年里无数次从 Java exception 的 stack trace 里获得解决问题的线索,也在研究 Linux 代码的过程中意识到能随时了解 call stack 的好处,可是我从把 call stack 和 debugger 联系上。重拾 debugger 之后的第一个新认识是它的作用不光是单步,更重要的是可以揭示 call stack 。如果没有 debugger 和 call stack ,阅读已有的 code base 的难度会提高一个数量级。

第二条:死锁

多线程问题和性能问题被我认为是 debugger 一无用处而非 log 不可解决的难题。直到两年前对着一个debug 状态下僵死的程序做了一个 pause 操作,debugger 显示的 call stack 稳稳当当的停在了死锁的位置。

第三条:UI 操作

UI 问题被认为是最容易应用 debugger 的,因为一般来说触发断点最直接,而且线程也不多。但是遇到 debugger 会干扰程序本身的 message queue 的时候就束手无策了。为了尽量避免手工加入 logging 代码,也找了各种土办法减少 debugger 对 message queue 的干扰。

比如,如果在窗口绘制代码中设置断点,就会让 debugger 在绘制窗口的过程中夺取焦点,continue 之后又会把焦点返回原来的被调试程序的窗口,在 Windows XP 上就会又触发窗口绘制。而 OS X 和 Windows Vista 的 buffered window 能让窗口绘制的代码不会像 Windows XP 里的窗口那样每次在 debugger 把焦点交还的时候都触发窗口绘制。尽量利用不同系统的消息处理差异来找到最合适解决问题的环境。

但是这些方法都要靠系统实现的差别和被调试程序逻辑的机缘巧合,并非每次都能成功使用。无法找到一定之规,有的情况也根本没有办法。比如,有些控件会处理失去焦点的事件,用如果在处理失去焦点的代码里加入断点,调试起来就很难分别哪些事件是正常情况下会发出的,哪些是受 debugger 的干扰发出的。

困惑了两年多之后终于发现处理 UI 调试的方法其实简单得令人发指。只要能找到纯命令行的 debugger(比如 gdb,Flash 的 fdb),用 ssh 或者 telnet 从另一台机器登录到被调试程序所在的机器,用命令行 debugger 调试就行。这时,任何断点的中断都是通过后台协议传递给 ssh/telnet client 上的终端而不会干扰被调试机器的 message queue 。回顾过去的十年,很惊讶被这个问题困扰了那么长时间的不光是我,还有我询问过的几乎所有同事,几乎每个人处理这种问题都是依靠 log(不得不说有些人,比如志岩还是提出了 remote debugging 的思路,但是很可惜只是一个灵感而不是整套解决方案)。

第四条:条件断点

如果一个断点的第一次、第二次、⋯⋯ 第 n 次被 hit 的时候你都并不想单步或者检查状态怎么办?当然可以多点几次 debugger 的 continue 按钮,让真正期待的第 n+1 次 hit 到来。但是如果第一次的 hit 会干扰第二次呢?(我听见有人说可以用 ssh+命令行 debugger 调试。不过大多数情况没有必要祭出 remote debugging 。)又或者 n 是 10000 怎么办?为了对付这种情况用了不少土方法,比如临时给被调试的程序代码增加一个分支特地用来设置断点,或者在程序弹出 modal dialog 的时候设置断点。

其实 gdb 一类的 debugger 一直都有条件断点的功能。可以让一个断点并不是每次 hit 的时候,而是满足一些额外条件的时候才中断,比如第 n 次被 hit ,或者一个变量大于某个值,或者一个 C 函数的返回值符合条件的时候 ⋯⋯

第五条:监视变量

你是否曾经大声咒骂过:这个变量到底什么时候被改动的?然后不得不一步步的 step over 查看,发现值发生变化之后,停止调试,重启程序,重复刚才的 use case ,step over 到刚才的地方,step into ,然后再 step over ⋯⋯

其实 gdb 和很多 debugger 都能自动监视变量的变化,并且在变化符合某种条件的时候自动中断。条件的配置和条件断点一样灵活。而且 x86 CPU 为了支持这种功能特地设置了几个 debugging resigter (32 位下四个,64 位下更多),所以监视变量几乎不会影响调试速度。不用这个功能都对不起 x86 CPU 上那些专门用来 debug 的硅。

第六条:Log

最终,还是会有些问题确实需要在不间断的执行中监视某些状态。也就是 log 。可是 log 必须得是在代码里加入 printf 吗?gdb 的 commands 命令让断点在 hit 的时候可以执行一段命令,任何合法的 gdb 命令,比如打印某个变量之后 continue 。

结论

… In many cases, you can find the problem through careful use of printf or IOLog statements. Some people swear by this method, and indeed, given sufficient time and effort, any bug can be found and fixed without using a debugger.

—— Kernel Programming Guide

接受 debugger 是一个抛弃原有成见的漫长过程。一开始我们总认为自己的土方法是最有效的(因为当我们抱着进入尖端领域的心情开始软件开发的生涯时,周围的前辈总会教你几个土方法)。然后我们知道真的有先进的工具。我还远远未了解所有 debugger 的所有功能。但是配置一个 debugger 是将我在所有开发平台上的第一件事。

地址空间划分(一)

2010年08月9日,星期一

研究过 32 位 Linux 内核的人都知道这个内核著名的 3G/1G 划分:低 3G 作为用户态空间,高 1G 作为内核态空间。内核态和用户态共享 4G 的 32 位地址空间。

这个划分成了 32 位内核的基本设计方式。Windows 内核也采用类似的划分(缺省为 2G/2G 划分,可以通过启动参数改为 3G/1G)。所以,可能很多人像我一样,很自然的把这种划分当成了内核的必要设计:内核态和用户态必须共处在同一个地址空间里;似乎如此内核才能管理用户态的内存。见到 Mac OS X 内核 XNU 的设计颠覆了我的观点。XNU 的内核态独占 4G 内核空间,而非与用户态共享。(如下左图显示。)

如果不是因为一个采用如此设计的内核就摆在面前(而且已经在上面工作了三年),我几乎不会想到内核的地址空间还能如此设计。看到这个设计的同时也豁然开朗:一开始认为『内核态和用户态必须共处在同一个地址空间』才能让内核『管理用户态的内存』的想法实在是短路。内核并不能了解用户态如何使用内存,更重要的是,内核并不能信任用户态内存的内容。所以,它根本不能通过和用户态『共处在同一个地址空间』这种方式来管理后者的内存空间。因为『共处在同一个地址空间』提供的唯一功能只是可以直接通过线性地址访问(也就是俗称的裸指针),而内核绝对不能碰用户态的裸指针。内核管理用户态内存的方式只能是通过设置页表和页目录来保证用户态的内存访问的合法性和一些特殊映射。

再者,Linux 和 Windows 下与内核共享地址空间的用户态只能是当前运行的进程。如果是通过系统调用进入内核,还多多少少可以说这时的当前进程和内核的关系比其它进程密切一些。如果是通过中断(时钟或者I/O)进入的内核,这时哪个进程是当前进程对内核来说完全是巧合。更何况无论如何内核也不可能只管理当前进程的内存空间。所以从这个角度说共享地址空间对内核管理用户态内存也没有帮助。

退回到 Linux 和 Windows 的设计,为什么还要让内核与用户态共享地址空间呢。其实是因为尽管每次进入内核态都可能发生进程切换,但是大多数情况下并非一定发生这样的切换。因此,共享地址空间可以避免进出内核态的时候进行地址空间(cr3)的切换。在 x86 构架下,切换地址空间要导致所有 TLB 失效,CPU 必须访问主存更新 TLB。所以,共享地址空间是一个性能 hack,仅此而已。这个性能 hack 如此历史悠久,以至于有些人如我一般完全无法想像还能有其它方式。

不过,XNU 的内核地址空间也并非和用户态空间绝对分离。在每个用户态地址空间的最高几兆也被保留为内核空间(如上右图所示)。这是因为从用户态切换到内核态(通过软中断或者比较新的 x86 syscall 指令)时,并不能同时切换地址空间,所以必须保留这段短短的共享空间来完成地址空间的切换。其实,我们可以把这个设计想像成一个处于 micro-kernel 和 monolithic kernel 之间的中间设计。和 micro-kernel 类似,内核也享有独立空间,但是和 monolithic kernel 类似,整个内核都是处在特权级,没有从微内核到内核服务的二次切换。

永远的页

2010年06月15日,星期二

最近看到有人质疑在今天的电子设备上阅读是否还需要保留『页』这个概念。他们认为『页』是印刷品时代的产物,在电子出版时代除了怀旧没有任何意义,电子出版物已经有了『页』的完美替代品:竖直滚动条。

对我来说,竖直滚动条平滑移动功能的直接后果之一是,在阅读的时候,你会情不自禁地试图避免阅读窗口(或者设备屏幕)边缘处的文字 —— 确实,它们太边缘了,因为没有页只有滚动条的内容没有上下留白,而对这种缺乏留白的逃避会延伸到你正在阅读的那四五行以外的一切文字,从而会无时无刻不让你希望把自己正在看的句子调整到窗口(屏幕)的正中间。于是窗口(屏幕)的大小失去了意义,读者的视界实际上被压窄到四五行。阅读的动作成了以句子为频度的『读 —— 滚动 —— 读』(甚至不是有阅读器还发明了自动卷动功能么)。竖直滚动条成了一个分散注意力的障碍而非方便的阅读工具。更糟糕的是,有了滚动条,文章的编辑者更难把握读者会看到什么样的页面布局,所以很多时候他们也就放弃了这样的思考。结果就是我经常在阅读的时候实在搞不好把一幅插图放到窗口的什么位置最协调(通常我发现最好的位置是赶紧把这个图片滚出窗口)。这实在是比『页』糟糕很多的阅读体验。

我很欣赏 Kindle for Mac 的做法:根据当前窗口大小把窗口的内容区域作为一页,不提供竖直滚动条,只提供翻页操作。因为没有滚动条,这是让我分心最少的一款阅读器。同时根据窗口大小动态的定义页的大小,也避免了在电子设备上照搬印刷品布局的古怪体验。我猜想在 iPad 和 iPhone 上的 Kindle 和 iBooks 也是类似的操作,因为没有窗口分割,所以页面的大小能更简单的固定为设备屏幕的大小 —— 但是 Kindle 没有进入 China App Store 所以我没有用过其 iPhone 版,也没有机会深入使用 iPad。

回顾一下,其实『页』这个概念还真的不一定就是印刷品时代仅仅因为技术局限而留下的遗产。古代人类不就使用过所谓的『卷轴』么?倘若假以改进,『卷轴』印刷品的便携性和不考虑到阅读注意力的单纯操作性不一定就输给『页』。所以我想,在印刷品时代,『页』并不是没有竞争者的无奈选择,而是在选择中胜出的更优秀方案。到了电子出版时代,人类历史依然会不断掀开新的一页。

Dict Mac 的发音功能

2010年06月8日,星期二

对于词典软件来说,发音是个必需具备的基本功能,而且 dict.cn 的 Web API 提供了 MP3 格式的单词(短语)发音,所以从开始写 Dict Mac 的起,发音就被列入计划加入的功能。0.02 版完成之后,也就是从 5 月 25 日左右开始准备实现发音功能。

最初的问题是找到合适的 API,花了一个晚上,从 blog 和论坛的帖子堆里发现无数名称,逐一在 Apple 的文档里查找印证,最后终于锁定 Audio Queue Service 这个 framework。有了 framework 的确切名称后就不必再流连于 Stackoverflow 之类的网站,开始专心阅读 Apple 的文档。

花了一个晚上草草看了一下《Audio Queue Service Programming Guide》,感觉不是很好。这个 framework 功能比较强大,设计得比较灵活,相对来说也就没有提供傻瓜化的播放 API,即使是播放一个 MP3 文件也要 400 多行代码。好在这些代码在 programming guide 里都已经直接给出。但是一来我不喜欢这种 copy/paste 代码的方式,二来如果这些代码有 bug(当然 programming guide 里的代码本身有 bug 的可能性不大,但是和 Dict Mac 的 code base 集成的时候很可能引入问题),也要花不少时间解决,而且很可能要重读 programming guide 甚至查阅 API reference。

另外的问题是选择把 dict.cn 的 MP3 文件 download 到本地文件系统之后再打开文件播放的方式,还是直接播放网络的流数据的方式。从尽量减小暴露给用户的复杂度的角度说,稍稍倾向后者。但是考虑到今后还要实现本地缓存,以及尽早实现功能,还是决定先写出把 MP3 文件按照单词作为索引存储在本地文件系统的代码。花了两个晚上写好了这些代码后,第二天白天我脑子里突然冒出一个问题:OS X 会不会有现成的播放 MP3 的命令行工具?从以往的经验看,OS X 的命令行工具还是相当强大的。Google 了一下,果然有一个 afplay。这样一下子把实现发音功能的剩余工作从至少两个晚上变成了一个小时 —— 只要 fork 一个新进程运行 afplay 即可。

其实刚开始写 Dict Mac 的时候就考虑过是不是把必要的 UI 之外的其它功能都做成一个后端独立进程。最后决定当前阶段还是写成一个进程。不过 afplay 至少让发音功能成为了一个独立的进程。一开始我还在抱怨为什么 Mac OS X 不能直接提供一个播放 MP3 的傻瓜接口,其实这样的傻瓜功能应该做成工具而不是 API,像 Audio Queue Service 那样灵活的接口才值得作为 API 的形式提供。这个事情上 OS X 继承了 UNIX 的精髓,也是像 Windows 这样的不管底层功能还是傻瓜功能一股脑的做成 API 的非 UNIX 类的 OS 最缺乏的文化。

Git、P4merge 和 OS X

2010年05月27日,星期四

Diff 是程序员最重要的工具之一。不过,除了在 mail 里发送 patch,我很难忍受传统 diff 的字符输出。在任何系统上开发软件,第一件重要的准备工作就是寻找趁手的 GUI diff 工具。GUI diff 工具的优势在于 side-by-side 的比较方式即能显示变化的部分,又不影响对每个版本的连续阅读。Linux 上比较好的是 KDE 的 kompare,也是我见到的第一种 GUI diff 工具。因为先入为主的看到 kompare 使用曲线来联系变化的对应关系,所以任何使用空行填充或者直线来显示对应关系的 GUI diff 工具对我来说也是比较勉强的 —— 可以暂时使用,但是仍然会不断寻找替代品。

平时工作用 Perforce 管理代码,它有个自带的 GUI diff 工具 P4merge。功能很强大(而且免费,虽然 Perforce 是商业软件,但是它的 client,包括 P4v 和 P4merge 都是免费的,而且 P4merge 可以完全脱离 Perforce server 独立使用),除了普通的比较两个文件(版本)之外还可以 3-way 比较(一般用来做人工的 branch merge,分别比较 branch fork 之前的原始版本,main branch 上的修改版本,和其它 branch 上的修改版本)。和 editor 不同的是,diff 工具操作的不仅仅是 check-out 的 version,而是 version repository 里的任意版本。所以 diff 工具要想和 version control 系统配合,简单的文件操作是不够的,一个成熟的 version control 系统必须在设计中考虑如何与外部的 diff 工具协作。

从上周开始用 Git 管理 Dict Mac 的 code base。用到昨天终于感到缺省的『 git diff 』不好用(类似『 diff -u 』的输出)。简单的 Google 一下就找到了答案:

git difftool …

原本以为只有在 Linux 上才会有缺省配置好的 GUI diff 工具和 Git 协作。在 Mac 上试过发现 OS X 上已经配置好了 opendiff 可以让 Git 直接使用。OS X 的 out-of-box 用户体验确实不是只用来蒙初学者的。连 developer 的工具也能准备齐全。但我还是最习惯 P4merge。于是试着配置:

git config –global diff.tool p4merge
git config –global \
difftool.p4merge.cmd /Applications/p4merge.app/Contents/MacOS/p4merge

然后运行『 git difftool 』,P4merge 是调用起来了,但是没有内容。原来 Git 缺省不会向 diff 工具传递参数,必须写到命令行配置里:

git config –global \
“difftool.p4merge.cmd \
/Applications/p4merge.app/Contents/MacOS/p4merge \”\$LOCAL\” \”\$REMOTE\”"

这里还有一个陷阱,在某些 shell 里,『 $ 』是不用加转义的。不过在 OS X 的 bash 里必须加。

P4mergeOnGit.png

Dict Mac 到 Mac OS X 10.5

2010年05月25日,星期二

把 Dict Mac 介绍给同事们之后遇到最多的回应是『怎么是 10.6 only 的?』我喜欢用最新的东西,加上在家里测试 10.5 也并不方便,所以开发的时候没多想就选了 10.6 SDK。没想到连公司同事这些 Mac 的重量级用户还有不少没升级到 10.6,于是今天中午花了一个小时把最低平台需求降到了 10.5。其间遇到了几个小问题。

第一是 NSApplication 的 delegate 在 10.5 之下没有类型,在 10.6 下则正式定义为 NSApplicationDelegate 接口。不过真正起作用的 selector 都没有变化。把 delegate 的基类从 NSObject <NSApplicationDelegate> 变成 NSObject 之后就解决了问题。第二个问题是 NSTextView 的背景色设置成 [NSColor clearColor] 在 10.6 下工作的很好,到了 10.5 下就变成了黑色,变成 [NSColor windowBackgroundColor] 才行。虽然效果相同,但是从正常思路来说我是希望让控件的背景透明,而不是让它『碰巧』和窗口同色,而且遇到那些背景色可以在运行的时候定制的窗口,非透明的控件还要多写代码来同步颜色。

上面两个问题解决之后,Dict Mac 就可以正常的用 10.5 SDK 编译了,在 OS X 10.6.2 上运行也一切正常。之后遇到的问题最离奇,程序在 OS X 10.5 上启动后就会崩溃,crash log 报告 selector [DictWindow awakeFromNib] 不存在。Google 之后发现遇到类似问题的人不少,唯一找到的 workaround 是把 [DictWindow awakeFromNib] 里的 [super awakeFromNib] 去掉(完全莫名其妙的改法,这是一个人记得刚刚升级到 Xcode 3.2 的时候并没有这个问题,然后从自己的 version control 找到当时的代码才得到的解决方案)。

降级 SDK 总会遇到一些小麻烦。也能从中看到 SDK 的每次升级都确实给开发工作减轻了不少负担,唯一遗憾的是用户们升级得没有那么快,新 SDK 的推出带来的好处要过上一段不短的时间才能让开发者受益。

网络改造

2010年04月13日,星期二

长久以来家里的网络由一套繁复的配置构成。首先是网通的 ADSL modem 连接电话线;然后是关闭了无线功能的 Netgear 无线路由器连接 ADSL modem,负责 ADSL 拨号;最后是 Apple AirPort Express 用桥接(bridge,也就是 AirPort 本身没有 IP 地址)方式把 Netgear 的网络扩展到无线。这么麻烦有两个原因:第一,Netgear 无线路由器太旧,所以 Mac OS X 10.5.6 之后的系统和 Netgear 无法正常工作(网速奇慢);第二,ADSL modem 太旧,Apple AirPort Express 没法驱动它无线拨号。

这么凑合着用了一年多,最近不知何故网速越来越慢。于是用 MacBook 直接连接 ADSL modem 想看看故障出在哪个环节。发现原来家里的 MacBook 和 MacBook Pro 也都没法在这个 modemo 上拨号。总之只要是 Apple 的设备和这个 modem 就不对付。还好在别人家的 ADSL 上用 MacBook (Pro) 成功拨号连接过,所以能证明问题不是出在 Mac 上(或者至少只是一个对旧协议的兼容问题)。

这一年凑凑合合用网络的体验,加上发现这个 modem 不能支持 Mac,终于决心请半天假让网通的人过来修一下。网通的人来了之后看了半天没看出什么所以然(老是叨唠在 PC 上出什么 618 错误在 Mac 上看不到),拿个新型号的 modem 一连我的 MacBook 就好了。赶紧掏钱让来人留下新 modem 离开。

打发走网通的人之后赶紧用 AirPort Express 连上新 modem,傻了,ping 外网不通。换上 Netgear 的路由器,傻了,新 modem 的指示灯也不闪。靠,刚才光顾着用 MacBook 连上高兴,忘了当面用路由器试一下了。心想打电话兴师问罪之前还是确定一下,用 MacBook 连上接着新 modem 的 Netgear,ping 通了 —— 原来这个新 modem 连无线路由器的时候灯闪的频率很低,绝了!

那么接下来就是 AirPort Express 的拨号问题了。我从来没在别人家用过这个 AirPort Express(只用过 MacBook (Pro) ),所以不好说网通的问题是不是已经被新 modem 解决了。又走了几遍 AirPort Configuration Utility 的 Wizard,还是没什么改观,只是记得以前配置的时候选项不是像今天的 Wizard 显示得这么少啊。忽然发现了这个『 Manual Setup 』按钮,点进去果然气象万千。

发现这个 DHCP 的地址段被配置成了 10.0.1.2。赶紧改成 192.168.1.2(如果设置成 10.0.x.x 的话,在 Internet -> PPPoE 这个 tab 里显示的 AirPort Express 的 router —— 估计是缺省网关 —— 总是 192.168.x.x,而不是从 ISP 传过来的)。

重启之后,终于拨号成功。

于是,我想,是不是这一年多不能用 AirPort Express 直接拨号的原因都是因为这个而不是什么旧 modem 不兼容?吓!没精力深究了。反正旧 modem 用 MacBook (Pro) 也拨不通不是?反正得换。下面是换下来退休的 modem 和 Netgear 路由器。

最后,这次改造的成果是网络快了不少。看来上面这两位里某个(或者两个)就是罪魁祸首。终于可以重新享受在床上看美剧的乐趣了。

唯实用主义

2010年04月11日,星期日

Joel 的 Blog 是个很有意思的关于软件开发的网站。里面有不少睿智的看法,比如『抽象泄漏』。但是有时候 Joel 的实用主义实在有些过度。只要两个方法都能解决同一个问题,就对它们进行无限制的无差别化,这就失去了从更高层次分辨优劣的能力。比如这篇 2003 年的《Biculturalism》,试图和 Eric Raymond 的《Art of UNIX Programming》唱唱反调。

试图和《Art of UNIX Programming》唱反调的 Blog 不止这一篇,比较新的还有这篇微软雇员写的。我觉得,以一篇 Blog 的篇幅想挑战一本书的观点,前者天然的处于下风,除非能一针见血的指出后者根基上的肤浅。可惜,Joel 的 Blog 只是说 Raymond 的观点稍稍过于执着;微软雇员的那篇则是在自设了很多假定的前提下玩逻辑游戏。如果你只是想说对手的观点稍稍有些过激(而不是全然推翻),或者想玩复杂的逻辑游戏,在篇幅和对手差一个数量级的情况下大多是自取其辱 —— 你很难有比对手更细致的分析,也很难把一个逻辑推导所需的前提假设的历史根源阐述的比对手更清楚。

回到 Joel 的这篇《Biculturalism》,开篇头一句『By now, Windows and Unix are functionally more similar than different』。看到这句话,非但没有感到 Windows 和 UNIX 之间的分歧有越来越小的迹象,我脑海里首先想到的就是kernel mode 下的 Windows GDI [1] —— 今天主流操作系统里唯一放在 kernel mode 里的窗口管理和图形界面系统,还有和 GDI 紧紧绑定的 window-based toolkit [2]。也许细心的读者会提醒,人家 Joel 说的是 functionally,你说的差异是 architectural。那么,架构上的差异是否完全对功能透明呢?我看 Joel 自己也不完全这样认为(比如『抽象泄漏』)。就像 Apache Web Server 在 Linux 上缺省采用多进程,在 Windows 上采用多线程 —— 架构上的差异导致的性能差别也会让功能的可用性发生质的改变。

Joel 说,UNIX 的文化在于崇尚为程序员提供便利,Windows 的文化在于崇尚为用户提供便利,我们最终是为用户而不是其它程序员提供产品。说这番话的时候 Joel 一定忽略了 Raymond 书上的引用的 Ken Thompson 的这一句话:

This is a consequence rather than a goal. I abhor a system designed for the “user”, if that word is a coded pejorative meaning “stupid and unsophisticated”.

问题在于,忽略了架构上的简洁的系统,是否还能可以提供完全相同的功能?比如我们提到过的,Windows 把 Toolkit(按钮、下拉框、文本框 ⋯⋯)放到窗口管理的 GDI 架构中,每一个控件都是一个窗口,其中还夹杂了直接访问硬件显存的 Direct Draw 窗口;而 UNIX 的窗口管理器一般采用 Toolkit-Agnostic 方式,窗口管理器只知道窗口的内容是一组二维像素,其中的 Toolkit 完全由应用程序空间的代码绘制。『实用主义者』会说,哪个用户会关心这些?不过,用户总是有设计者原本意想不到的应用场景。比如,今天的应用软件很多时候都加入了平滑动画功能 —— 当窗口的内容改变的时候,软件会显示一系列的中间状态。像图片旋转 90 度这样的操作,往往会短暂的显示中间的旋转状态。通常在旋转的过渡状态矩形图片的角会超出显示区域(因为我们都知道矩形的对角线比边长)。在一个 Toolkit-Agnostic 的窗口管理方式里,因为图片不过是一个像素组,超出的部分会被自动 clip。而在每个控件都是独立窗口的系统中,像 Direct Draw 这样的浮动窗口的显示在这种情况下是很难不显示出丑陋的一面的。如果这个例子只是个美观问题,那么用户还有更为严肃的应用场景。现在使用 VNC 一类的远程控制软件的用户很多。VNC 这类软件在 Linux 和 Mac OS X 上很少出问题。因为 VNC 对于传输基于像素的 Toolkit-Agnostic 方式的显示处理方式单一。而在 Windows 上,VNC 程序遇到不同类型的控件就会有很多问题 [3]。

所以,当 Joel 声称 Windows 和 UNIX 在功能上趋同,而且 Windows 更注重最终用户的时候,他既忘记了自己提出的『抽象泄漏』理论,又忘记了用户总会用意想不到的方式来使用你的软件(stupid and unsophisticated)。这也是唯实用主义在嘲笑别人热衷于设计的内部简洁的时候总是犯的两个错误。

脚注

  1. 关于 GDI 处于 kernel mode 这个问题,按照一般的思路人们会归于旧的 PC 硬件的性能遗留的历史问题。有趣的是,事实正好相反。在 NT 4.0 之前 GDI 是在 user mode 的。
  2. Window-based Toolkit,即每个控件是一个独立的窗口。这样的设计下,窗口管理器必然介入控件的层次关系的管理和消息的分派。与之对应的是 Toolkit-Agnostic 窗口管理,即窗口管理程序不了解控件是如何存在的。整个窗口内容是一个像素矩形区,比如在 Mac OS X 里,控件完全由 Cocoa 或者 Carbon 库生成和负责 sub-window 级别的消息分派(这两个库连接到应用程序的进程空间)。UNIX 的 X 是提供一定程度的 window-based toolkit,不过现代的 Linux/UNIX 程序一般建立在 GTK/QT Toolkit 之上,绕过了 X 本身的控件,此时 X 也作为一个 Toolkit-Agnostic 管理器存在。
  3. 当然这不是 VNC 协议本身的问题,而是具体 VNC 程序实现的问题。不过关键在于 Windows 的架构让 VNC 程序的实现和测试比 Toolkit-Agnostic 下的 VNC 程序更复杂也更费时费力。

Mighty Mouse 小球挂掉了

2010年02月25日,星期四

Mighty Mouse 上的小球是出名的容易脏。今天中午我的无线 Mighty Mouse 的小球是无论如何清洁都不能向下滚动了(办公室的 Mac Pro 的有线鼠几个月之前就已经如此了)。下午赶紧在 Apple Store 的网站上约了一个维修。心里还在盘算会怎么处理。这个鼠标买的比较早,可能已经过保了。也许打开清洁一下,或者干脆不给修了?

晚上去了 Apple Store,被告知只要有在保内的 Mac 就能保修鼠标。说是保修,其实也就是给换了一个新的。先前的那个用了一年多了,平时也没怎么保护(起码没像对 MacBook Pro 那样),使用痕迹还是很重的。想不到能很干脆的被换了一个新的。

只是不知道公司的那个有线鼠怎么才能通过层层官僚享受 Apple 的保修呢?(好多同事的小球不能用的 Mighty Mouse 都被 IT 换成 Dell 的鼠标了。不过似乎有人更喜欢 PC 的鼠标。)

另外,今天在店里用了一下最新的 Magic Mouse,觉得挺好用的。没发现有什么有悖人体工程的地方 —— 有人觉得作为鼠标 Magic Mouse 太扁了,其实 Magic Mouse 更像一个本身会移动的 Touch Pad,转换观点之后就不觉得有什么难用(有人会觉得自己的 Touch Pad 不够隆起吗?)。有人说 Magic Mouse 移动速度太慢,我在店里的 iMac 的配置上也没觉得,和我机器上的 Mighty Mouse 一样快(我机器上的鼠标配置一般都是比较快的)。看来我要攒钱在这个新 Mighty Mouse 的小球挂掉之前买个 Magic Mouse。

大部头与网路短篇

2010年02月20日,星期六

新年前后开始打算学习 OpenGL。按照惯常的思路:像 API 出了小毛病这样小问题靠上网查查资料;像 OpenGL 这样大的主题要找本厚厚的大部头从头到尾慢慢研究。于是我从家里的书架上取出一本搁置已久的《Interactive Computer Graphics: A Top-Down Approach Using OpenGL》 —— 这是我的另外一个习惯:看到感兴趣的先买下来,不管有没有时间学,也不管当时兴趣能持续多久,于是书架上会有不少这样的『储备』(可谓『高束焉,度藏焉』)。不过很多『储备』也并未一直束之高阁下去,日后一旦兴趣恢复也会阅读。所以坚持想买就买的原则,不怕买错也不怕买了不读。

当初买这本《Interactive Computer Graphics: A Top-Down Approach Using OpenGL》之后发现 Windows 对 OpenGL 的支持日薄西山,一下子没了兴趣。不料多年之后转到了Mac OS X —— 对 OpenGL 优化最好的平台之一。也算验证了书不怕搁置的理论。

可惜真正开始读之后发现内容并不理想。这本书是国外大学的教材,有教材的通病 —— 前言和泛泛的介绍罗里罗嗦,而且大部分是以前就了解的知识。好不容易读完了泛泛介绍,后面的内容也不流畅。3D 是一个要求对基础数学和对 API 的了解都很深入的主题。可惜这本书对二者的结合并不理想。讲 API 的时候对需要了解的数学基础一笔带过或者根本不提。也许是因为 3D 实在不是一个很容易在一本书里用一套结构讲述清晰的知识体系。这个主题似乎更需要这样的阅读:分别精读两本书,先完整的学习 3D 的数学工具,比如变换矩阵,homogenous 坐标系,等对数学工具了解透彻再学习一本讲具体 API 的书。

也就是说,当学习的主题越来越脱离具体的产品,学习者就会越来越发现大部头的问题所在。每个大部头都在试图讲解自己的一套体系,但是这套体系会和其它的体系发生很多平行的联系。从根本上说,这是我们需要学校的原因:学校帮助我们组织各个大部头之间的平行联系。另一方面,这也是非计算机专业的人可以通过自学学习计算机科学达到比较高的工作水平的原因,因为计算机科学的各个分支和其它分支的平行联系都比较弱。学习一门计算机科学的分支往往不需要了解其它分支或者需要很少的时间准备一些独立的知识点即可。而像通信、自动化、电力这样的学科分支之间就有比较强的平行联系。

但是无论如何,计算机科学里还是有像 3D 这样与另一门或者几门分支具有较强的复杂平行联系的分支。对于离开学校很久的人来说,学习这样的知识要比学习运用一个数据库或者服务器产品困难很多。

不过我不想再体验学校的学习方式,转而尝试一下应用更多的网络资源 —— 这算是我离开通信行业进入软件业之后养成的一个新习惯。一方面,网络上像 Google 和 Wikipedia 这样的工具在三年前离开通信之后有了很大发展,另一方面,软件行业面临的跨系统的希奇古怪的问题比通信业多出十倍。在通信行业能用官方的文档解决大多数问题。而在软件业里遇到的种种问题,没有 Google 这样的工具是不可能解决的。

结果相当顺利。第一个例子是 OpenGL 的投射矩阵(projection matrix)。《Interactive Computer Graphics: A Top-Down Approach Using OpenGL》对这个概念完全没有任何解释。只是对 gluPerspective() 的效果做了一个简单的介绍。gluPerspective() 只是投射矩阵的一种特例(从它的 glu 前缀就能看出来)。单单介绍这个 API 的作用根本不能理解投射矩阵的意义,而用 Google 搜索『OpenGL transformation』能找到这篇文章,介绍了投射矩阵在整个 pipeline 中的位置;顺藤摸瓜即可找到对投射矩阵的详细解释 —— 而不了解投射矩阵的一个重要相关概念 homogenous 坐标也没有关系,因为这篇详细解释的链接里就指向了这篇。这种超链接的组织是 HTML 的本意,不过整个网站的 wiki 风格说明在 Wikipedia 成功之后,这种知识组织方式才越来越被更多人运用的更为娴熟而超过了书籍。

第二个例子是 OpenGL 的选择模式(Selection Mode)。这次书里很遗憾地有很多基本错误。比如对 gluPickMatrix() 何时调用以及调用前后需要哪些其它 API 的辅助的代码例子都有错误;对 glPushName()glLoadName() 的区别也没有任何解释就直接在代码例子里使用了后者。其实 glLoadName()glPopName()glPushName() 共同使用的一个快捷方式。这种对基本操作不加解释就直接利用快捷方式是很让读者迷惑的。Google 查找『gluPickMatrix』的结果指向这篇,对选择模式的解释很令人满意。

其实,在第一个例子中网上资料比书的解释更详细的时候,我就犹豫是继续按照书的结构学习还是完全采用网上的资料。后来还是决定暂且按照书的组织结构。岂料不过一天就又卡在选择模式这个问题上。经过第二个例子,我倒是发现学习资料的组织结构不像以前认为的那么重要。以前我选择书籍最看重的是组织结构里注重知识的循序渐进,减少或者消除前向引用 —— 就是前面的章节引用后面的章节介绍的概念(哪怕是说明了必要性也很令人不快)。而今天参考网上的资料,不用理会原本的结构如何,遇到不太懂的概念只要在网上顺藤摸瓜即可。

回顾这几年的学习经历,Google 和 Wikipedia 已经彻底的改变了我的学习方式。相比网络上的资料,大部头有更强的知识集中、系统性和结构性。可是这种结构性是出于作者对读者背景和理解力的主观预测。结果往往是详略和节奏都和读者期望的不同。特别是像 3D 这样把数学和具体 API 结合起来的,在不同分支之间有平行联系主题,如果仅靠阅读大部头就需要从头到尾阅读多部,然后主要凭借记忆自己建立起平行联系。

网络上的知识一般被认为是零乱而且缺乏组织的。不过这些年已经不知不觉发生了巨变。首先是个人 blog 的发展让很多作者把网络资料的水准从 BBS 的帖子提高到了出版物的水平。再有,Wikipedia 这样的网站在本身收集知识的同时,也把组织知识的先进方式公开给了所有知识传播者。让 HTML 在传播知识方面有了高层次的范例。最后,Google 让所有这些知识片段按读者的自我需求动态组织成为一个个较大的主题。这三者缺乏任何一种都不可能让网络知识的水平达到传统书籍的水平。而现在的情形是它们的协作已经至少在工程技术领域让网络化组织的知识已经基本可以取代长篇的出版物。