treenode

在路上。

BlogJava 首页 新随笔 联系 聚合 管理
  5 Posts :: 1 Stories :: 53 Comments :: 0 Trackbacks

前言

 
作为一个专注于
C/S 方面开发的程序员,我一直对“面向对象的编程框架如何与 Windows 操作系统的消息机制打交道”这个问题有着相当大的兴趣。读者想必知道,象 MFC VCL SWT 这样的类库在实现界面处理的时候,有几个主要问题是不得不考虑的。首先是如何为窗口和控件这样的界面以面向对象方式进行包装——这一方面可以说没多少技术上的难题;从一般意义上讲,不过是把 HWND 作为第一个参数的函数分类整理一下而已。当然,具体作起来还是有不少东西需要认真考虑,只是这些问题多半是在设计的层面,考虑包装是否完善、维护和扩展起来是否方便等等;在实现上基本上就没什么需要克服的技术障碍了。而另一方面——即如何处理系统消息机制,则是一个颇费脑筋的问题了。其中最大的难点之一,就是 Windows 的消息系统依赖于窗口过程(术语叫做 Window Procedure ),而这个窗口过程却是一个非面向对象的、普通的全局函数,它完全不理解对象是什么;而为了让整个程序 OO 起来,你还非得让它去操纵对象不可。因此,如何将窗口过程用面向对象的方法完美的封装起来,就成为各种类库面临的最大挑战之一。当然,这也理所当然的成为各个开发小组展示自身功力的绝好舞台。

 
据我所知,在此一问题上,不同的类库采纳了不同的做法。较早的
MFC 使用了窗口查找表的技术,即为每个窗口和对应的窗口过程建立一个映射;需要处理消息的时候,则是映射表中找到窗口所对应的过程,并调用之。这样会带来几个问题。首先是每次进行查表势必浪费时间,为此 MFC 不惜在关键处使用 Cache 映射和内联汇编的方法以提高效率。第二个问题:映射表是和线程相关联的,如果你将窗口传递给另外一个线程, MFC 无法在该线程中找到窗口的映射项,也就不知该如何是好,于是只能出错。我已经在很多地方看到有人问跨线程传递窗口指针的疑问,多半都是因为不理解 MFC 的消息处理机制。正因为如此, MFC 的使用者必须强制遵守一些调用方面的约定,否则会出现很多莫名其妙的错误,这无疑是框架不够友好的表现。而稍晚出现的 VCL ATL 则使用了一种比较巧妙的 Thunk 技术,利用函数调用过程中使用堆栈的原理,巧妙的将对象指针“暗度陈仓”地偷偷传递进去,并通过一些内存中的“小动作”越过了通常的处理机制。这样做的好处是节省了额外维护映射表的开销,速度相当快,同时也不存在线程传递的问题。当然,这个过程因为大量使用汇编,而且需要对函数调用的底层机制有深刻的理解,所以很难为一般程序员所理解和运用。(相应的维护起来也难度也比较高——还记得 Anders 离开 Borland 以后相当长时间没有人敢改动 Delphi 底层代码的往事吗?)

 

在众多框架中, SWT 算是比较年轻的一个,也是颇为独特的一个。之所以说它特殊,因为它是用 Java 编写的。我们知道,和 Windows 平台上的本地开发工具不同, Java 程序是生活在自己的虚拟机中的,除非通过 JNI 这个后门,否则它对底下的操作系统根本一无所知。这显然为设计者提出了更高的挑战。那么, SWT 又是如何实现这一点的呢?非常幸运, SWT 是完全开放源代码的(当然, MFC VCL 也是开放的,不过这种开放就比较小家子气——许多时候只有你购买昂贵的企业版以后才能看到这些宝贵的源码, D 版且不论)。开放源代码为我们研究其实现扫清了障碍。

 

准备工作

 

