第三节 剖析SWT的设计原则
在第一章我们已经介绍过,SWT使用底层操作系统提供的本地控件库,它仅仅是程序与底层系统交互的Java接口。本地控件的生命期(lifecycle)就像是Java控件对象的一个镜中像:创建Java控件的时候,本地控件同时被创建;销毁Java控件的时候,本地控件也被销毁了。这种设计避免了一种情况的出现,就是底层控件还没有创建的时候调用代码控件的方法(method)。这种情况在其他的工具包(toolkit)中是存在的,使得本地控件与代码控件的生命周期不一致[2]。
举个例子,看看MFC(Microsoft Foundation Classes)的两步式创建过程。要创建一个按钮,代码如下:
CButton button; // 在栈上创建C++对象
button.Create(<parameters>); // 创建窗口对象
假如在C++对象和(构建于本地窗口控件之上的)本地窗口控件的创建代码之间插入代码,例如:
CButton button; //在栈上创建C++对象
CString str = _T("Hi"); //创建一个 CString ,记录按钮的文本
button.SetWindowText(str); // 设置按钮的文本—出问题了
button.Create(<parameters>); //创建窗口对象
代码在编译的时候不会报错,但运行时与开发时不同。调试时,会触发断言(assertion),而release版本的行为是未定义的。
控件的容器[3]
多数GUI要求,在创建一个控件之前要指出它的容器;在控件的生命期中,它是属于那个容器的[4]。容器的生命周期决定了控件的生命周期。另外,许多本地控件有详细的特性,也就是样式(style),这个必须在创建的时候设定。例如,一个按钮(button)可以是一个点击按钮(push button),也可以是一个复选框(checkbox)。因为构建SWT控件时要创建它对应的本地控件,所以它必须把这个信息传递给它的构造函数(constructor)。SWT控件通常需要两个参数:容器和样式(a parent and a style)。容器是类org.eclipse.swt.widgets.Widget或它的子类;可选的类型是在SWT类中预先定义好的整数常量(integer constant)。可以传递单个样式,也可以用位或(OR)合并多种样式。在本书中,我们讲解一个控件的时候,会同时介绍它的样式。
销毁控件
Swing的开发者把本节的内容作为SWT低等的一个证据,并以此嘲笑。通常,Java的开发者在这里确实会觉得不适应甚至讨厌,因为本节的内容是:编程者要自己做清除工作。这个概念,受到Java的开发者嫌弃,因为它抛开了Java垃圾回收机制,而倒退到很久以前那种要开发者负责的情况。
为什么要我销毁对象?Java的垃圾收集机制极好地管理着内存,但是GUI资源管理运作于重重约束之下。可用的GUI资源数目是非常受限的,而且,在很多平台上,是整个系统的限制。由于SWT直接与本地底层图形资源一起工作,每个SWT资源需要一个GUI资源,所以及时地释放资源不仅可以提升当前SWT程序的性能,也会提示正在运行的其它GUI程序的性能。Java的垃圾收集机制没有时间保障,因此会造成图形资源的不良管理。正因为如此,编程者必须自己负起责任。
这个任务有多重?事实上,一点都不重。在SWT的一系列文章中,Carolyn Macleod 和Steve Northover描述了两条简单的规则[5]:
- 谁创建,谁销毁
- 父已亡,子亦亡[6]
规则1:谁创建,谁销毁
在本章的开头,我们已经知道SWT对象创建的时候,本地资源也会被创建。换句话说,调用SWT对象的构造函数的同时,底层的本地资源被创建。假设您编写了下面的代码,您已经构建了一个SWT的Color对象,也在GUI底层平台分配了一个color资源:
Color color = new Color(display, 255, 0, 0); //创建红色
根据规则1,您创建了它,所以您使用完后必须销毁它,如下:
color.dispose(); //我创建,我销毁
然而,如果您不是调用构造函数获得一个资源,您不需销毁这个资源。例如,考虑下面的代码:
Color color = display.getSystemColor(SWT.COLOR_RED); // 取到红色
再次地,您有了一个Color对象,它容纳了底层平台的红色资源,但不是您申请(allocate)的。根据规则1,您不该销毁它。为什么不能?它不属于您——您是借过来的,其它的对象(object)可能正在使用它或将要用到。销毁这样一个资源会代理惨重的损失。
规则2:父已亡,子亦亡
对每一个用new创建出来的SWT对象,都要调用dispose()将很快变得枯燥,并可能使SWT处于边缘地位。所幸,SWT的设计者意识到了这一点,他们创建了一个自动销毁的逻辑级连[7]。一个容器被销毁的时候,它的所有控件也会被销毁。这意味着一个Shell被销毁的时候,所有属于它的控件也被自动销毁了。您将看到,在“hello, World”程序中,尽管用构造函数创建了一个Label对象,但从来不会调用label.dispose()。当用户关闭Shell的时候,Label对象自动地被销毁了。
您可能觉得您从不需要调用dispose(),本节纯属浪费空间。确实,可能在您写的很多程序中,资源都有容器,它们会自动的被销毁。那么,考虑一下这种情况:您想改变Text控件的字体。您的代码可能如下:
Text text = new Text(shell, SWT.BORDER); // 创建text
Font font = new Font(display, "Arial", 14, SWT.BOLD); // 创建字体
text.setFont(font); // 设置字体
您创建的字体(Font)对象没有容器,所以不会自动销毁,甚至是Shell关闭以及使用它的Text对象销毁的时候。您可能觉得要自己销毁font是个负担,但要认识到text与销毁font没有什么关系——它并不拥有font。事实上,您可能把一个Font对象应用于多个控件;自动销毁会带来严重的后果。
忽略已销毁的对象
机敏的读者可能已经注意到本章讨论的镜像生命期中有个间隙。这些情况下会怎样:封装了本地控件的Java对象仍然在有效范围内(is still in scope),但它属于的Shell对象已被销毁了;或者一个控件的销毁函数已被人为的调用了——本地控件被销毁了?底层的本地控件不存在的情况下,不能调用这个Java对象的函数?
确实是这样。如果调用一个本地控件已被销毁的控件的函数,会惹不少的麻烦。一旦一个控件被销毁了,即使它仍在有效范围内,不该对它做任何事情。是的,这个Java对象是可用的,但底层的对等体已被销毁了。如果试图对一个已被销毁的控件做什么,会得到一个SWTException,内容是“控件已被销毁[8]”。看看清单3-2的代码。
import org.eclipse.swt.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;
public class Broken
{
public static void main(String[] args)
{
Display display = new Display();
Shell shell = new Shell(display);
shell.setLayout(new RowLayout());
Text text = new Text(shell, SWT.BORDER);
shell.open();
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
{
display.sleep();
}
}
System.out.println(text.getText()); // 出错!
display.dispose();
}
}
清单3-2
代码能编译和运行,但在主窗口关闭之后,会在控制台打印如下的stack trace:
org.eclipse.swt.SWTException: Widget is disposed
at org.eclipse.swt.SWT.error(SWT.java:2332)
at org.eclipse.swt.SWT.error(SWT.java:2262)
at org.eclipse.swt.widgets.Widget.error(Widget.java:385)
at org.eclipse.swt.widgets.Control.getDisplay(Control.java:735)
at org.eclipse.swt.widgets.Widget.isValidThread(Widget.java:593)
at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:315)
at org.eclipse.swt.widgets.Text.getText(Text.java:705)
at Broken.main(Version.java:24)
再罗嗦几句。如果在Windows XP上运行这个程序,会跳出一个对话框,说“javaw.exe遇到一个问题,需要关闭”,还要问您“是否发送错误报告给Microsoft?”
本小节的内容是很简单的:一旦一个对象被销毁,不管是显式调用了dispose()函数,还是它的容器被销毁了,就不要再理它(leave it alone)。
[2] 译注:原文This design avoids issues with calling methods on a code object when the underlying widget hasn't yet been created, which can occur in other toolkits that don't match the lifecycles of the code widget and the native widget.
[3] Parent,译者觉得翻译为“容器”更形象。
[4] 译注:原文Most GUIs require you to specify a parent for a widget before creating that widget, and the widget "belongs" to its parent throughout its lifecycle.
[5] 原注:Carolyn MacLeod and Steve Northover, SWT: The Standard Widget Toolkit—Part 2: Managing Operating System Resources, www.eclipse.org/articles/swt-design-2/swt-design-2.html.
[6] 译注:原文If you created it, you dispose it. Disposing the parent disposes the children.
[7] 译注:原文a logical cascade of automatic disposal
[8] 译注:原文Widget has been disposed