全球变暖和大肠杆菌

2010/02/03

前些天和朋友聊到中国在这次哥本哈根会议上被众国认为搅乱议题,朋友说搅合的人很多,还有一帮国外教授在说全球变暖是某些财团为了推动自己的低炭经济产品推出的骗局。我突然感慨,人类文明发展了几千年,还是没什么开化,都大祸临头了还在争论些没有意义的问题。

朋友突然说,前些天才知道大肠杆菌是怎么死的,是因为繁殖太快被自己的粪便淹死的。一个人和一个大肠杆菌的差别大到没有比较的意义。可是一亿人和一亿大肠杆菌似乎没什么区别。

iPad 的独特用处

2010/01/31

Apple 的 iPad 终于如期公布。不过和同事聊起这个东西,我们的第一感觉不约而同是『没什么用』:携带不方便,在随身负荷方面,MacBook (Pro) 和 iPhone 都没有挑战人们已有的习惯(相对计算能力而言),iPad 可谓又一次挑战(如果 Newton 算是一次失败的话),能成功吗?没有键盘的意义是什么呢?是为了没有键盘而没有键盘吗?键盘在我眼里仍然是人机接口里带宽最高的设备。要想超越键盘,除非对人脑的工作原理有更深一步的了解,让电脑直接越过手指理解用户的意图。

搁下这些问题,先忙其它俗务。今天和久不见面的亲戚聊了一晚上,发现话题格外多。先是聊到太空旅行的一个问题争论不下,用 iPhone 查到了资料然后再展开讨论了一番。然后给他在 iPhone 上看了最近写的几篇 blog。最后是一个智力休闲的 iPhone 游戏,不多的两三关花掉我们今晚最后的半个多小时。开车回家的时候我突然想到,如果 iPhone 的屏幕再大一些,对这次聚会来说绝对是一个完美的工具!只是屏幕再大一些,不要键盘,也不需要复杂的 content-creating 应用,我需要的就是这样一个大号 iPhone。如果有人觉得理由不够充分的话,可以想想是否在咖啡厅见到过两个人并排挤在一台电脑之前的情景,或者几个人把一台笔记本电脑傻里傻气地转来转去。

我和身边的人总是不约而同的说,iPhone 是应急计算的理想工具,比应急计算更复杂的任务则完全可以寻找应用笔记本电脑的工作环境。这是百分之百的真实感受 —— 只不过因为我们太习惯独处了。我们一贯的印象是电子设备和社交是互不相容的 —— 还记得大学的时候在宿舍看别人打游戏是多么傻气的行为,看别人在你面前写短信是多么讨厌。作为一个独处的用户,我们的第一反应是没人需要仅仅是屏幕大四倍的 iPhone。但是当生活不是独处的时候,iPad 让线下社交和网络拥有了一个合理的结合点。

看到电影里的微软 Surface 设备,我们都会觉得用它来进行任务布置是多么流畅,但是也毫不留情的嘲笑在现实生活中谁会花费如此不菲的价格,谁又能保证和朋友的所有聚会都在布置了 Surface 的私人沙龙里进行?iPad 正是 Apple 完美的回应。

所以说,什么人类不应该习惯垂直显示,什么指向设备和屏幕 cursor 分离,在我看来都不是使用平板电脑的迫切理由。而 Apple 的 iPad 发布演示也忽略了线下社交这个情景,可能老乔太喜欢唱独角戏了(不过他演示 iSight 的时候和部下们合作过一次),也可能线下社交的热闹场景拿到演示台上太过风格诡异。总之,我们太喜欢面对屏幕通过网线交流,或者面对面为一个能两秒钟就 Google 出来的问题争论一个小时和因为一个没法记起来的细节而中断话题。互联网要么是我们的隔阂,要么是遥不相及的东西。iPad 是一个改变我们生活方式的机会,是真正让互联网回归帮助而非阻碍人们交流的角色。只是把 iPhone 扩大到四倍,就是改变生活的工具。