在上路之前,我们应当准备好足够的武器。当然, Eclipse 是必不可少的——我使用的是最新的 Eclipse 3.2 RC6 版本,不过只要是 3.x 的版本,在核心代码方面应该不会有很大差别,所以对本文的目的而言, Eclipse 3.0 以上的任何版本都是够用的。此外,如果你还没有安装任何界面开发方面的插件的话,我强烈建议你安装一个 Eclipse.org 官方的 Visual Editor 。这倒不是说我认为该插件对界面开发有多大的助力——事实上从功能上来说它要比 SWT Designer 等同类产品逊色;但是该插件最大的好处在于可以非常简单的设定好 SWT 程序所运行的环境,还包括源代码支持,这样你就可以很轻松的跟踪到 SWT 源代码内部去了。并且这个工具是没有使用限制的,也不需要注册激活,这一点要比 SWT Designer 来得方便。

 

安装 Visual Editor 以后,你可以在创建项目的过程中使用 Java Settings 页面,或者在项目创建以后再选择项目属性,从 Java Build Path 分支下的 Libraries 页面访问同样的界面:

然后按下 Add Library 按钮。如果 Visual Editor 安装正确,这里会多出一个 Standard Widget Toolkit 项。选择它然后 Next


默认选中的 IDE Platform 不用变,不过最好也勾选上 Include support for JFace library



然后按 Finish 。这样准备工作就完成了。

 

 上路吧!

 

现在我们可以对 SWT 的源代码着手进行分析了。不过,应当从哪里开始下手呢?答案取决于对消息机制的理解。我们知道,任何 Windows 程序(严格地说,应当是有用户界面的程序,而不包括控制台应用和系统服务程序)都是从 WinMain 开始的;而 WinMain 中最重要的部分则是消息循环,这也是任何 Windows 程序得以持续运行的生命之源,所以有人称之为“消息泵”,就是因为它象心脏一样为应用程序的生命源源不断的输送动力。通常,在用 SDK 编写的程序中会有如下的调用:

while  ( GetMessage( & msg, NULL,  0 0 ) )

{

   TranslateMessage( 
& msg );

   DispatchMessage( 
& msg );
}

 
SWT 应用程序,尽管实现方法不同,但是看起来非常相似:

while  (  ! shell.isDisposed() )

{

    
if  (  ! display.readAndDispatch() )

       display.sleep();

}

仅从文字上推断,也很容易猜想:Display.readAndDispatch()方法所作的和SDK程序中Translate/Dispatch两行所作的事情应该是类似的;而sleep方法,则在SDK程序中没有直接的对应物。接下来,我们可以按住Ctrl键然后点击readAndDispatch方法,去探查一下它内部是如何实现的。

public   boolean  readAndDispatch () {

    checkDevice ();

    drawMenuBars ();

    runPopups ();

    
if  (OS.PeekMessage (msg,  0 0 0 , OS.PM_REMOVE)) {

       
if  ( ! filterMessage (msg)) {

           OS.TranslateMessage (msg);

           OS.DispatchMessage (msg);

       }

       runDeferredEvents ();

       
return   true ;

    }

    
return  runMessages  &&  runAsyncMessages ( false );


虽然这里有一些新鲜的东西,不过总体上来说没有太大意外。我们如预想的那样看到了对Translate/DispatchMessage方法的调用,这证明SWT的消息循环和一般的本地程序是没有本质差别的。不过和SDK程序有所不同的是,这里使用了PeekMessage,而非传统SDK程序中所使用的GetMessage。(事实上,现代的大多数UI框架也倾向于采用PeekMessage而非GetMessage,不信的话你可以自己去查查看。)

 

为什么是 PeekMessage 而非 GetMessage 呢?这是因为:除了操作系统通过正常途径发送来的消息以外,应用程序通常还要额外使用一些内部的消息,这些消息需要通过“非常规”的途径进行处理。如果使用 GetMessage 的话,它只有在应用程序消息队列中存在消息的时候才会被唤醒,那些“非常”消息就失去了获得及时处理的机会。例如, SWT 就创建了一些用于线程通信的内部消息,这些消息是 Display.syncExec Display.asyncExec 得以正常运作的基础。上面 filterMessage runDeferredEvents 方法就对此有所涉及。不过因为这些辅助方法和本文的主题没有直接关系,所以我不打算对它们作什么说明;如果你有兴趣的话,可以自己去研究一下这些函数内部究竟做了些什么。

 

接下来我们看看 SWT 消息循环中另外一个意义不明的方法: sleep

public   boolean  sleep () {

    checkDevice ();

    
if  (runMessages  &&  getMessageCount ()  !=   0 return   true ;

    
if  (OS.IsWinCE) {

       OS.MsgWaitForMultipleObjectsEx (
0 0 , OS.INFINITE, OS.QS_ALLINPUT, OS.MWMO_INPUTAVAILABLE);

       
return   true ;

    }

    
return  OS.WaitMessage ();

}

中间的代码明显是针对WinCE系统的,可以不去管它。有点意外的是这里出现了WaitMessage,这是一般程序中比较少见的一个函数调用。不过认真想想,原因大概也可以理解。PeekMessageGetMessage的不同之处在于:如果消息队列中没有消息可抓,那么GetMessage会释放控制权让其他程序运行,而PeekMessage却不会。即使是在抢占式多任务操作系统中,一个程序总是攥着控制权不放也不是好事。因此,如果真的没有任何消息需要处理,那么WaitMessage将使线程处于睡眠状态,直到下个消息到来才再次唤醒——这也是SWT为什么把该方法定名为sleep的原因。

 

通过上面的研究我们看到:抛开无关的细节,消息循环的处理本身是非常简单的。然而,这些研究尚不足以解决我们的疑惑。最关键的窗口过程究竟是在哪定义的呢?很显然,我们需要追踪窗口的创建过程,来找到定义窗口过程的地方。所以接下来的研究对象就是 Shell

 

Shell 类并没有类似 create 这样的方法,因此我们可以合理的猜想:创建窗口的过程大概就放在构造函数中。

 

接下来我们跟踪 Shell 的实现代码来证实此猜想。不过有一点值得先作个说明:你可能已经知道, Shell 对象具有一个很深的继承层次——它的直接父类是 Decoration ,而这个类的父类又是 Canvas Canvas 的父类是 Composite ,依此类推。你必须知道这个层次的原因是: Shell 创建过程中经常会用到祖先类中的一些方法,同时也会重载祖先类中的部分方法,因此在跟踪代码的时候,你也得根据方法的调用者实际所在的类,在这个类层次中上下移动。 Eclipse 提供的 Hierarchy 视图是个不错的工具,可以让它来帮助你,如下图所示。小心不要迷路!


 

经过一番跟踪,我们有了如下的发现:

l        通常,我们调用的是型如Shell(Display)或者Shell(Display, style)这样的构造函数。这两个构造函数都会调用内部的其他一些形式的构造函数,最终调用如下的形式:

Shell(Display, Shell parent, int style, int handle);

l         上述方法的最后一步调用了createWidget()。这个方法的名字应该让你马上有一种“我找到了”的感觉;

l         Shell本身并没有定义createWidget()方法,实际上它调用的是Decorations.createWidget

l         Decorations.createWidget其实并没有做什么事,只是简单的调用上级(Canvas)的实现,然后修改一些内部状态。不过,Canvas并没有重载createWidget,因此控制继续向上,来到Scrollable

l        同样,Scrollable.createWidget也是简单的向上调用。Control类才是完成真正工作的地方。我们可以从代码中看到,这个类作了相当多的工作:

void createWidget () {

    foreground 
= background = -1;

    checkOrientation (parent);

    createHandle ();

    checkBackground ();

    checkBuffered ();

    register ();

    subclass ();

    setDefaultFont ();

    checkMirrored ();

    checkBorder ();

    
if ((state & PARENT_BACKGROUND) != 0) {

       setBackground ();

    }

}


有经验的读者从名字应当能够猜到,上面这么多方法中,createHandle才应当是真正值得我们关心的。

void createHandle () {

    
int hwndParent = widgetParent ();

    handle 
= OS.CreateWindowEx (

       widgetExtStyle (),

       windowClass (),

       
null,

       widgetStyle (),

       OS.CW_USEDEFAULT, 
0, OS.CW_USEDEFAULT, 0,

       hwndParent,

       
0,

       OS.GetModuleHandle (
null),

       widgetCreateStruct ());

    ….

}


我没有把完整的代码列出来;因为,既然已经看到了CreateWindowEx,就知道我们想找的东西已经就在眼前,没有必要再找下去了。

 

createWindowEx方法必须指定要创建的窗口类名字,也就是上面代码中windowClass()方法所作的事情。我们接着看看这个类名应当是什么。然而,我们发现windowClass()Control类中定义为抽象方法:

abstract TCHAR windowClass ();

这意味着实际上类的名字是由具体的子类来指定的。所以我们还要继续跟踪下去。因为继承层次上每个类都能够改写这个方法,所以我们不应该从现在的位置回头向下,而是应当从最底层的Shell开始向上找——这样,你找到的第一个被重载的地方就是最终的实现。

 

Shell的确实现了windowClass()方法,方法如下:

TCHAR windowClass () {

    
if (OS.IsSP) return DialogClass;

    
if ((style & SWT.TOOL) != 0) {

       
int trim = SWT.TITLE | SWT.CLOSE | SWT.MIN | SWT.MAX | SWT.BORDER | SWT.RESIZE;

       
if ((style & trim) == 0return display.windowShadowClass;

    }

    
return parent != null ? DialogClass : super.windowClass ();

}


因为这里涉及到其他一些变量,所以其意图最初看上去可能不是很明确。总体的逻辑大概是这样的:如果Shell发现用户要创建的是一个对话框,那么将返回Dialog的内部类名。否则,调用上级类的实现(shadowClass则是SWT内部维护的一个需要特殊处理的类)。

 

因为Shell的实现调用了基类,所以我们还是要往上走。DecorationsCanvasComposite都没有重载windowClass()方法。继续来到Scrollable类中,这个方法具有如下的实现:

TCHAR windowClass () {

    
return display.windowClass;

}


现在线索转到了Display类。然而,windowClass只是Display类的一个字段,而非方法,这个字段一定是在哪个地方得到了初始化。问题就是:究竟在哪初始化的呢?

 

好在,我们只需要在Display类查找哪里修改了windowClass字段就可以了。很快可以发现如下的方法:

protected void init () {

    
super.init ();

       

    
/* Create the callbacks */

    windowCallback 
= new Callback (this"windowProc"4); //$NON-NLS-1$

    windowProc 
= windowCallback.getAddress ();

    
if (windowProc == 0) error (SWT.ERROR_NO_MORE_CALLBACKS);

    …

    
/* Use the character encoding for the default locale */

    windowClass 
= new TCHAR (0, WindowName + WindowClassCount, true);

    windowShadowClass 
= new TCHAR (0, WindowShadowName + WindowClassCount, true);

    WindowClassCount
++;

上面代码中用到了两个相关字段:windowName是一个实例变量,其值为“SWT_Window”;而windowClassCount则是一个静态变量,没有说明初始值,那么就是默认值0

 

稍稍分析一下就能明白:当init()方法第一次被调用的时候,windowClass将被设置为字符串“SWT_Window0”(你可以将TCHAR对象视为和字符串等同的东西),然后windowClassCount递增。如果init()方法第二次被调用,那么下一个类名将会是SWT_Window1。不过,通常情况下我们的SWT程序仅有一个Display对象,也仅会初始化一次。也因此,所有顶层窗口的类名都应当是“SWT_Window0”。

 

你可以用SPY++或者Winsight32之类的工具来证实这一点(如下图)。

知道了类名以后怎么办呢?还是要从消息机制的原理上找到线索。而在Windows中将一个窗口类和窗口过程连接起来的关键是:调用RegisterClass或者RegisterClassEx,并将类名和窗口过程的地址作为参数一并传入。所以,下面我们的目标是查找在哪里调用了RegisterClass

 

因为windowClass是定义在Display类中的,按照就近的原则,我们就从这里找起。果然不出所料,在init()方法接下来的部分就有这样的代码:

/* Register the SWT window class */

    
int hHeap = OS.GetProcessHeap ();

    
int hInstance = OS.GetModuleHandle (null);

    WNDCLASS lpWndClass 
= new WNDCLASS ();

    lpWndClass.hInstance 
= hInstance;

    lpWndClass.lpfnWndProc 
= windowProc;

    lpWndClass.style 
= OS.CS_BYTEALIGNWINDOW | OS.CS_DBLCLKS;

    lpWndClass.hCursor 
= OS.LoadCursor (0, OS.IDC_ARROW);

    
int byteCount = windowClass.length () * TCHAR.sizeof;

    lpWndClass.lpszClassName 
= OS.HeapAlloc (hHeap, OS.HEAP_ZERO_MEMORY, byteCount);

    OS.MoveMemory (lpWndClass.lpszClassName, windowClass, byteCount);

    OS.RegisterClass (lpWndClass);


init()方法的其他部分还注册了另外一些辅助窗口,比如阴影窗口等;此外还注册了一个全局钩子。这些部分和消息机制的核心没有直接关系,可以不去管它。关键在于这一行:

    lpWndClass.lpfnWndProc = windowProc;

 

回头看看,在init()方法的开头部分,windowProc成员是这样初始化的:

    /* Create the callbacks */

    windowCallback 
= new Callback (this"windowProc"4); //$NON-NLS-1$

    windowProc 
= windowCallback.getAddress ();

    
if (windowProc == 0) error (SWT.ERROR_NO_MORE_CALLBACKS);


这里出现了一个神秘的类:Callback。有Windows 编程经验的读者大概会回想起,在Windows消息机制中,Callback是一个非常核心的概念。虽然Java程序员或许不熟悉它,不过事实上它可谓是Windows中的“控制反转”或曰“依赖注入”——早在Java和模式大行其道之前很久,Windows中的一些手法已经暗合了最新的编程范式,只是当时没有人给它起一个听上去比较吓人的名字而已。

 

跑题了,回到正文上来。先不看Callback的实现,从这段代码我们大概可以猜到:

l         Callback类就是将OO的世界和非OO的世界连接起来的桥梁;

l         Callback的构造函数中,提供了处理消息的目标对象和处理消息的方法名称。最后那个参数4你不妨先猜猜看是什么意思;

l         CallbackgetAddress()返回的应该是一个地址,也就是——你应当猜到了——正是回调函数的地址;

l         Callback背后一定有某种魔法,把传入的对象方法和getAddress返回的回调函数巧妙的连接起来。

 

接下来,我们要进行的是这个历程中最艰苦的部分:揭示Callback类背后的神秘魔法。

(未完待续) 

posted on 2006-06-03 12:46 TreeNode 阅读(3462) 评论(10)  编辑  收藏 所属分类: SWT,JFace和RCPJava技术

Feedback

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-04 09:01 Jet Geng
强人。期待下一篇。  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-04 16:41 foxcai
楼主.
你后面的图片都没有贴上来啊

能不能处理一下啊.
  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-04 17:32 TreeNode
答楼上,我已经努力了一整天,不知是否因为文章太长格式复杂,这个HTML编辑器速度难以忍受而且频频出现脚本错误,上传文件也失败。我觉得很失望。或许我会想其他办法解决。  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-05 21:57 foxcai
楼主表着急.
慢慢来.
期待你的下篇.  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-05 22:43 foxcai
看来楼主的Win编程也很厉害啊.
看了楼主的文章感觉霍然开朗,有了些感觉.
强烈期待下篇.

楼主能不能写一些关于SWT线程的文章呢?  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-05 23:51 TreeNode
今天上传文件仍然失败,我放弃了。全文做成PDF格式,有兴趣的可以到这里下载:

http://www.yousendit.com/transfer.php?action=download&ufid=19BF243E3E7F9D9C


或者如果有Eclipse中文社区帐号的话,这里也可以:

http://www.eclipseworld.org/bbs/read.php?tid=5132


SWT的线程,只要了解Display对象提供的几个同步方法,其他方面和一般的Java线程没有什么差别了。Eclipse.org上面的文章也说得很明白,似乎没有什么东西可写的。  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-09 03:01 foxcai
已经下载了.

希望楼主能有更多好的文章.  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-19 11:15 hhh
能否谈谈swing呢!  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2006-06-19 19:13 TreeNode
Swing我不熟悉,不评论。
  回复  更多评论
  

# re: SWT: 深入内幕之消息机制探秘(上篇) 2007-06-29 12:39 SWT
具体两个线程间是如何通信的 ?能否给举个实例!谢谢 !现在急用
有的话!通知我一声!非常感激!我的QQ是84750858  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航: