摘要
缓慢的 GUI 是 Java 最大的性能问题之一。这篇 Java 技巧说明如何创建构造速度较快的 GUI,这是通过只构建必要的部件实现的。
缓慢的图形用户界面 (GUI) 是人们对 Java 常发的牢骚。尽管构造快速的 GUI 可能既费时又费力,但如果使用我提供的这个类,您只须做少量的工作就可以加快 GUI 的速度。对于彻底的 GUI 改进而言,这只是第一步;但是,这个类是极好的一种解决方案,因为它很简单。有时这个类对性能的提高足以满足您的需要,您无须做任何额外的工作。
方法
这种方法很容易表述:直到必要时才构建 GUI 组件。从宏观上讲,这是所有 Java 程序的默认工作方式。毕竟,谁会在初始化时就构造程序的全部可能的框架和窗口呢?但是,我在本文提供的这个类允许您进行更精细、更惰性的构造。
我构建的用来支持 GUI 组件惰性构造的这个实用工具类基于三点经验。第一点,大多数 GUI 面板都很简单,并且无需做任何额外的工作就可以很快完成初始化。
将大量 GUI 组件堆积到屏幕上的面板是大多数缓慢初始化的原因。这些面板主要属于两种类型,即带选项卡的面板或滚动面板。
第二点,通常直到要查看 GUI 组件时才需要构造它。第三点,只有在下列方法之一被调用之后 GUI 组件才是可见的:
public void paint (Graphics)
public void paintComponents(Graphics)
public void paintAll (Graphics)
public void repaint ()
public void repaint (long)
public void repaint (int, int, int, int)
public void repaint (long, int, int, int, int)
public void update (Graphics)
创建实用工具面板类的思想是,将大多数 GUI 构建代码移出初始化块和构造函数。我们将这些代码放入一个名为 lazyConstructor() 的函数中。这个函数完成平常由 GUI 构造函数完成的大多数工作,但它本身并不是构造函数。这个实用工具面板类确保每当调用任何绘制或更新方法之前都调用一次 lazyConstructor 方法。
在选项卡面板内使用这个面板类可以使带选项卡的面板更快地完成构造,因为最初只需构造可见的那个选项卡面板。
代码
该类的代码如下所示:
import java.awt.*;
/**
* LazyPanel 是一个抽象基类,它提供将 Panel 对象的置入操作延迟到
* 实际查看该面板时再进行的功能。当使用 CardLayout 和选项卡面板
* 视图时这个类极为有用,因为它允许根据您的操作构造子视图,而不必
* 一开始就等待全部构造完成。
*
* 如果子类选择覆盖下面的任一个方法,则它们必须保证被覆盖的方法
* 首先调用父类的方法。这些方法是:
*
* public void paint (Graphics)
* public void paintComponents(Graphics)
* public void paintAll (Graphics)
* public void repaint ()
* public void repaint (long)
* public void repaint (int, int, int, int)
* public void repaint (long, int, int, int, int)
* public void update (Graphics)
*
* 其中的每个方法都确保首先构造面板,然后便将调用转发给父类。
*
* 如果您要使用这个类,请首先创建它的一个子类,然后尽可能地
* 将子类构造函数中的代码移到 lazyConstructor 方法中。下面是
* 使用 LazyPanel 的一个示例:
*
*
*
* import java.awt.*;
*
* class BusyPanel extends LazyPanel
* {
* public BusyPanel (int rows, int cols)
* {
* this.rows = rows;
* this.cols = cols;
* }
*
* protected void lazyConstructor()
* {
* setLayout (new GridLayout (rows, cols));
* for (int i = 0; i < rows * cols; ++i)
* {
* add (new Button (Integer.toString (i + startValue)));
* ++startValue;
* }
* }
*
* static private int startValue = 0;
*
* private int rows;
* private int cols;
* }
*
* 您可以这样使用它:
*
* import java.awt.*;
* import java.awt.event.*;
*
* class TestFrame extends Frame
* {
* public TestFrame ()
* {
* setLayout (new BorderLayout ());
* add (nextButton, "South");
*
* framePanel.setLayout (layout);
* for (int i = 0; i < 30; ++i)
* framePanel.add (new BusyPanel (8, 8), "");
* add (framePanel, "Center");
*
* nextButton.addActionListener (
* new ActionListener()
* {
* public void actionPerformed (ActionEvent event)
* {
* layout.next (framePanel);
* }
* }
* );
*
* setSize (400, 300);
* }
*
* private CardLayout layout = new CardLayout();
* private Button nextButton = new Button ("Next Panel");
* private Panel framePanel = new Panel();
*
* static public void main (String args[])
* {
* (new TestFrame()).show();
* }
* }
*
* 要查看使用 LazyPanel 的优越性,请试着将 lazyConstructor() 方法中
* 的代码移到 BusyPanel 的构造函数中,重新编译该类,然后回头再看本例。
* 启动时的额外延迟就是 LazyPanel 类要消除的问题。
*
* 这种处理对 swing 同样有效。只须对 LazyPanel 稍作修改,让它
* 扩展 JPanel(而不是 Panel)即可。
*/
public abstract class LazyPanel extends Panel
{
// 我只希望调用 lazyConstructor 一次。
private boolean lazyConstructorCalled = false;
// Swing 的某些版本在将组件添加到它们的容器之前
// 调用 paint() 方法。我们不希望在显示组件之前
// 调用 lazyConstructor。
private boolean isConstructorFinished = false;
/**
* 创建一个 LazyPanel。
*/
protected LazyPanel ()
{
isConstructorFinished = true;
}
public void paint (Graphics g)
{
callLazyConstructor();
super.paint (g);
}
public void paintAll(Graphics g)
{
callLazyConstructor();
super.paintAll (g);
}
public void paintComponents (Graphics g)
{
callLazyConstructor();
super.paintComponents (g);
}
public void repaint ()
{
callLazyConstructor();
super.repaint();
}
public void repaint (long l)
{
callLazyConstructor();
super.repaint (l);
}
public void repaint (int i1, int i2, int i3, int i4)
{
callLazyConstructor();
super.repaint (i1, i2, i3, i4);
}
public void repaint (long l, int i1, int i2, int i3, int i4)
{
callLazyConstructor();
super.repaint (l, i1, i2, i3, i4);
}
public void update (Graphics g)
{
callLazyConstructor();
super.update (g);
}
/**
* 强制调用在子类中实现的 lazyConstructor() 方法。
* 如果多次调用某个给定对象的这个方法,则除第一次
* 调用以外的任何其他调用不会执行任何操作。
*/
public synchronized final void callLazyConstructor()
{
// 以下代码的大致思想如下所示:
// 1) 检查该方法是否被成功调用过。
// 如果是,只返回而不执行任何操作。
//
// 2) 否则 ... 调用惰性构造函数。
// 3) 调用 validate 以使所添加的全部组件变为可见的。
// 4) 注意我们已在运行的程序。
if ((lazyConstructorCalled == false) && (getParent() != null))
{
lazyConstructor();
lazyConstructorCalled = true;
validate();
}
}
/**
* 任何子类都必须实现这个方法。本应该
* 出现在子类构造函数中的大多数组件创建
* 代码将出现在此处。请参阅上面的
* 示例。
*/
abstract protected void lazyConstructor ();
}
使用 LazyPanel
LazyPanel 类的使用很简单,但您应该注意几点。
别在 lazyConstructor 中发出异常
对于面板,我们很容易编写在发生错误时发出异常的构造函数。不幸的是,lazyConstructor只是一个普通方法,而不是构造函数。因为 lazyConstructor 是从 paint() 中被调用的,所以它不能发出除 RuntimeExceptions 和 Errors 以外的任何异常。因为除了 AWT 线程之外再没有任何别的调用程序捕获这些异常和错误,所以最好不要发出任何异常。如有必要,您可以将 lazyConstructor 方法的主体放在一个 try/catch 块中,然后在这个 catch 块中进行一些敏感的操作。
GUI 编制器不合作
用 Inprise 公司的 JBuilder 或 Symantec 公司的 Cafe 中的 GUI 编制器生成的代码不能很好地与 LazyPanel 类配合使用。GUI 编制器生成的代码使用初始化函数为面板构造大多数 GUI 组件。这样就体现不出 LazyPanel 的优点,因为没有将任何操作移到 lazyConstructor() 方法中。
一种方法是手动将初始化函数中的组件构造代码移到 lazyConstructor 方法中。不幸的是,这样一来 GUI 编制器将不能再修改面板以作进一步的增强。更好的一种方法是保留 GUI 编制器生成的面板不动,而将它放入一个 LazyPanel 中。这样 LazyPanel 就只包含一个组件 -- GUI 编制器构建的组件。
代码的最终执行速度并没有加快
从最终结果来看,这个方法实际上并没有使应用程序加速。如果您的 GUI 最初需要占用 1 MB 内存,创建全部组件要花 30 秒的时间,则使用这个方法之后仍然需要 1 MB 内存,完全构建它仍然要花 30 秒的时间。LazyPanel 将构建所需 GUI 组件的开销分摊在不同的时间段上,而不是预先就请求全部的内存和时间。但是,LazyPanel 并没有采取任何措施来减少开销。通常,用这个类来加速 GUI 就足够了。但是在另一些情况下,使用一两个 LazyPanel 只是重大性能优化的开始。
小结
通过只进行必需的最小初始化,带有许多组件的 Java GUI 可更快地完成初始化。 LazyPanel 类使您几乎不必进行额外的编程即可加快复杂 GUI 的初始化。对于提高复杂 GUI 的速度而言,这是个不错的起点,对于不太复杂的 GUI,通常只进行这一步就足够了。
作者简介
Mark Roulo 是 JavaWorld 上的 Java 技巧的技术协调员。他从 1989 年开始从事专业编程,并且在 Java 尚处于 alpha-3 版时就开始使用 Java。他是 KLA-Tencor 的全职员工,他参与开发了一种 500KLOC、分布式、并行、多计算机的图像处理(还有其他功能)应用程序,整个程序几乎都是用 Java 编写的。
参考资源
有关 CardLayout 的文档资料:
http://java.sun.com/products/jdk/1.2/docs/api/java/awt/CardLayout.html
关于带选项卡的面板的一篇教程:
http://java.sun.com/docs/books/tutorial/uiswing/components/tabbedpane.html