Chrome OS 为什么不会成功

2010/01/24

与朋友聊起 Chrome OS,一多半认为这个东西不会有什么前途。让我自己觉得 Chrome OS 没有前途的原因不是别的,正是那一小半对齐心存希望的朋友的理由和 Google 高层自己的言论。

觉得这个操作系统大有希望的人一般认为 Chrome OS 的目标不是完全替代主流操作系统,而是在 netbook 等设备上取得主导地位,所以作为一个有特定目标用户的系统有可能成功。Chrome OS 的主要策略就是,把 Google 认为人们不应该不会在 netbook 上使用的应用(也就是非 web app 喽)统统割掉,借此达到安全和简洁。对于 Chrome OS 功能方面的质疑,Matthew Papakipos(Chrome OS 项目的领导人)说:『如果希望用 Windows,你应该购买 Windows 机器;如果希望运行 Photoshop,你应该购买 Windows 工作站。』没错,这句话一点问题都没有。但是,也许 Matthew Papakipos 忘记了一个事实 —— 任何市场的划分都有灰色地带。从 netbook 到 laptop 到 desktop 到 workstation 的划分是一个配制逐渐增高的过程。其中每个机型的高端用户都会有一定的需求运行高一级机型的低端甚至中端应用。Chrome OS 的策略是忽略了这一点,看到 netbook 上 99% 的人都用 web app,就把自己的功能局限在 web app 上,而且这条线是硬性划分的 —— 你想当 Chrome OS 用户吗?好,你的一切运行本地应用的机会都被硬性取消了。这种做法缺乏对用户需求的基本认识。一个产品可以针对特定的用户群,但是永远、永远不能在功能上针对某一个用户群进行一刀切的操作。

可能有人会说,Windows 也有客户版和服务器版,Photoshop 也有标准版和 Extend 版。不错,但是 Windows 的客户版拿掉或者加上一个 feature,本质上还是 Windows。如果 Chrome OS 要加上一个 web app 以外的 feature,还叫 Chrome OS 吗?没有『自我阉割』式的独特性,Chrome OS 不过是另一个普通的 Linux 发行版而已。Chrome OS 把自己的本质建立在否定用户群的模糊划分的事实上。

哦,Matthew Papakipos 还说了,当你用 laptop 进行需要超过两个半小时的工作的时候,你就得插上电源,而 netbook 解决了这个问题。问题是,如果我要在外边晃荡四个多小时的时候,为什么不能找到一个有电源的地方呢?暂且承认 Matthew Papakipos 的说法是对的,那么,在那些环境需要进行两个半小时以内的无外接电源工作的用户和那些环境允许进行四个小时以上的有电源工作的用户之间有一个灰色地带 —— 环境需要进行四个小时左右的无电源操作的用户。所以,netbook 是有用的。好了,看来 Google 的高层们是明白市场有灰色地带的。那么,在 netbook 上运行 web app 的用户和在 desktop 上运行大型应用(Matthew Papakipos 口中的 content-creating 应用)的用户之间,就没有一个灰色地带吗?没有一个希望在高端 netbook 上偶尔运行 content-creating 应用的人群吗?哦,Matthew Papakipos 的理论是不是这些人应该拥有两台电脑?那么,他的理论是人们能忍受在两台电脑上来回切换,而不能忍受多带一个 laptop 充电器和拔插电源。

可以说,Google 或者很多创新者的成功就在于发现以前被忽视的用户群。而 Google 在 Chrome OS 上的做法恰恰是忽视用户群。自以为是地把用户分成只需要运行 web app 和需要运行其它应用的泾渭分明的两群。当然,每个人都可以有多台电脑,但是如果 Chrome OS 解决问题的方式是增加用户的状态切换(而且是在两台机器之间),那么这种方案又有什么优势呢?从另一个方面来说,专用系统是必要的,但是一个专用系统在服务主要用户和主要需求的同时,必需能够应急的服务一些高端用户的应急需求。也就是说,netbook 操作系统必需在需要的时候能够紧急充当短时间的 laptop 操作系统的功能。很多有经验的用户甚至会在自己机器上装 Apache 一类的软件备不时之需。从技术上说,服务灰色用户群的能力就是技术人员常说的『伸缩性(scalability)』。

