SWT深入内幕之消息机制探秘


第 20 页 SWT: 深入内幕之消息机制探秘 作者:Treenode 于 2006-6-3 前言 作为一个专注于 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 版且不论)。开放源代码为我们研究其实现扫清了障碍。 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 由于使用 SWT 的过程中遇到一些障碍,我开始考虑有没有突破其限制的办法,也以此为契机阅读 了 SWT 的部分源代码,了解到其中一些底层的东西。本文可以说是这次研究的副产品。我把自己 的一些心得整理出来,希望有类似爱好的朋友能从中发现一些有趣的东西。当然,更希望对 SWT 有丰富经验的读者来批评指正。 本文欢迎转载,但是请不要随意修改文中的内容。 准备工作 在上路之前,我 们 应 当 准备好足够的武器。当 然 ,Eclipse 是必不可少的——我使用的是最新的 Eclipse 3.2 RC6 版本,不过只要是 3.x 的版本,在核心代码方面不会有很大差别,所以对本文的目的而言, Eclipse 3.0 以上的任何版本应该都是够用的。此外,如果你还没有安装任何界面开发方面的插件的 话,我强烈建议你安装一个 Eclipse.org 官方的 Visual Editor。这倒不是说我认为该插件对界面开发 有多大的助力——事实上在我看来,它的功能要比 SWT Designer 等同类工具逊色;但是该插件最大 的好处在于可以非常简单的设定好 SWT 程序所运行的环境,还包括了源代码支持,这样你就可以 很轻松的跟踪到 SWT 源代码内部去了。此外,这个工具是没有使用限制的,也不需要注册激活, 以学习的目的而言这是一个胜于 SWT Designer 的优点。 Visual Editor 和 Eclipse 平台的其他插件都可以从 http://www.eclipse.org/downloads 上下载。 安装 Visual Editor 以后,你可以在创建项目的过程中使用 Java Settings 页面,或者在项目创建以后 再选择项目属性,在 Java Build Path 分类下的 Libraries 页面访问同样的设置界面: PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 然后按下 Add Library 按钮。如果 Visual Editor 安装正确,这里会多出一个 Standard Widget Toolkit 项。选择它然后 Next。 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 默认选中的 IDE Platform 不用变,不过最好也勾选上 Include support for JFace library,免得以后用到 JFace 的时候多添麻烦。 然后按 Finish。这样准备工作就完成了。 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 开始 Dirty work 吧! 现在我们可以对 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 程序中所使用的 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 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,这 是一般程序中比较少见的一个函数调用。不过认真想想,原因大概也可以理解。PeekMessage 和 GetMessage 的不同之处在于:如果消息队列中没有消息可抓,那么 GetMessage 会释放控制权让其 他程序运行,而 PeekMessage 却不会。即使是在抢占式多任务操作系统中,一个程序总是攥着控制 权不放也不是好事。因此,如果真的没有任何消息需要处理,那么 WaitMessage 将使线程处于睡眠 状态,直到下个消息到来才再次唤醒——这也是 SWT 为什么把该方法定名为 sleep 的原因。 通过上面的研究我们看到:抛开无关的细节,消息循环的处理本身是非常简单的。然而,这些研究 尚不足以解决我们的疑惑。最关键的窗口过程究竟是在哪定义的呢?很显然,我们需要追踪窗口的 创建过程,来找到定义窗口过程的地方。所以接下来的研究对象就是 Shell。 焦点转向 Shell Shell 类并没有类似 create 这样的方法,因此我们可以合理的猜想:创建窗口的过程大概就放在构造 函数中。 接下来我们跟踪 Shell 的实现代码来证实此猜想。不过有一点值得先作个说明:你可能已经知道, Shell 对象具有一个很深的继承层次——它的直接父类是 Decoration,而这个类的父类又是 Canvas, Canvas 的父类是 Composite,依此类推。你必须知道这个层次的原因是:Shell 创建过程中经常会用 到祖先类中的一些方法,同时也会重载祖先类中的部分方法,因此在跟踪代码的时候,你也得根据 方法的调用者实际所在的类,在这个类层次中上下移动。Eclipse 提供的 Hierarchy 视图是个不错的 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 工具,可以让它来帮助你,如下图所示。小心不要迷路! 经过一番跟踪,我们有了如下的发现: 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 (); } } PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 有经验的读者从名字应当能够猜到,上面这么多方法中,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) == 0) return display.windowShadowClass; } return parent != null ? DialogClass : super.windowClass (); } 因为这里涉及到其他一些变量,所以其意图最初看上去可能不是很明确。总体的逻辑大概是这样的: 如果 Shell 发现用户要创建的是一个对话框,那么将返回 Dialog 的内部类名。否则,调用上级类的 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 实现(shadowClass 则是 SWT 内部维护的一个需要特殊处理的类)。 因为 Shell 的实现调用了基类,所以我们还是要往上走。Decorations、Canvas、Composite 都没有重 载 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 之类的工具来证实这一点(如下图)。 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 知道了类名以后怎么办呢?还是要从消息机制的原理上找到线索。而在 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()方法的其他部分还注册了另外一些辅助窗口,比如阴影窗口等;此外还注册了一个全局钩 子。这些部分和消息机制的核心没有直接关系,可以不去管它。关键在于这一行: PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 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 Callback 的 getAddress()返回的应该是一个地址,也就是——你应当猜到了——正是回调函数的 地址; l Callback 背后一定有某种魔法,把传入的对象方法和 getAddress 返回的回调函数巧妙的连接起 来。 接下来,我们要进行的是这个历程中最艰苦的部分:揭示 Callback 类背后的神秘魔法。 Callback – 魔法是如何炼成的 首先,我们看看 Callback 对象的构造函数。 public Callback (Object object, String method, int argCount) { this (object, method, argCount, false); } 上面遗留的一个问题在这里得到了解答:Callback 构造函数的最后一个参数就是回调函数所需的参 数个数。回想一下窗口过程的原型: LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 没错,正是 4 个! 继续往下跟踪。Callback 构造函数又调用了内部的另外一种形式的构造函数,并附加了下列参数: isStatic=false 说明 Display 类处理消息的方法是实例成员,不是静态成员; isArrayBased=false 说明参数是逐个传递,而不是作为一个数组统一传递的; errorResult=0 这个参数是用来处理 Java 异常的。 上述参数都是为了能够更加灵活的处理任何形式的回调方法而准备的。既然我们已经知道窗口函数 有固定的原型,这些参数现在来讲对我们的研究来说没什么意义。OK,略过略过。 真正的处理位于这个形式的构造函数中: public Callback (Object object, String method, int argCount, boolean isArrayBased, int errorResult) { /* Set the callback fields */ this.object = object; this.method = method; this.argCount = argCount; this.isStatic = object instanceof Class; this.isArrayBased = isArrayBased; this.errorResult = errorResult; /* Inline the common cases */ if (isArrayBased) { signature = SIGNATURE_N; } else { switch (argCount) { case 0: signature = SIGNATURE_0; break; //$NON-NLS-1$ case 1: signature = SIGNATURE_1; break; //$NON-NLS-1$ case 2: signature = SIGNATURE_2; break; //$NON-NLS-1$ case 3: signature = SIGNATURE_3; break; //$NON-NLS-1$ case 4: signature = SIGNATURE_4; break; //$NON-NLS-1$ default: signature = getSignature(argCount); } } /* Bind the address */ address = bind (this, object, method, signature, argCount, isStatic, isArrayBased, errorResult); } PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 前半部分可谓一目了然——仅仅是保存内部信息的赋值操作而已。接下来的部分似乎是用来指 定签名(signature)的,那么这个签名用来做什么呢?答案是调用JNI。我们知道,不仅Java 程序可以通过JNI调用C/C++程序,反过来C/C++也可以调用Java,不过这就要比前者麻烦的多。 其中一个必需的步骤,就是指定要调用的方法签名,包括参数的数量和类型、以及方法的返回值。 这可以视为数据传递的协议,只有Java和C/C++两方就协议达成一致,在 调 用 的时候才能正确的相 互协作,不 会出现问题。(熟悉COM的读者可能会说:啊,这不就是Java和C++之间的Marshaling 么!没错,完全可以这么理解。)如果你跟踪这段代码,会发现最后生成的签名类似于“(IIII)I” 这样的形式。很容易猜想,该字符串意味着该方法有4个整数类型的参数,也返回一个整数类型的 返回值——正是窗口函数的原型! Callback构造函数的最后一步是调用bind();Callback类将自身的引用也作为一个参数,和 构造函数传递进来的其他参数一起发送给bind方法。而bind()则是一个本地方法: static native synchronized int bind (Callback callback, Object object, String method, String signature, int argCount, boolean isStatic, boolean isArrayBased, int errorResult); 这意味着:接下来我们必须离开 Java 美好的 OO 世界,到 C 的蛮荒之地——JNI 代码中——来一探 根源。 异世界的代码——欢迎来到 JNI 的领域! 在往下看之前请做好心理准备。以下的代码来自一个奇怪的、一般程序员都不太愿意接触的领域— —这就是 JNI,位于 Java 和 C/C++两个截然不同的世界之间的一个灰色地带。如果你(和我一样) 并不会对 C 语言敬而远之,而且对细节感兴趣的话,可以继续往下。否则的话,可以直接跳到下一 部分去看结论好了。 bind()方法的实现大致如下: JNIEXPORT SWT_PTR JNICALL Java_org_eclipse_swt_internal_Callback_bind (JNIEnv *env, jclass that, jobject callback, jobject object, jstring method, jstring signature, jint argCount, jboolean isStatic, jboolean isArrayBased, SWT_PTR errorResult) { int i; jmethodID mid = NULL; jclass javaClass = that; const char *methodString = NULL, *sigString = NULL; if (jvm == NULL) (*env)->GetJavaVM(env, &jvm); if (!initialized) { memset(&callbackData, 0, sizeof(callbackData)); initialized = 1; PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 } if (method) methodString = (const char *) (*env)->GetStringUTFChars(env, method, NULL); if (signature) sigString = (const char *) (*env)->GetStringUTFChars(env, signature, NULL); if (object && methodString && sigString) { if (isStatic) { mid = (*env)->GetStaticMethodID(env, object, methodString, sigString); } else { javaClass = (*env)->GetObjectClass(env, object); mid = (*env)->GetMethodID(env, javaClass, methodString, sigString); } } if (method && methodString) (*env)->ReleaseStringUTFChars(env, method, methodString); if (signature && sigString) (*env)->ReleaseStringUTFChars(env, signature, sigString); if (mid == 0) goto fail; for (i=0; iNewGlobalRef(env, callback)) == NULL) goto fail; if ((callbackData[i].object = (*env)->NewGlobalRef(env, object)) == NULL) goto fail; callbackData[i].isStatic = isStatic; callbackData[i].isArrayBased = isArrayBased; callbackData[i].argCount = argCount; callbackData[i].errorResult = errorResult; callbackData[i].methodID = mid; return (SWT_PTR) fnx_array[argCount][i]; } } fail: return 0; } bind()的实现相当长,而且样子比较古怪,咋看上去恐怕会吓倒一大票人。然而,其中许多部分,包 括初始化、字符串的获取等等,是调用 JNI 的必需步骤,繁琐没错,难倒不算难。我把这段代码的 大致思路说明下: l 使用 Lazy Initialization 的手法,如果发现方法是第一次被调用,则将 Java 虚拟机的引用保存起 来供以后使用,并初始化内部数组 callbackData; l 将方法名称和签名从 Java 字符串转换为本地字符串以便读取; PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 l 根据对象引用和签名,获得对象方法的引用,并保存在 mid 变量中(这是一个 jmethodID,如 果你不知道此物为何,最好补习一下 JNI); l 在 内 部 数 组 callbackData 中找到第一个可用的位置,并将方法传递来的所有参数保存在该位置; l 最后,返回内部数组 fnx_array 中的一个成员。这个数组的意义以及其与 callBackData 的关联留 到后面讲解。 上面代码中用到的 callbackData 是一个结构数组,其结构类型声明如下: typedef struct CALLBACK_DATA { jobject callback; jmethodID methodID; jobject object; jboolean isStatic; jboolean isArrayBased; jint argCount; SWT_PTR errorResult; } CALLBACK_DATA; 你可以看到,这个结构虽然成员不少,其实相当简单——它只是把 bind()方法传递进来的参数全部 做一个本地拷贝,供以后使用。仅此而已。那么这个方法有什么神奇的地方呢?注意它返回 fnx_array[argCount][i];我们前面说过,这个返回值就是回调函数的地址。一切魔法都在这里发生。 来看看 fnx_array 的定义。大概是这个样子的: SWT_PTR * fnx_array[MAX_ARGS+1][MAX_CALLBACKS] = { FN_A_BLOCK(0) FN_A_BLOCK(1) FN_A_BLOCK(2) FN_A_BLOCK(3) FN_A_BLOCK(4) FN_A_BLOCK(5) FN_A_BLOCK(6) FN_A_BLOCK(7) FN_A_BLOCK(8) FN_A_BLOCK(9) FN_A_BLOCK(10) FN_A_BLOCK(11) FN_A_BLOCK(12) }; FN_A_BLOCK 是一个宏,而它展开后又包括多个 FN 宏。经过完全展开(我得说,这个宏展开的过 程对不熟悉宏机制的人来说根本就是一场噩梦。我在展开它的时候也是很吃了一惊)以后,你会发 现这是一个相当宏大的数组,包括 13*128 个指针成员,占用超过 6K 的空间。把它完全写出来的话 是根本不可能的,这里我们且稍稍做点简化。还记得窗口过程的参数有几个吗?没错,4 个。再作 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 一个简化,我们只考虑函数第一次被调用的情况(回忆一下,所有 SWT 窗口共用一个类名: SWT_Window0,因此所有顶层窗口也共享一个窗口过程,只需要调用 bind 一次即可)。这时, callbackData 数组中所有位置都是可用的,因此 fnx_array[argCount][i]等于 fnx_array[4][0]。也就是说 我们只要关注这个数组第五行第一列的元素就够了。 我不准备在这里列出详细的展开步骤——尽管过程相当麻烦,不过基本上是一些文字替换的操作, 没什么技巧性可言。展开以后结果大致是这个样子的: jint* fnx_array[12+1][128] = { { (jint*)fn0_0,(jint*)fn1_0, ... (jint*)fn15_0 }, { (jint*)fn0_1,(jint*)fn1_1, ... (jint*)fn15_1 }, ... { (jint*)fn0_4,(jint*)fn1_4, ... (jint*)fn15_4 }, ... { (jint*)fn0_12,(jint*)fn1_12, ... (jint*)fn15_12 }, }; 这样就可以看清楚了:fnx_array 原来是一个庞大的函数指针数组。那么,我们来看看 fn0_4 方法是 怎么定义的。可是这个 fn0_4 定义在哪呢?用文字查找是找不到的。不过原因很容易猜想——这么 多非常类似的方法,多半是另外一个宏把它隐藏起来了。在距离 fnx_array 不远的地方,我看到这样 的定义: FN_BLOCK(0) FN_BLOCK(1) ... FN_BLOCK(12) 看上去和 fnx_array 的排列方式很相似,不是吗?好的,我们展开其中一个,看看到底是什么...... LRESULT CALLBACK fn0_4(jint p1, jint p2, jint p3, jint p4) { return (LRESULT) callback(0, p1, p2, p3, p4); } Bingo!!这正是预想中的东西。 把所有东西串起来! 现在,整个局面就豁然开朗了。不过,这里的步骤相当多,一时间可能会让你有有点似乎混乱。让 我们再整理一下全部思路。请参照下图: PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 操作系统 SWT-Java代码 SWT-JNI代码 Display.init Display.windowProc Callback bind jmethod jmethod ... jmethod callbackData f()=callback(0) f()=callback(1) ... f()=callback(127) f()=callback(0) f()=callback(1) ... f()=callback(127) f()=callback(0) f()=callback(1) ... f()=callback(127) f()=callback(0) f()=callback(1) ... f()=callback(127) f()=callback(0) f()=callback(1) ... f()=callback(127) fnx_array callback(index,...) Window 0.0 0.1 1 2 3 5 6 7 9 8 4 为了清晰起见,我将整个过程分为三个阶段,逐一进行说明。 (第一阶段)JNI 初始化阶段 这一阶段的主要工作是初始化内部表格映射,舞台在 JNI 代码这一边。 0.0、 JNI 代码创建了一个数组,即 callbackData,用于存放将要调用的 Java 方法信息; 0.1、 JNI 代码另外创建一个表格 fnx_array,存放本地方法指针,其中每个指针都指向一个 fnxx_yy 方法(这里 xx 和 yy 代表该方法所在的行号和列号)。每个 fnxx_yy 方法的实现都是简单的调 用 callback 函数,并将第一个参数为该方法所在的列号; (第二阶段)映射建立阶段 这一阶段完成 SWT 部分和 JNI 代码的对接。 1、 Display 对象的 init()方法创建一个 Callback 对象,其参数为希望作为消息函数使用的 windowProc 方法得名称和类型签名; 2、 Callback 对象调用 bind()方法,将 Display 对象引用和其 wndProc 方法名、签名等信息传递给它; 3、 bind 方法从表格 callbackData 数组中找到一个空闲的位置,把由参数传入的对象方法(即 Display.windowProc 方法)信息存放在这里,并记住存放的位置序号。这样,就在 callbackData 数组和 Display.windowProc 方法之间建立了映射关系; 4、 bind 方法从表格 fnx_array 数组中上一步记下的列位置所在的列取出一个方法指针,并作为窗口 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 过程的地址返回给 SWT 部分; 5、 SWT 将返回的窗口过程地址作为参数调用 RegisterClass API。操作系统内部会完成把即将创建 的窗口类和此窗口过程联系起来的工作。 (第三阶段)消息处理阶段 这一阶段,窗口已经创建成功,并且操作系统中产生了一条针对该窗口的消息。让我们看看这条消 息是如何到达 SWT 进行处理的? 6、 操作系统根据消息所发生的窗口类,找到对应的窗口过程(即 fnx_array 中某个位置上的函数), 并将消息发送给该函数; 7、 该窗口过程的实现简单的调用 callback(),不 过 在第一个位置附加了该函数所在的列号作为参数; 8、 callback 函数从第一个参数获得该列的序号,并从 callbackData 数组中该列的位置上找到需要调 用的 Java 方法信息(在第三步中,这个位置已经和 Display.windowProc 关联起来了); 9 、 callback 函数根据找到的方法信息,使用 JNI 规范调用另一端的 Java 方 法 , 也 就 是 Display.windowProc。 现在,控制权来到了 Display 类的 windowProc 方法。这一条消息经过操作系统的调用,历经 JNI 硕 大无朋的表格参照和迂回曲折的方法指针,最终来到 SWT 代码。这真是一条迢迢长路(侯捷先生 戏称此种手法为“消息的二万五千里长征”)。但是,其中的技巧确实值得称道。我们看到,SWT 采 用了一种典型的“以空间换时间”的策略,创建了一个巨型的二维数组和多达上千个的方法,换来 的是方法指针调用的迅捷速度。 细心的读者可能会注意到上图中一处似乎赘余的地方。fnx_array 是一个二维表格,同一列中所有方 法的列序号都是相同的。那么它们有什么区别呢?为什么不能归并成一个?答案是:不同的行是为 具有不同参数个数的回调方法准备的。对窗口过程来说,它固定只有 4 个参数,因此只用到了第 5 行、也就是索引号为 4 的那一个。你可以看到 fnx_array 数组的定义最多允许方法有 12 个参数。其 实,接受更多参数的方法也是可以实现的。还记得 bind 方法包括一个 isArrayBased 参数吗?多个参 数完全可以通过打包成数组的方式传入,虽然 SWT 中似乎用不到这一点。(当然,在现实中你大概 也很少有机会接触参数比 12 个还要多的方法。) SWT 的这种消息实现机制是否是最好的呢?取决于你对“好”的定义,可能会得到不同的答案。对 于我来说,其实在开始研究 SWT 消息机制的时候,是处在一种“有热情没信心”的状态,不能确 定 SWT 是否使用了汇编一类的魔法,不然的话或许就没有这篇文章了。最终我发现 SWT 在这个问 题上采用的是一种相对比较中庸的态度:它也采用了一些非常规的技巧,然而整个处理过程仍然是 比较清晰的,没有一味追求效率而使用太过 Smart 的手段。尽管如此,在了解 SWT 的实现后,我 也开始思考几个问题: PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 1、SWT 用宏 MAX_CALLBACKS 将回调过程的界限固定为 128 个。这个数字够用吗?毕竟一个应 用程序中的窗口数目通常会远远超过 128 个。然而再深入考虑一下,我认识到这基本上不是问题。 事实上,不管窗口再多,窗口过程就是那么几个。例如,所有 Shell 窗口有着相同的类名—— SWT_Window0,因此也就共享同一个窗口过程。另外的 Windows 标准组件,如按钮、标签等等, 它们的窗口过程是由 Windows 自身来维护的,不需要占用数组里的空间。可见,实际上需要维护的 窗口过程数目是相当少的。 2、这种实现方法在效率上有没有问题?在上面的代码中可以明显的看到,通过 JNI 调用 Java 需要 许多步骤,尽管这些步骤是必须的,但是这样做无疑要被本地调用来得慢。我没有测试数据,因此 不敢说这种差距到底有多大;不过从纯粹使用者的角度讲,我的经验是 SWT 应用程序的运行速度 基本上和本地应用程序没有什么差别,区别在于 SWT 程序启动的时候通常要有好几秒钟的等待(虚 拟机的通病,不管 Java 还是.Net 程序都有类似症状)。当然,这也还是在可接受范围之内的。 本文的内容到此应当算告一段落了。然而,为了完整性起见,我还是把“万里长征之后的故事”— —也就是消息到达 windowProc 以后如何进一步分派的过程列出来,如果你想要定制消息处理过程 的话,或许可以从这里得到一些提示。Display.windowProc 实现如下: int windowProc (int hwnd, int msg, int wParam, int lParam) { if (msg == OS.WM_NCHITTEST) { if (hitCount++ >= 1024) { try {Thread.sleep (1);} catch (Throwable t) {} } } else { hitCount = 0; } int index; if (USE_PROPERTY) { index = OS.GetProp (hwnd, SWT_OBJECT_INDEX) - 1; } else { index = OS.GetWindowLong (hwnd, OS.GWL_USERDATA) - 1; } if (0 <= index && index < controlTable.length) { Control control = controlTable [index]; if (control != null) { return control.windowProc (hwnd, msg, wParam, lParam); } } return OS.DefWindowProc (hwnd, msg, wParam, lParam); } 仍然忽略一些无关宏旨的代码,读者会注意到这里一些有趣的地方。USE_PROPERTY 这一段代码 表明了 SWT 将窗口句柄和 Shell 关联起来的策略,也就是利用了窗口所提供的额外字节或者 Prop PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn 第 20 页 信息。(需要说明的是,微软的文档中将此方法视为一种不好的编程风格,因为 UserData 字段通常 认为是给客户程序员使用的,而不应当由框架来占用。不过,既然 SWT 将窗口隐藏在 Shell 对象的 背后,并没有开放设置 UserData 的接口给一般的 Java 程序员使用——除非通过上面那样直接调用 OS 类——那么这样做倒也没有什么问题。) 接下来看 control.windowProc 是如何实现的。这个代码相当长——不过只是消息映射而已,所以我 只列出最前面几个。其意义一看便知。 int windowProc (int hwnd, int msg, int wParam, int lParam) { LRESULT result = null; switch (msg) { case OS.WM_ACTIVATE: result = WM_ACTIVATE (wParam, lParam); break; case OS.WM_CHAR: result = WM_CHAR (wParam, lParam); break; ... if (result != null) return result.value; return callWindowProc (hwnd, msg, wParam, lParam); } 所有的 WM_xxx 方法都是默认访问权限(也就是 package friendly)的。因此,只有和 Control 类位 于同一个包中的类才能获得重载这些方法的权限,你自己的类则是不行的。这无疑会给希望深入底 层的程序员(比如我)带来一些障碍。很明显,Eclipse 开发者并不希望你这么做,也没有提供给你 对底层太多的控制权。他们希望你只使用他们所提供的 addXXXListener 接口来处理消息。不 过 要 绕 开这个麻烦(不修改源代码的话),办法也不是没有——只要把自己的类声明在org.eclipse.swt.widgets 包中就行了(因为 SWT 包并没有被密封)。当然了,这是一个后门,你应当在没有其他办法的情况 下才考虑这么做。我不是一个纯粹的 OO 主义者,所以我并不介意在常规方法不能解决问题的时候 走走后门。事实上,为了突破 SWT 不提供自绘按钮的限制,我自己就曾使用过这个后门。如果你 对这个话题感兴趣的话,可以看看我最初发在中文 Eclipse 社区上的一个帖子 (http://www.eclipseworld.org/bbs/read.php?tid=5107)。当然,这都是题外话了。 对坚持看到最后的读者说声谢谢,辛苦你看这么又长又臭的文章。如果对本文有问题的话可以通过 以下两个邮箱任何一个与我联系(后面一个也是 MSN 号,不过我在线的时候并不多,呵呵。) hao.yu@yeah.net yuhao_shenzhen@hotmail.com 最后祝编程愉快。 PDF 文件使用 "pdfFactory Pro" 试用版本创建 www.fineprint.cn
还剩19页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

lwc0715

贡献于2010-10-14

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf