我安装好Win8 CTP后做的第一件事情就是用调试器研究Win8各个组件的协作关系。从我半天的研究结果看来,Win8真是一个让我爱不释手的产品。Win8里面涉及到的很多技术正好也是我的兴趣所在。这篇文章简单回顾一下这些技术的变迁,优缺点,和对Win8的影响.
注意,下面提到的对Win8的分析, 是基于公开的Win8 CTP来做的。相信Win8面世的时候,这些技术和细节,都会发生重大改变。所以这篇文章不具备实践上的指导价值。
COM - Common Object Model 通用组件模型
COM是上个世纪中期设计出来的伟大产品。COM旨在解决软件复用的问题。在COM以前,大家都是用代码级别的复用,常见的就是C/C++的库,无论是原代码库还是lib库,都是需要编译后才能重用的。COM使得技术人员可以在二进制上进行复用。从Win95, OLE32和Office95系列开始,COM就是微软平台上的一个技术基石,无论是DirectX API, 还是最常见的剪贴板,以及后来.NET Framework的host接口,都离不开COM。但任何伟大的产品,都有局限的一面。COM在局限性在下面一些地方。
STA/MTA/NTA等等线程模型过于复杂
线程模型,特别是STA,设计的目的是方便使用者。但COM的线程模型严重依赖于太多系统组件,比如Win32 Message, RPC和Windows系统服务,使得程序员需要熟悉和了解太多系统知识才可以正确地使用线程模型。否则用STA导致死锁简直就是家常便饭。
开发工具没有提供足够支持
COM和Visual Studio 6.0的关系,就如同现在CLR2/VS2005, CLR3.5/VS2008和CLR4/VS2010的关系一样铁。使用COM开发,当时的选择要么是VB6,要么用ATL。这两者都有天生的局限。VB6适合企业开发,特别是当时流行的MIS系统,数据库系统这样的CS应用,但是VB6不够灵活。而且VB6里面由于缺少多线程支持,无法用MTA的模型。ATL功能强大,足够灵活,但是使用起来特别复杂。每次实现一个接口,都要做一大堆C++的仪式性工作,比如实现多重继承,定义新的模板,使用大量的C宏。神经再粗大的程序员,都经不起这样用C++的。
无止境地扩充到DCOM, COM+, DTC, MSMQ以及后来的.NET Remoting/WCF, 使得最后的复杂度无法控制
为了更好地适应企业级别的开发,COM被进一步延伸和演化成了DCOM和COM+。所谓"企业级别",其实是指对安全性和可伸缩性的更高要求。经典的COM是一个进程内模型,无法让不同的代码运行在不同的帐号下的,因为同一个进程只能启动在唯一的帐号下。虽然通过impersonate等方法可以适当地解决问题,但是为了让安全模型更为全面,正确的做法是让不同的接口实现,能够跨越进程甚至跨越机器等安全边界运行,要能够赋予不同的接口不同的安全级别,能够和域帐号集成,支持不同等级的加密等等。远程通用组件模型,也就是DCOM就这样诞生了。读者可以尝试运行以下dcomcnfg.exe这个工具,展开一些节点,看看属性页,就能体会到DCOM的功能是多么让人眼花缭乱了。对于可伸缩性,微软更上一层楼,在DCOM的基础上加入了对象池和新的同步模型做成了COM+。风靡十年的ASP,就是运行在COM+框架下的最好例子。更让人叹为观止的是,微软把DTC和MSMQ的设计也和COM+模型绑定起来。陌生的读者可能不了解DTC是个多么nb的东西。简单说DTC是可以让程序员一行代码都不用写,就让SQL Server和Oracle的数据库操作运行在同一个事务边界里面。可以想象,在MSSQL Server刚进入市场的时候,微软推出这样的功能,对于抢占Oracle的市场份额有多么重要。DTC和COM+当时成为了很多大公司的标准配置,以至于后来设计.NET Remoting和WCF的时候,都还是要考虑对DTC的支持。这些强大而且复杂的功能,让本来就复杂的COM更加恐怖。这样的复杂度虽然也体现了当时的市场需求,但由于缺乏配套的开发工具,新的开发语言和更优美的抽象,使得这条路最后越走越窄。
.NET Framework/CLR
在我眼中,CLR的各方面简直是无可挑剔的。但可能正是因为CLR太好了,让微软从2003年开始,对unmamanged world的投资就不大了。
为了争取企业客户,全力推广CLR是最正确的做法。毕竟绝大多数的程序员,一辈子都是和数据库,和UI代码打交道。你总不能让他们一辈子都用C去管理内容,创建窗口句柄吧。CLR的性能也很有竞争力,与之对应的编程语言和开发工具也非常给力,每个新版本都带来长足进步。但是,这些再好,也无法掩饰一个悖论: 要用C#写出性能可以和C++一个数量级的程序,不是不可能,而是花费的代价往往比直接用C++写更大。这里面有很多原因,比如系统最底层的API还是unmanaged的,通过CLR作interop的性能损失无法忽略。比如用C#的话,程序员的控制力很弱,从工具和语言层面上很难对性能精雕细作,一不注意就box/unbox了。比如JIT编译器为了实现自己的安全模型和异常处理,每次访问成员函数的都是都要生成代码确认this指针是否为空。这个世界上还是有很多程序,是需要对性能做严格控制的。在CLR高速发展的几年中,对应的系统平台,Win32 API, C++开发工具,只有完善性的改善,并没有重大突破。这个问题对Windows平台本身影响不大,但如果寄希望于在移动设备上,大家都指着C#来开发,就有点天方夜谭了。这样的失衡,使得微软在移动设备和消费者产品这两个严重需要unmanaged和系统投入的领域缓慢发展了很长时间。
WPF
在我看来,WPF是一个设计得很美的产品。WPF解决了传统Win32 UI程序的四大局限:1) Win32的绘图是由各自Window元素独立控制,基于GDI的。WPF引入了rendering thread来提高性能,优化算法,借用GPU加速。2) Win32依赖于GDI Object,在开发复杂窗口程序的时候,很容易就遭遇资源泄露和资源不足。比如早期的淘宝旺旺,开到几十个窗口的时候,程序就会出问题。所以淘宝针对这个问题,使用了统一控制台,合并多个窗口到标签页的方法来解决。而WPF只有最外面的窗口使用了Win32 Window和GDI,内部的元素都是抽象成了WPF自己的元素,不额外占用Win32 GDI资源的。3) Win32窗体程序严重依赖Windows Message模型。这要求程序员对系统知识有深入的了解。而且Win32 API并不是非常利于使用,比如要进行UI thread和Worker thread之间的通信,往往需要和SendMessage这样的API打交道。在WPF中,引入了Dispatcher类和BeginInvoke方法,把这些复杂问题抽象了。加上CLR提供了更方便高效的开发环境,使用WPF是很愉快的工作。4) Win32缺乏数据,设计和代码三者之间的模式抽象。这三者在WPF中对应了数据绑定,XAML文件,以及后台代码。在WPF中可以更直观地使用各种模式比如MVC和MVVM。这些都体现了WPF设计上的优美。
再优美的东西都还是有局限性的。WPF的问题在于过多的模式和对CLR过度的依赖。了解WPF框架的人都知道,就一个简单的dependent property, 就把设计模式这本书里面的模式用掉一大半了。分析WPF框架代码的话,简直就是看一本设计模式的百科全书。我曾经统计过,关于mouse click这样一个event回调,WPF里面有7种不同的实现方法,分别各有好处,旨在解决不同问题。在这样高度灵活的背后,牺牲的是程序性能。无论是五花八门的模式,还是最常用的数据绑定,背后的主力都是CLR的reflection。过度依赖于reflection导致WPF程序规模一大,性能上就出问题。就算再怎么优化,也总找不到原生Win32程序那般流利的感觉。使用reflection也体现了对CLR的依赖。所以前面CLR的局限性,也适用于WPF。
微软产品的互操作性
微软的产品线虽又长又多,但是各个产品之间一定是能够互操作的,比如C#可以和C++互相调用。任何语言开发的程序都可以嵌入Browser Control来借用IE的功能。Office暴露了VBA接口,通过VBScript都能够自动化Office程序。各种管理工具无论是Explorer还是MMC,都暴露了编程接口可以让程序员添加自己的功能。我见过客户用ASP.NET在后台用VBScript生成Excel表格,然后把表格嵌入在IE浏览器中,再使用JavaScript来帮助客户编辑,最后提交回MSSQL数据库做完成报销功能的。完善的互操作性充分保护了用户的投资,使得客户对微软平台用一次就上瘾,几乎没有不可能完成的任务。仔细分析,其实这些互操作有一个共性,都是把暴露COM接口作为内部实现原理。这个做法导致了三个局限性,首先是牵涉到了前面提到的COM的复杂度。其次是潜在的性能损耗。最后是在具体开发的时候,都需要一些仪式性的工作来引入或者定义COM接口,使得开发过程不够自然流畅。在CLR和COM互操作的调用栈里面,CLR的RCW, CCW, 安全处理, 列集拷贝等等,耗费的时间带来的性能开销简直是可以到了肉眼可察觉的地步(听硬盘的声音和看任务管理器里面CPU的波动)。
Win8
在讨论完这些技术背后的故事后,再看看为啥我就对Win8爱不释手了。
Win8引入了Windows Runtime, 简称WinRT. WinRT是一个操作系统模块,运行在用户态,介于Win32的上层和应用程序的下层,目的在于提供更高效友好的开发接口供Win8的程序员使用。WinRT在二进制模型上基本就是照搬了经典的COM。WinRT和CLR互不依赖,WinRT可以被CLR使用。WinRT通过C/C++实现,效率高是一个方面,更重要的时Win8引入了projection的概念,就是可以把WinRT的API用最直接最高效的方法,提供给上层的编程语言调用。这个语言可以是C#, C或者JavaScript。
对于第一次接触Projection的朋友,可以把Projection认为是一种新的Windows API模型。传统的操作系统API,要么是暴露DLL的方法,要么是通过COM接口。无论是哪一种,在CLR中调用的时候都有不小的开销。使用这些传统API的效率,比调用一个C#自己的方法,效率差了多个数量级,根本的原因在于CLR的安全模型、内存模型和传统的unmanaged模型不兼容,所以跨越边界的调用需要额外的代码来处理。而Projection提供的模型,是在提供新功能的同时,还针不同编程模型和语言,提供了最利于它们调用的方法。这样就主动避免了不同模型之间为了互相兼容导致的开销,也使得程序员写代码的时候非常自然流畅,调用的时候根本感觉不到和调用本地函数的区别。当然,能够实现这一点, 也是得益于CLR, C#语言和VS开发工具这十年的长足发展. 举个例子, C# 5.0中引入了await关键字, WinRT中引入了async operation. Projection技术把C#中的await语句转换为WinRT async operation的调用,而且这个调用直接从managed code直接跳到unmanaged code,中间没有任何冗余,也不需要CLR Engine的介入。进一步的信息, 可以参考Build大会关于WinRT的多个演讲。后面的callstack也提供了直观的例子.
前面提到了COM的局限性在于一个轻量的二进制模型,被硬生生的扩展成一个无所不能的框架。WinRT取其精华,去其糟粕,借用了COM的轻便,舍弃了复杂性,在扩展性上依托于上层的编程语言和工具。WinRT通过projection的技术解决了传统互操作性效率不高使用不方便的问题。以前的路线是希望所有的产品和技术最后都统一到CLR上来。现在修正为底层模型通过WinRT和C来实现,然后把这一层高效的组件无缝提供给上层的开发技术比如CLR来使用。这个转变重新重视unmanaged层面的二进制模型。归纳为,unmanaged模型的优势在于执行效率高,可以通吃所有场景,缺点在于开发和使用成本也高。CLR的优势在于开发成本低,缺点在于无法通吃各种需求。现在微软自己用unmanaged来做WinRT,然后把WinRT提供给上层语言,这两者就可以取长补短了。
有了WinRT,有了unmanaged的回归,再加上微软开发工具和C#语言的长足发展,前面介绍的各种技术在Win8里面就相得益彰了。Metro是如何修复WPF的缺陷就显而易见了: Win8的metro程序继续使用WPF中引入的rendering模型和XAML, 但是在control的基础设计和实现上,从CLR转移到了C,然后通过WinRT来暴露给使用者。至于使用的灵活性,比如要不要实现数据绑定,就看上层使用者自己的选择了。
附录
下面是我研究过程中看到的一些Win8的callstack. 基于这些callstack和我以往的经验,才写了上面的文章.
Stack1:
Windows_UI_Immersive!`Windows::Internal::CMessageDialog::ShowAsync'::`50'::::
Windows_UI_Immersive!Windows::Internal::CMessageDialog::ShowAsync+0x1a0
image08ee0000!DomainNeutralILStubClass.IL_STUB_CLRtoCOM()+0x8c
application1!Application1.MainPage+d__0.MoveNext()+0xcd
application1!Application1.MainPage.button_Click(System.Object, Windows.UI.Xaml.RoutedEventArgs)+0x80
stack2:
combase!CComActivator::DoCreateInstance+0x121
combase!CoCreateInstanceEx+0x51
combase!CoCreateInstance+0x65
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::_TryToUnregisterForIHMNotifications+0x3b
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::_HideWindow+0x31
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::HideWindow+0x9
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::DestroyWindow+0x24
Windows_UI_Immersive!Windows::Internal::CPopupWindow::Destroy+0x57
Windows_UI_Immersive!Windows::Internal::CClosePopupCommandHandler::Invoke+0xe 0x73
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::_OnButtonPress+0xc0
Windows_UI_Immersive!Windows::Internal::CPopupWindowImpl::OnMessage+0x18b
DUI70!DirectUI::NativeHWNDHost::WndProc+0x73
USER32!InternalCallWinProc+0x23
USER32!UserCallWinProcCheckWow+0x100
USER32!DispatchMessageWorker+0x3d4
USER32!DispatchMessageW+0x10
Windows_UI_Immersive!SHProcessMessagesUntilEventsEx+0xe2
Windows_UI_Immersive!Windows::Internal::PopupWindowOperation::Show+0x103
Stack3:
Windows_UI_Xaml!HWWalk::RenderChildren+0x7a
Windows_UI_Xaml!HWWalk::RenderContentAndChildren+0x2d1
Windows_UI_Xaml!HWWalk::Render+0x61e
Windows_UI_Xaml!HWWalk::RenderChildren+0x7a
Windows_UI_Xaml!HWWalk::RenderRoot+0x1c4
Windows_UI_Xaml!CCoreServices::NWDrawTree+0x698
Windows_UI_Xaml!CCoreServices::NWDraw+0x1d6
Windows_UI_Xaml!CRenderTarget::Draw+0x13
Windows_UI_Xaml!CWindowRenderTarget::Draw+0x40
Windows_UI_Xaml!CXcpBrowserHost::OnTick+0x88
Windows_UI_Xaml!CXcpDispatcher::Tick+0x184
Windows_UI_Xaml!CXcpDispatcher::OnReentrancyProtectedWindowMessage+0x133
Windows_UI_Xaml!CXcpDispatcher::ProcessMessage+0xa4
Windows_UI_Xaml!CXcpDispatcher::WindowProc+0x69
USER32!InternalCallWinProc+0x23
USER32!UserCallWinProcCheckWow+0x100
USER32!DispatchMessageWorker+0x3d4
USER32!DispatchMessageW+0x10
windows_ui!Windows::UI::Core::CDispatcher::ProcessMessage+0xc7
windows_ui!Windows::UI::Core::CDispatcher::ProcessEvents+0x6c
Windows_UI_Xaml!CJupiterWindow::RunCoreWindowMessageLoop+0x3b
Windows_UI_Xaml!CJupiterControl::RunMessageLoop+0x1d
Windows_UI_Xaml!CJupiterControlLight::RunMessageLoop+0x8
Windows_UI_Xaml!DirectUI::DXamlCore::RunMessageLoop+0x15
Windows_UI_Xaml!DirectUI::ViewProvider::Run+0x11
twinapi!Windows::ApplicationModel::Core::CoreApplicationView::ViewProviderThreadProc+0x27