Chrome OS 把自己展示成专用系统,又把缺少伸缩性作为自己的特征。Google 鼓吹的安全性和简洁易用在我看来并不是简单粗暴的砍掉伸缩性的理由。我不能证明它没有用户,但是不能想像它会有什么意义上的成功。

调试专用代码的最小集

2010/01/01

不知道别人如何,有段时间我常常想在 code base 里加点儿调试专用的代码(就是那种通常被括在『#if _DEBUG_ ... #endif』之类的宏里面的代码),也许是处于对潜在问题的忧虑,或者纯粹就是让程序看起来高级点儿。不过最后总是发现还是删掉(或者压根不写)它们更整洁。如果善于利用 debugger ,需要额外的调试代码的情况还是不多见的。即使是必需的调试代码,问题解决之后也很少需要用再用到。所以我还是愿意必要时候随手写几个 printf ,然后删掉它们。

把调试专用代码长久留在 code base 里要多留意很多:编码规范、对其它部分的功能和调试的影响,等等。随便写几行日后就删掉的调试用代码则简单的多,不必顾及这些,只要能解决眼前的问题。更重要一点,留在 code base 中的调试代码必须对解决一再被发现的潜在问题真正有价值。只有这类问题,你知道它在未来很可能会被发现,但是现在却属未知。而它被发现的时候,用工具和几行 printf 又无法解决,才值得把调试专用代码留在 code base 中。

迄今为之,考虑到把代码留在 code base 里耗费的精力和调试工具的强大能力,我认为只有一种问题值得事先写一些调试代码:某种内存增长的问题。但是这种内存增长的范围很小,绝大多数内存增长问题都不属这种问题。比如,内存泄漏是内存增长的一个常见原因,但是大多数内存泄漏不需要调试代码。良好的 code review 能去除大多数内存泄漏,统一的内存管理策略(比如普遍的利用局部拷贝和引用计数)也能消除大多数内存泄漏。现代的内存监测工具甚至能把大多数引起内存泄露的代码位置明确标识出来。对大多数内存泄漏来说,良好的设计和强大的工具比调试代码更有用。

真正需要专用调试代码的内存增长问题是进行长时间的操作时内存缓慢但持续地增长,短时间之内幅度不明显,积累起来(比如几十分钟或者几小时之后)不容忽视。这种增长也许不是内存泄露(比如,有的情况里存在问题的长时间操作结束或者被用户中途取消,内存会回复),所以用检测内存泄漏的工具通常无法查明原因。而且,因为是长时间操作,内存检测工具运行的开销经常无法容忍,比如一个运行 20 分钟的 use case ,如果把程序连上内存检测工具,运行时间也许会超过一个小时。只要功能慢慢丰富,code base 复杂度不断增加,一个应用程序迟早会遇到这类问题(一般在 3.0 版本左右吧)。但是当最初写下隐含这类问题的代码的时候,你或者你的前任还在忙于功能测试。就算从一开始就重视性能测试,也很难做到边写大段代码边测试每次耗时 20 分钟的 use case。这种问题用外部工具如同隔靴挠痒,又很难在早期发现,当问题显露的时候,code base 已经相当复杂。因此一开始就得在 code base 里做些未雨绸缪的准备。

准备起来很简单,定义一个 dictionary 类型的数据结构(比如一个 std::map),Key 值是程序里生成的实例比较多的类(或者 struct,如果是 C 语言)名,value 是类的实例个数。维护这个 dictionary 的方法就是在对象的 constructor 和 destructor 里对相应的 value 值加一和减一(C 语言要写一组能接受 struct 名称的 debug 版本的 malloc/free)。然后设计一个操作,让你能在长时间的操作中随时打印这个 dictionary 的内容,比如一个随时可见的按钮或者一个菜单项。而且别忘了把所有这些对用户需求来说多余的代码都用条件编译括起来,别影响 release 版本的性能和外观。

这些调试代码纪录的东西不多,但足以解决问题。如果没有这些信息,解决内存增长就如同大海捞针。一旦有了这些信息,大部分情况下通过显示的实例个数的变化都能找到是哪个对象造成的内存增长,找到这个就如同有了金钥匙,接下来顺藤摸瓜用些简单的断点调试和代码查找就能找到病根。某些工具 —— 特别是在 Java 之类的动态语言领域,可以自动收集这些信息。不过『土办法』有独特的优点。比如,程序里会有很多类或者对象的创建的销毁(或者说『生命周期』)完全被其它类控制,这种类或者对象的实例个数不必被纪录,只需纪录控制它们生命周期的类的实例个数。但是外部工具往往不能区分这点。『土方法』需要你自己动手写点代码,所以很容易区分哪些实例个数是真正需要纪录的而哪些又可以忽略。土方法另一个不可替代的好处是消耗的资源非常少,所以能研究一些本身就消耗资源很多的长时间操作。而那些本身就消耗很多资源的工具遇到这些问题往往没一会儿就让系统僵死了。最后,这个办法对程序设计有一点点(也许是有益的)要求:用 C 或者 C++ 的时候不要直接把 char* 或者 byte* 类型的大块内存到处直接分配和引用,给这些 buffer 封装一个 struct,这样才能在调试中得到更有意义和启发性的名字。

所以,我暂时不打算在下一个项目中写『#if _DEBUG_ ... #endif』,除了上面说的纪录实例个数的代码。

Snow Leopard 的修正准则

2009/12/22

只要应用程序够复杂,每次操作系统升级都会弄坏几个本来工作得好好的功能,有些是应用程序本身代码的问题,只是在系统升级时才突然现身。这些 bug 在旧的操作系统上深藏不露,系统一升级才兴风作浪,大多因为操作系统的 API 行为变得更『正确』了。

升级到 Mac OS X Snow Leopard 之后我们的应用程序就出现了两个这样的 bug。第一个是应用程序 fork 之后立刻在子进程里调用 File Manager 的函数。跑了几年都没问题的代码,一到 Snow Leopard 上就僵死。第二个是 MPEG 的播放代码,同样是用户使用了很多年都没有问题,一到 Snow Leopard 上播放的视频就不停的闪烁。原因都是应用程序调用 API 的时机不对:File Manager 必须在 exec*() 函数调用之后才能使用;用 Core Video 配合 Quick Time 播放视频,必须在取出后一帧之后才能销毁前一帧。这些限制在 Leopard 版的文档上就有说明,幸运的不幸的是,系统『恰好』缓存了某些数据让不恰当的 API 调用『碰巧』能工作,掩饰了旧代码中的错误。显然,我们可以猜到,程序在 Snow Leopard 上才初次显露问题是因为新的操作系统去掉了那些缓存。

这样看来,OS X 岂不是自寻烦恼?旧的 API 行为在原来的应用程序里工作得好好的,何苦修改呢?大致猜想有这些原因:一是缓存让 API 的实现复杂化了。加上一个缓存,就得再加上一套代码保证缓存和真实数据的同步(或者在无法同步时清空缓存)。二来,因为缓存不能完全替代获取或者计算真实数据的代码,所以缓存、维护缓存同步的代码、计算真实数据的代码要同时占用内存。应用程序的活跃部分的体积会增大,原来能适合 CPU cache 的部分可能必须在主存和 cache 之间换进换出,本来为了提高效率的缓存却适得其反。最后一点,缓存让 API 行为变得不可预测。一段『碰巧』能工作的代码意味着当它碰巧不工作的时候更难被监测到和修正。

这些 bug,或者更广泛的说,所有系统升级中暴露的遗留代码的问题都说明了软件(特别是 API )设计的一个陷阱 —— 性能和 API 的确定性行为哪个更重要。结论是,确定性行为不仅更重要,而且往往带来更简洁的设计和更好的性能。

这是『修正准则』(Rule of Repair)的变形实例。Leopard 里的 API 不完美的修正了应用程序的错误输入(这里的输入是调用时机),引入了更复杂的实现和不确定的行为。Snow Leopard 去掉了缓存,消除了 silent success,但是 failure 不够 noisy。结果是功能被搞坏,新出现的 bug 诊断起来也很困难。所以,不要只在文档里说明你的 API 需要什么样的 precondition。如果在你的 API 文档里出现这样的文字 —— 在某某条件不满足的情况下调用某个 API 的行为是不确定的,那你就得三思。应用程序的作者不可能有时间细细品味文档,他们只会在出问题的时候(或者更窄的范围,程序崩溃或者挂起的时候;甚至没有人会注意你的 API 会有内存泄露,即使瞥一眼 Activity Monitor 就能看到疯狂的增长)才匆匆翻阅一下。所以,你的那些告诫会被人忽视数年。直到有一天你决定清理一下你的 API 实现,让它变得更简洁。这个时候那些告诫才会出来咬人。那么,亡羊补牢的方法是用一个 crash 来表达你的告诫(那样会在 debugger 里清晰的显露出出错点),而不是听任程序出现任何可能的行为(比如僵死和闪烁)。

不论如何,Snow Leopard 毕竟简化了实现,尽管做得不完美,起码比打着向后兼容的口号,为了让几个 killer-app 的 buggy code 正常工作而保留丑陋的实现然后把文档变成让人无法理解的技术诉讼书(或者辩护书)更好。

链表迷魂阵

2009/12/15

只要粗粗看过数据结构,对链表的印象一定是插入、删除操作都很快。不过对 C++ 标准库里的 list(也就是 std::list )就得多加小心。比如下面的代码:

std::list node_list;
...
NodeData a = node_list.front();
...
node_list.remove(a);

熟悉 STL 的人可能一眼发现其中的陷阱:node_list.remove(a) 可不是一个 O(1) 的操作(虽然经典链表的删除操作是)。这是因为 std::list<T>::remove(const T&) 实际是一个经典链表的查找操作加上一个经典删除操作。虽然能看出这个问题的人也许不在少数,但是犯下这个错误的人也不在少数。而且,当前者遇到后者编写的代码时,修改起来往往很头疼。因为虽然 NodeData 是从链表中取出的,但是它并没有存储前后节点的信息。所以要想把上面的有缺陷的代码改正,必须把所有涉及到 NodeData 参数传递和临时存储的地方都加以修改。所以说,使用 std::list::front() 这样的拷贝语义函数的行为本身看起来就是个错误。

这里先插上两句说说所谓『经典』链表(因为后文会拿来比较)。很多数据结构的书里的链表就是把 NodeData 这个对象本身加上一个 prev 指针和一个 next 指针,用来分别指向前一个和后一个节点。所以『经典』链表的数据结构和节点数据本身由一个对象表示。经典链表对 NodeData 的定义是侵入式的 —— 如果你希望把一个原来和链表操作完全没有联系的对象或者 struct 加入链表,就必须修改它本身的结构。相对的,std::list 的方式是把链表看成一种『容器』,包容原有的对象而无需修改其结构。

所以你的第一反应也许是应该永远用 std::list::begin() 或者 --std::list::end() 之类的操作返回 iterator,相比 NodeData,iterator 更像经典链表的节点,带有前后节点的联系。但是 C++ 永远不会给你简单的方案。这个方案的问题在当在代码的多处拥有指向同一个 NodeData 的 iterator 时,调用任何一个 iterator 或者 list 本身的 erase() 方法都会导致其它 iterator 失效。而且,C++ 标准库仅仅告诉你唯一的以定义行为是其它 iterator 会『失效』。你甚至没有一个 valid() 方法来检验一个 iterator 是否失效。C++ 标准期待的是你无论如何知道这一状态并且决不能再对那些 iterator 做任何操作。

所以 std::list 相比『经典』链表有两个致命问题。第一是直接取出 NodeData 的操作丢失了和 list 本身的联系,让后续操作承受性能损失。第二,『经典』链表可以通过设置 prev/next 让任何拥有 NodeData 指针的代码轻易的判断一个节点数据是否还在链表中,而 std::list 对 iterator 行为的粗略定义让这一点变得不可行。

接下来我们把问题搞得更全面(也更复杂)一些,考虑 NodeData 的生命周期。C++ 标准库一贯的拷贝语义让这个问题得到了(幼稚地)解决。但是对于 std::list 来说,我们已经看到拷贝语义的操作会让取出的节点丢失和 list 的联系,而总是取出 iterator 又会带来失效的问题。

当然,经典链表虽然没有 iterator 失效问题,但是仍然要面对何时销毁节点数据本身的问题。不过,讽刺的是并非所有的数据都是可复制(copiable)的,所以 std::list 里面存储的也经常并非数据本身,而是数据的指针。因此,在同样面对 std::list<NodeData> 的『联系切断』和『iterator 失效』两难之际,std::list<NodeData*> 还必须面对经典链表的销毁节点数据的时机问题。更遗憾的是,C++ 的内存管理法宝 shared_ptr 在这个问题上完全无能为力。一个 std::list< shared_ptr<NodeData> > 同样拥有『联系切断』和『iterator 失效』的矛盾。这时只有经典链表,加上在 NodeData 中实现手工引用计数才算一个比较完美的方案。

链表是 C++ 的好几个设计理念走麦城的地方。链表重在『链』,它的灵活性就在于『链』。把链表作为『容器』,特别是和 STL 其它容器一样保持拷贝语义操作就毁了链表。而且基于拷贝构造和自动析构的共享指针和手工的引用计数相比也并非处处领先。

如果一开始能抛开 STL 那种容器的概念和对数据节点不侵入的要求,C++ 的链表设计不会这么差。比如 Linux 内核的链表设计,把链表节点作为链表数据的一部分,让链表数据包含节点,而不是反之。这样的设计用 C++ 模版也完全可以作到。(当然我更喜欢 Linux 内核的基于宏的设计,而且 Linux 的设计通过一个可被优化的 forced cast 同样保证了类型安全。)

后记:

写这篇 blog 的时候又重温了一下 Java 的 LinkedList 文档,发现 C++ 还不是最差的。LinkedList 的文档是,至少 6.0 还如此 —— 对 LinkedList 做的任何结构更改(什么意思?删除节点算吗?)都会导致所有已经获取的 iterator 失效。什么玩艺,这样的话我还用链表干什么?

战术核弹

2009/12/01

这个啤酒挺劲!照片也挺有意思。

Wikipedia Thanks to Me[2]

2009/11/30

给 Wikipedia 捐款还有另外一个源渊。一年半以前完全转向 Mac 的一个重要原因也是因为 Wikipedia 。当时我对技术比较沮丧,每天都沉浸于阅读二战史。不过刨根问底的深度优先式阅读习惯没有改变,遇到不懂的名词都要上 Wikipedia 查查 —— 和技术文章相比,阅读二战史还需要更多的访问它。因为偶尔在公司里阅读或者手头只有 iPod touch,所以会用 Mac 或者 iPod touch 来访问 Wikipedia。结果发现它的页面在 OS X (或者 iPhone OS) 上渲染得比 Windows 上漂亮很多(后来和志岩聊起此时,尽管认为不少网站都是如此,但我们都同意 Wikipedia 的差异更明显一些)。

以当时对技术沮丧的程度,我已经认为自己不会在业余时间研究技术了。加上使用 Wikipedia 的感觉,我决定买一台给『非技术用户』使用的电脑。这算是 Wikipedia 带来的一个小小转折。当然后来 MacBook 一到手,接触到 UNIX、Cocoa、OpenGL 这些东西,对技术的感觉突然似乎又回来了。

Wikipedia Thanks to Me

2009/11/24

樊志岩要求热爱知识的人给 Wikepdia 捐款。响应号召得到了这个:

Chrome OS 的自我设限

2009/11/23

In fiction, there have been stories of laws passed forbidding the construction of “a machine in the form of the mind of man”. In fact, the competitive advantage — economic, military, even artistic — of every advance in automation is so compelling that passing laws, or having customs, that forbid such things merely assures that someone else will get them first. … … The other approach to Drexlerian confinement is to build rules into the mind of the created superhuman entity (Asimov’s Laws). I think that performance rules strict enough to be safe would also produce a device whose ability was clearly inferior to the unfettered versions (and so human competition would favor the development of the those more dangerous models).

弗诺文奇,《即将来到的技术奇异点

Chrome OS 不允许安装运行除了 OS 之外的任何 native code。有史以来,第一次电脑硬件上运行的 machine code 被限制在了一个特定的范围之内(就连 iPhone 也并未做出如此严格的限制)。Google 声称 Chrome OS 的自我设限让这个操作系统具备了空前的安全性。

谈到『自我设限』,很快想到的就是阿西莫夫设定的机器人技术的三大定律。人造的定律怎么能无法逾越?在《钢窟》中,阿西莫夫解释了三大定律坚不可摧的原因是全世界的机器人科学家心有默契,在 50 年内的发布的每一条制造机器人所需的数学定律中都嵌入了三大定律。这样,如果有人想越过三大定律,就必须独力重建全世界科学家 50 年的成果。不过,弗诺文奇在《即将到来的技术奇异点》中提出的质疑更有力。像三定律这样的自我设限必然对机器人的性能产生不利影响,在一个充满利益斗争的世界里,自我设限只能被对手利用。所以,类似『囚徒悖论』,竞争让技术必然走向无限的发展。弗诺文奇说,即使人们能意识到技术的无限发展会导致人类的灭亡,也没有办法通过自我设限来解决。

回头看看 Chrome OS 的自我设限,感觉似有相通之处。在功能方面,Chrome OS 无法创造出真正的 killer app —— 任何用于 Chrome OS 的应用都是 web 应用,所以都自动地为其它操作系统获得。从用户体验方面来说,其它的操作系统都是 Chrome OS 的不设限的超集。所以,其它操作系统上的开发者一定能利用 Chrome OS 的限制创造出比后者更好的用户体验。讽刺的是,尽管 web 版的 Lantitude 在 iPhone 上并不受限制,Google 还是对于 Apple 不让其发布 native 的 Google Lantitude for iPhone 大大的抱怨了一番;反过头来推出了一个只能运行 web 应用的操作系统。从安全方面来看,任何一个有经验的用户都能在 Linux 或者 OS X 上(甚至 Windows)达到几乎同样的安全级别。Chrome OS 除了让不重视安全的用户抱怨之外,这番苦心似乎也得白费。

更绝妙的是 Chrome OS 完全开源。由此 Google 的 vanilla 版本就处在了和其它团体的 customized 版本的竞争位置。正如弗诺文奇的解释,自我设限在功能、用户体验和安全方面的开销必然让处于竞争地位的各个版本争相放弃这些限制,从而让一个抛开自我设限这个就乏善可陈的操作系统变得毫无意义。我已经等不及一个允许允许第三方用 C 扩展 JavaScript 的 Chrome OS 修改版的出现了;下一步,允许第三方浏览器的 Chrome OS ?