SwingWorker

本文示例代码请从 这儿 下载
 
               正确理解和使用Swing线程模型编程是编写响应灵活的Swing程序的关键。从Java SE 6开始引进的SwingWorker能帮你轻松的编写多线程Swing程序,改善你Swing程序的结构,提高界面响应的灵活性。 SDN(Sun developer Network)上有一篇很好的文章: Improve Application Performance With SwingWorker in Java SE 6详细演示了如何使用SwingWorker改善Swing应用程序。把它翻译过来同大家共享。
摘要
            桌面应用程序员常见的错误是误用Swing事件调度线程(Event Dispatch Thread, EDT)。他们要么从非UI线程访问UI组件,要么不考虑事件执行顺序,要么不使用独立任务线程而在EDT线程上执行耗时任务,结果使编写的应用程序变得响应迟钝、速度很慢。耗时计算和输入/输出(IO)密集型任务不应放在Swing EDT上运行。发现这种问题的代码并不容易,但Java SE 6提供了javax.swing.SwingWorker类,使修正这种代码变得更容易。
               本文演示了一个使用SwingWorker类创建和管理任务线程的例子,描述了如何避免编写运行缓慢、感觉迟钝、容易失去响应的用户界面。这个演示例子叫Image Search,它展示了如何使用SwingWorker API来和网站 Flickr进行交互、搜索并下载图像。
               如果需要理解Swing Ui的基本概念,包括事件处理和侦听等UI编程,可以参照前面的文章,或从Sun官方网站下载阅读Java教程的 Swing部分。
演示程序介绍
               Image Search执行的耗时任务是访问Flickr网站服务,该任务不应该在EDT上执行。Image Search程序搜索Flickr站点,搜索匹配用户输入的查询条件的图像,下载匹配图形的缩略图。当用户从缩略图列表中选择某缩略图时,它将下载该图的原始图片。该演示程序使用SwingWorker类作为任务线程,从而避免了在EDT上执行这些耗时任务。
               当用户输入查询条件时,程序在Flickr网站请求一个图像查询。如果有符合查询条件的图像,程序下载上限为100个的缩略图像。可以修改程序改变下载图像的数目。搜索和下载图像的同时,有一进度条显示搜索进度。图1显示了查询字段和进度条:
使用SwingWorker之一
图1,搜索图像并显示下载进度
               每当程序成功下载一个缩略图片后,就添加到一个JList组件中,图片从Flickr站点到达后就被添加列表中。程序使用SwingWorker的一个实例,程序能在每个图片到达时添加到列表,而不用等待所有的图片都到达。图2显示列表中的图片:
SwingWorker_第1张图片
图2匹配缩略图的列表
               当从列表选择一个图片,程序将下载该图片的原始图片,并显示在列表的下面。当大图片下载时,另一进度条将显示下载进度。图3显示列表和图片下载进度条。
SwingWorker_第2张图片
图3选中缩略图下载大图片
               最后,当所有图片数据下载完毕后,程序在列表下方显示图片。
               程序使用SwingWorker来完成所有图片搜索和下载任务。另外,程序还演示了如何取消任务,如何在任务完成之前获得即时结果。该程序有两个SwingWorker的子类:ImageSearcher和ImageRetriever。ImageSearcher类负责搜索和获取图片列表中的缩略图,ImageRetriever类负责用户从列表选择时下载原始版本的图片。本文用这个类来描述SwingWorker类的主要功能。图4显示程序的整个外观。
SwingWorker_第3张图片
图4使用SwingWorkere类创建响应灵活程序界面
回顾Swing线程基础
               一个Swing程序中一般有下面三种类型的线程:
       * 初始化线程(Initial Thread)
    * UI事件调度线程(EDT)
    * 任务线程(Worker Thread)
        每个程序必须有一个main方法,这是程序的入口。该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了。
        Swing程序只有一个用EDT,该线程负责GUI组件的绘制和更新,通过调用程序的事件处理器来响应用户交互。所有事件处理都是在EDT上进行的,程序同UI组件和其基本数据模型的交互只允许在EDT上进行,所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。
        Swing编程时应该注意以下两点:
1.从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误。
2.在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理。
3.应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大树据量的文件。
        总之,任何干扰或延迟UI事件的处理只应该出现在独立任务线程中;在初始化线程或任务线程同Swing组件或其缺省数据模型进行的交互都是非线程安全性操作。

               SwingWorker类帮你管理任务线程和Swing EDT之间的交互,尽管SwingWorker不能解决并发线程中遇到的所有问题,但的确有助于分离Swing EDT和任务线程,使它们各负其责:对于EDT来说,就是绘制和更新界面,并响应用户输入;对于任务线程来说,就是执行和界面无直接关系的耗时任务和I/O密集型操作。
使用合适线程
               初始化线程运行程序的main方法,该方法能处理许多任务。但在典型的Swing程序中,其主要任务就是创建和运行应用程序的界面。创建UI的点,也就是程序开始将控制权转交给UI时的点,往往是同EDT交互出现问题的第一个地方。
               Image Search示例的主类是MainFrame,从其main方法启动。许多程序使用下面方法启动界面,但 这是错误的启动UI界面的方法:
public class MainFrame extends javax.swing.JFrame {
  ...
  public static void main(String[] args) {
    new MainFrame().setVisible(true);
  }
}

               尽管这种错误出现在开始,但仍然违反了不应在EDT外的其他线程同Swing组件交互的原则。这个错误尤其容易犯,线程同步问题虽然不是马上显示出来,但是还要注意避免这样书写。
                正确启动UI界面应该如下:
public class MainFrame extends javax.swing.JFrame {
  ...
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new MainFrame().setVisible(true);
      }
    });
  }
}

               使用NetBeans IDE的开发者应该对这段代码很熟悉,NetBeans通常会自动生成这段代码。这段启动代码虽然和SwingWorker没有直接关系,但是这个编程范式很重要。SwingUtilities类包含一些静态方法帮你同UI组件交互,其中invokeLater方法意思是在EDT上执行其Runnable任务。Runnable接口定义了可作为独立线程执行的任务。
               在初始化线程中使用invokeLater方法能正确的初始化程序界面。就像前面文章所提到的,此方法是异步执行的,也就是说调用会立即返回。创建界面后,大部分初始化线程基本上就结束了。
               通常有两种办法调用此方法:
       * SwingUtilities.invokeLater
       * EventQueue.invokeLater
               两个方法都是正确的,选择任何一个都可以。实际上,SwingUtilities版只是一个薄薄的封装方法,它直接转而调用EventQueue.invokeLater。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。
               另种将任务放到EDT执行的方法是SwingUtilities.invokeAndWait,不像invokeLater,invokeAndWait方法是阻塞执行的,它在EDT上执行Runnnable任务,直到任务执行完了,该方法才返回调用线程。
               invokeLater和invokeAndWait都在事件派发队列中的所有事件都处理完之后才执行它们的Runnable任务,也就是说,这两个方法将Runnable任务放在事件队列的末尾。
               注意:虽然可以在其他线程上调用invokeLater,也可以在EDT上调用invokeLater,但是 千万不要在EDT线程上调用invokeAndWait方法!很容易理解,这样做会造成线程竞争,程序就会陷入死锁。
将EDT线程仅用于GUI任务
               Swing框架负责管理组件绘制、更新以及EDT上的线程处理。可以想象,该线程的事件队列很繁忙,几乎每一次GUI交互和事件都是通过它完成。事件队列的上任务必须非常快,否则就会阻塞其他任务的执行,使队列里阻塞了很多等待执行的事件,造成界面响应不灵活,让用户感觉到界面响应速度很慢,使他们失去兴趣。理想情况下,任何需时超过30到100毫秒的任务不应放在EDT上执行,否则用户就会觉察到输入和界面响应之间的延迟。
               幸运的是,不会仅仅因为有复杂的任务、计算或输入输出密集任务需要作为GUI事件处理任务执行,Swing的性能就要有所降低。毕竟有许多桌面程序执行耗时任务,比如处理电子表格公式、跨越网络查询数据库、通过Internet向其他程序发送信息。即使有这些任务,界面仍然可以让用户感觉到响应灵活、快捷。编写响应灵活的程序需要创建和管理独立于EDT的线程。
               在Image Search程序中,有两个事件如果完全在EDT上处理就会降低界面的响应速度:图像搜索处理和选中图片下载处理。
               两个事件处理都要访问Web服务,这些服务通常要许多秒后才能响应,在此期间,如果程序在EDT上进行Web服务交互,用户就不能取消搜索或者同界面交互,像这两种都不应该在EDT上运行。
               图5显示了在A和B点之间,EDT不能处理UI事件,AB两点之间代表了程序访问Flickr网站Web服务的IO操作时间:
SwingWorker_第4张图片
图5. 在执行Web服务期间EDT不能响应UI事件
               javax.swing.SwingWorker类是Java SE 6中新出现的类,使用SwingWorker,程序能启动一个任务线程来异步查询,并马上返回EDT线程。图6显示了使用SwingWorker后,事件处理立即返回,允许EDT继续执行后续的UI事件。
SwingWorker_第5张图片
图6.使用任务线程,程序能够在避免在EDT上执行I/O密集型任务

SwingWorker基础

    本节简要介绍SwingWorker的功能。SwingWorker的定义如下:

public abstract class SwingWorker<T,V> extends Object implements RunnableFuture

       SwingWorker是抽象类,因此必须继承它才能执行所需的特定任务。注意该类有两个类型参数:T及V。T是doInBackground和get方法的返回类型,V是publish和process方法要处理的数据类型。后文将作详细解释。

    该类实现了java.util.concurrent.RunnableFuture接口。RunnableFuture接口是Runnable和Future两个接口的简单封装。由于SwingWorker实现了Runnable接口,因此SwingWorker有一个run方法。Runnable对象一般作为线程的一部分执行,当Thread对象启动时,它激活Runnable对象的run方法。由于SwingWorker实现了Future接口,因此SwingWorker产生类型为T的结果值并提供同线程交互的方法。SwingWorker实现以下接口方法:

    * boolean cancel(boolean mayInterruptIfRunning)
    * T get()
    * T get(long timeout, TimeUnit unit)
    * boolean isCancelled()
    * boolean isDone()

    SwingWorker实现了所有的接口方法,实际上你仅需要实现以下SwingWorker的抽象方法:

protected T doInBackground() throws Exception

    doInBackground方法作为任务线程的一部分执行,它负责完成线程的基本任务,并以返回值来作为线程的执行结果。继承类须覆盖该方法并确保包含或代理任务线程的基本任务。不要直接调用该方法,应使用任务对象的execute方法来调度执行。

    在获得执行结果后应使用SwingWorker的get方法获取doInBackground方法的结果。可以在EDT上调用get方法,但该方法将一直处于阻塞状态,直到任务线程完成。最好只有在知道结果时才调用get方法,这样用户便不用等待。为防止阻塞,可以使用isDone方法来检验doInBackground是否完成。另外调用方法get(long timeout, TimeUnit unit)将会一直阻塞直到任务线程结束或超时。获取任务结果的最好地方是在done方法内:

protected void done() 

    在doInBackground方法完成之后,SwingWorker调用done方法。如果任务需要在完成后使用线程结果更新GUI组件或者做些清理工作,可覆盖done方法来完成它们。这儿是调用get方法的最好地方,因为此时已知道线程任务完成了,SwingWorker在EDT上激活done方法,因此可以在此方法内安全地和任何GUI组件交互。

    没必要等到线程完成就可以获得中间结果。中间结果是任务线程在产生最后结果之前就能产生的数据。当任务线程执行时,它可以发布类型为V的中间结果,覆盖process方法来处理中间结果。后文还将提供这些方法的更多详细信息。当属性改变时,SwingWorker实例能通知处理器,SwingWorker有两个重要的属性:状态和进程。任务线程有几种状态,以下面SwingWorker.StateValue枚举值来表示:

    * PENDING
    * STARTED
    * DONE

    任务线程一创建就处于PENDING状态,当doInBackground方法开始时,任务线程就进入STARTED状态,当doInBackground方法完成后,任务线程就处于DONE状态,随着线程进入各个阶段,SwingWorker超类自动设置这些状态值。你可以添加处理器,当这些属性发生变化来接收通知。

    最后,任务对象有一个进度属性,随着任务进展时,可以将这个属性从0更新到100标识任务进度,当该属性发生变化时,任务通知处理器进行处理。

实现简单的ImageRetriever

    当点击列表所略图时,事件处理器创建了一个ImageRetriever实例并执行之。ImageRetriever下载选中的图片并在列表下面展示它。当实现SwingWorker子类,须指定doInBackground和get方法返回值的类型。因为ImageRetriever并不生成中间结果,它使用特殊类型Void作为中间类型,ImageRetriever的任务的结果是一图片,因此使用Icon类型作为doInBackground和get方法的返回类型,下面代码显示了ImageRetriever的大部分实现:
public class ImageRetriever extends SwingWorker<Icon, Void> {
       private ImageRetriever() {}     
       public ImageRetriever(JLabel lblImage, String strImageUrl) {
               this.strImageUrl = strImageUrl;
               this.lblImage = lblImage;
       }     
       @Override
       protected Icon doInBackground() throws Exception {
               Icon icon = retrieveImage(strImageUrl);
               return icon;
       }     
       private Icon retrieveImage(String strImageUrl)
                       throws MalformedURLException, IOException {           
               InputStream is = null;
               URL imgUrl = null;
               imgUrl = new URL(strImageUrl);
               is = imgUrl.openStream();
               ImageInputStream iis = ImageIO.createImageInputStream(is);
               Iterator<ImageReader> it =
                       ImageIO.getImageReadersBySuffix("jpg");             
               ImageReader reader = it.next();
               reader.setInput(iis);               ...
               Image image = reader.read(0);
               Icon icon = new ImageIcon(image);
               return icon;
       }     
       @Override
       protected void done() {
               Icon icon = null;
               String text = null;
               try {
                       icon = get();
               } catch (Exception ignore) {
                       ignore.printStackTrace();
                       text = "Image unavailable";
               }
               lblImage.setIcon(icon);
               lblImage.setText(text);
       }     
       private String strImageUrl;
       private JLabel lblImage;
}

    因为ImageRetriever类下载图象并把它以label图标的方式展现,因此为了方便在其构造函数中要提供一个JLabel实例和图象URL。ImageRetriever需要图象URL来下载图象,需要一个JLabel实例来展现下载的图象,如果使用内部类实现任务线程ImageRetriever,由于可以直接访问这些信息,你甚至不需要在构造函数中提供这些信息(图象URL以及展现图象的JLabel实例)。这样做这些信息不会在ImageRetriever实例之间共享,所以更容易帮助程序实现线程安全。

    注意ImageRetriever指定Icon作为doInBackground和get方法的返回类型,因为并不产生任何中间数据,所以指定Void类型作为中间结果类型。

public class ImageRetriever extends SwingWorker<Icon, Void>

    在该实现中,doInBackground方法必须遵循类协议,返回一个Icon类型的对象。通过在类定义指定Icon类型,编译器强制doInBackground和get方法要返回Icon类型的值。不要覆盖get方法,因为该方法在SwingWorker中是一个final方法。

    doInBackground方法从类构造函数中提供的URL中获得图象并产生一个Icon结果:
@Override
protected Icon doInBackground() throws Exception {
   Icon icon = retrieveImage(strImageUrl);
   return icon;
}

    当doInBackground方法完成后,SwingWorker在EDT上调用done方法,不要直接调用这个方法,因为SwingWorker超类会调用这个方法。done方法获得Icon结果并把它放到标签label上。在本例中,lblImage引用通过ImageRetriever构造函数传入进来。

@Override
protected void done() {
  ...
  icon = get();
  ...
  lblImage.setIcon(icon);
  ...
}

    progress属性值范围是从0到100,当你在任务实例内处理这些信息时,你可以调用setProgress方法来更新这个属性。当ImageRetriever通过ImageIO API下载图象时,它调用setProgress方法来更新其progress属性。下面的代码展示如何使用IIOReadProgressListener实例来跟踪ImageReader对象任务、下载图象并更新progress属性:

reader.addIIOReadProgressListener(new IIOReadProgressListener() { ... public void imageProgress(ImageReader source, float percentageDone) { setProgress((int) percentageDone); } public void imageComplete(ImageReader source) { setProgress(100); } });

    当任务属性发生变化时,它通知处理器对象。在前面例子代码中,当ImageRetriever类从ImageIO API那儿收到更新信息时调用setProgress方法并使用这些信息来设置自己的进度属性,作为结果,属性变化处理器知道了当前下载了多少图象数据。

    图7显示ImageRetriever下载图象完成后的结果:

SwingWorker_第6张图片

图7.SwingWorker线程在任务完成后更新进度条和标签图标

    ImageRetriever演示了一个SwingWorker的简单实现,在简单实现中,只需覆盖方法doInBackground就行。然而,因为只有doInBackground方法完成之后,任务最后的结果才能获得,所以你也应该覆盖done方法。

    SwingWorker在doInBackground方法完成之后激活done方法,所以应该从done方法中获得任务结果,并且应该使用get方法获得任务结果。通过在ImageRetriever构造函数提供这些UI组件,保证了能在任务线程的done方法中直接更新这些组件。前面的例子显示了如何使用从Flickr站点获取的图象设置标签label的图标。参考ImageRetriever的完整实现了解如何从Flickr网站获得图像。

使用简单ImageRetriever

    现在已经创建了简单的SwingWorker实现ImageRetriever,如何使用它呢?首先,要实例化,接着调用它的execute方法。当用户从JList中选择一个缩略图,演示应用程序的MainFrame类就使用ImageRetriever任务线程。当用户点击一个缩略图,列表选择发生变化,产生一个列表选择事件,listImageValueChanged方法是事件处理器。listImageValueChanged方法获得选中的列表项,并生成相应缩略图片对应的图象的Flikr服务URL字符串,事件处理器接着调用retrieveImage方法,retrieveImage方法包含生成和使用ImageRetriever任务线程的重要代码。

private void listImagesValueChanged(ListSelectionEvent evt) { ... ImageInfo info = (ImageInfo) listImages.getSelectedValue(); String id = info.getId(); String server = info.getServer(); String secret = info.getSecret(); // No need to search an invalid thumbnail image if (id == null || server == null || secret == null) { return; } String strImageUrl = String.format(IMAGE_URL_FORMAT, server, id, secret); retrieveImage(strImageUrl); ... } private void retrieveImage(String imageUrl) { // SwingWorker objects can't be reused, so // create a new one as needed. ImageRetriever imgRetriever = new ImageRetriever(lblImage, imageUrl); progressSelectedImage.setValue(0); // Listen for changes in the "progress" property. // You can reuse the listener even though the worker thread // will be a new SwingWorker. imgRetriever.addPropertyChangeListener(listenerSelectedImage); progressSelectedImage.setIndeterminate(true); // Tell the worker thread to begin with this asynchronous method. imgRetriever.execute(); // This event thread continues immediately here without blocking. }

    注意每次下载新的图像,retrieveImage方法都生成新的ImageRetriever实例。SwingWorker实例不可复用,每次执行任务必须生成新的实例。

    正确使用SwingWorker的方法是实例化,如果需要获知线程发生的状态变化通知,则要添加属性变化处理器,最后执行。execute方法是异步执行,它立即返回到调用者。在execute方法执行后,EDT立即继续执行。图6显示这两条线程之间的交互。

    前面所讲的retrieveImage方法生成一个ImageRetriever,并向该实例提供一个JLabel引用,任务线程使用它显示图象,retrieveImage方法和EDT不需要再访问任务线程。这些代码生成线程、执行、并根据需要继续处理UI事件。

    注意retrieveImage方法在执行线程之前往ImageRetriever实例添加属性改变处理器,MainFrame类包含进度条跟踪当前图像下载的状态,事件处理器会接收到所有SwingWorker线程事件的通知,其中一个事件是进度事件。

    下面的事件处理器类响应进度事件,更新进度条。本文程序有两个进度条,一个跟踪搜索以及下载缩略图进度,另一个跟踪下载大图片的进度。程序使用相同的ProgressListener类,不同实例,来跟踪任务进度,因此需要在ProgressListener构造函数中传要更新的进度条,下面是ProgressListener的代码:

/** * ProgressListener listens to "progress" property * changes in the SwingWorkers that search and load * images. */ class ProgressListener implements PropertyChangeListener { // Prevent creation without providing a progress bar. private ProgressListener() {} ProgressListener(JProgressBar progressBar) { this.progressBar = progressBar; this.progressBar.setValue(0); } public void propertyChange(PropertyChangeEvent evt) { String strPropertyName = evt.getPropertyName(); if ("progress".equals(strPropertyName)) { progressBar.setIndeterminate(false); int progress = (Integer)evt.getNewValue(); progressBar.setValue(progress); } } private JProgressBar progressBar; }

    ImageRetriever类的使用是简单的,给定一个图像URL,它负责下载该图像,向事件处理器提供进度信息来更新MainFrame类的进度条,你不需要知道关于创建和使用任务线程任何信息。只要一点努力,就可以从SwingWorker子类获取更多效果。下一节将显示如何实现并使用更复杂的任务子类。

实现ImageSearcher

    SwingWorker的子类可能既会生成最终结果也会产生中间结果,记住线程在doInBackground方法结束后才产生最后结果,但任务线程也可以产生和公布中间数据。比如当ImageSearcher类从Flickr Web服务中获取缩略图列表时,每当下载一个缩略图时,列表便应显示这个缩略图,没理由要等待所有匹配图像下载完毕才把结果放在列表中。

    实现SwingWorker子类时,在类声明处要指定最终和中间结果的类型,ImageSearcher搜索并下载匹配的缩略图。由于该类在任务结束时产生匹配图像的列表,所以该类使用List作为类的类型参数,为表明它中间发布的数据是匹配图片,它还使用ImageInfo作为类型参数,ImageSearcher的定义如下:
public class ImageSearcher 
extends SwingWorker<List<ImageInfo>, ImageInfo> {
   public ImageSearcher(DefaultListModel model, String key,
               String search, int page) {
       this.model = model;
       this.key = key;
       this.search = search;
       this.page = page;
   }
   ...
}

    这部分说明了几点:首先List<ImageInfo>类型参数说明任务结束时ImageSearcher的doInBackground和get方法返回一个ImageInfo对象列表;其次当类下载图像时它会发布一些ImageInfo对象,在它们可用后可以立即可以显示出来。因为类的构造函数参数之一是列表模型,因此任务线程会直接更新模型。正如后面看到的一样,它的确是直接更新列表模型;另外,任务线程需要一个Flickr API主键(由Flickr提供)和一个查询项。因为该web服务使用分页方式提供结果,还需要一个页码参数来决定选择哪些匹配的图集。为方便起见,该演示总是返回匹配页面的第一页。因为doInBackground方法是任何任务线程的重心,先来看以下ImageSearcher的实现:

@Override
protected List<ImageInfo> doInBackground() {
  ...
  Object strResults = null;
  InputStream is = null;
  URL url = null;
  List<ImageInfo> infoList = null;
  try {
    url = new URL(searchURL);
    is = url.openStream();
    infoList = parseImageInfo(is);
    retrieveAndProcessThumbnails(infoList);
  } catch(MalformedURLException mfe) {
    ...
  }
  return infoList;
}

    它从Web服务打开一个流,提供一个查询URL,parseImageInfo方法产生匹配图片的信息列表,解析由该web服务返回的一个XML文件。retrieveAndProcessThumbnails方法使用解析过的列表下载所有的缩略图。最终结果是一个完整的包含缩略图数据的ImageInfo对象列表。infoList对象同先前提到的类和方法定义类型相同,是List<ImageInfo>类型。

    该类同ImageRetriever类相似,因为它也需要更新进度条,并提供图像数据。本文不再详细叙述ImageSearcher的doInBackground、done、get和setProgress方法,因为它们基本上同前面类中的方法相似。但是,ImageSearcher类不仅仅下载单个图片,它还要下载匹配的前100个缩略图片,这儿是演示SwingWorker其他功能的好地方:publish和process方法。

    你可以使用publish方法来发布要处理的中间数据,当ImageSearcher线程下载缩略图时,它会随着下载而更新图片信息列表,还会发布每一批图像信息,以便UI能在图片数据到达时显示这些图片。如果SwingWorker子类发布了一些数据,那么也应该实现process方法来处理这些中间结果。任务对象的父类会在EDT线程上激活process方法,因此在此方法中程序可以安全的更新UI组件。

    下面代码显示了ImageSearcher是如何使用publish和process方法的:

private void retrieveAndProcessThumbnails(List<ImageInfo> infoList) {
  for (int x=0; x <infoList.size() && !isCancelled(); ++x) {           
    // http://static.flickr.com/{server-id}/{id}_{secret}_[mstb].jpg
    ImageInfo info = infoList.get(x);
    String strImageUrl = String.format("%s/%s/%s_%s_s.jpg",
    IMAGE_URL, info.getServer(), info.getId(), info.getSecret());
    Icon thumbNail = retrieveThumbNail(strImageUrl);
    info.setThumbnail(thumbNail);
    publish(info);
    setProgress(100 * (x+1)/infoList.size());
  }
}   
/**
 * Process is called as a result of this worker thread's calling the
 * publish method. This method runs on the event dispatch thread.
 *
 * As image thumbnails are retrieved, the worker adds them to the
 * list model.
 *
 */
@Override
protected void process(List<ImageInfo> infoList) {
  for(ImageInfo info: infoList) {
    if (isCancelled()) {
      break;
    }
    model.addElement(info);
  }     
}

    为在任务执行中而非任务结束时发布数据,要调用publish方法,并以参数的形式提供要发布的数据。当然像前面所说的那样,必须在类声明中指定中间数据的类型。在本例中这个类型是ImageInfo。前面所述retrieveAndProcessThumbnails方法显示了如何在线程下载缩略图时发布ImageInfo对象。

    当从任务线程调用publish方法时,SwingWorker类调度process方法。有意思的是process方法是在EDT上面执行,这意味着可以同Swing组件和其模型直接交互。process方法将ImageInfo对象的缩略图添加到列表模型中,这样图片就会立即显现在列表中。

    注意process方法的参数,它并没有使用单个ImagInfo对象,而是这种对象的一个列表。原因是publish方法能够以批模式来调用process方法,就是说,每个publish调用并不总是产生相应的process调用。如果可能,publish方法会收集对象并以对象的列表为参数调用process方法。实现process方法要以对象列表的方式处理,就像下面的代码:
@Override
protected void process(List<ImageInfo> infoList) {
   for(ImageInfo info: infoList) {
       ...
       model.addElement(info);
   }         
}

    如果想允许程序用户取消任务,实现代码要在SwingWorker子类中周期性地检查取消请求。调用isCancelled方法来检查是否有取消请求。ImageSearcher代码的许多地方都有isCancelled方法的调用,在循环迭代或者其他检查点调用这个方法确保线程能即时获得取消请求。线程周期性地检查这种请求并停止工作。比如ImageSearcher类在以下几个点检查取消请求:

    * doInBackground方法的子任务在获取每个缩略图之前
    * process方法中在更新GUI列表模型之前
    * done方法中在更新GUI列表模型最终结果之前

    doInBackground方法调用retrieveAndProcessThumbnails方法,该方法循环列表的图像数据并获取这些图像的缩略图。然而当任务线程正在执行循环时,用户可以启动新的查询。因此这儿也需要检查取消请求:
private void retrieveAndProcessThumbnails(List<ImageInfo> infoList) {
   for (int x=0; x<infoList.size(); ++x) {
       // Check whether this thread has been cancelled.
       // Stop all thumbnail retrieval.
       if (isCancelled()) {
           break;
       }
       ...
}
该类处理缩略图的同时就发布它们,其结果是在EDT上运行process方法。如果用户请求取消,或者启动新的搜索,可以通过在process方法内检查来避免这种情况的发生。
protected void process(List<ImageInfo> infoList) {
   for (ImageInfo info: infoList) {
       if (isCancelled()) {
           break;
       }
       model.addElement(info);
   }
}
最后,一旦任务线程完成,它还有一个机会更新GUI的模型,这就是在done方法中。因此在这儿也要检查取消请求:
@Override
protected void done() {
   ...
   if (isCancelled()) {
       return;
   }
   ...
   // Update the model.
}
ImageSearcher类是一个更为完整的SwingWorker的例子,它比ImageRetriever做的更多,ImageSearcher类往GUI上发布中间数据,并处理任务取消请求。两个类都在后台执行任务,并通过事件处理器跟踪进度。
使用ImageSearcher类
演示程序提供一个搜索输入栏。当用户输入图片查询条件时,MainFrame类创建一个ImageSearcher实例,输入一个查询条件并产生一个键盘事件,输入栏的键盘事件激活searchImage方法,该方法实例化一个ImageSearcher对象并执行之:
private void searchImages(String strSearchText, int page) {
   if (searcher != null && !searcher.isDone()) {
       // Cancel current search to begin a new one.
       // You want only one image search at a time.
       searcher.cancel(true);
       searcher = null;
   }
   ...
   // Provide the list model so that the ImageSearcher can publish
   // images to the list immediately as they are available.
   searcher = new ImageSearcher(listModel, API_KEY, strEncodedText, page);
   searcher.addPropertyChangeListene r(listenerMatchedImages);
   progressMatchedImages.setIndeterminate(true);
   // Start the search!
   searcher.execute();
   // This event thread continues immediately here without blocking.
}
     注意代码在ImageSearcher构造函数中提供一个listModel作为参数,这个模型允许任务线程能直接访问更新列表内容。也可以向任务对象添加一个属性改变处理器。上文添加了一个属性改变处理器来更新进度条。除此外还需要添加一个处理器以响应任务线程的状态变化,特别是要侦听DONE状态并使用前文提到的get方法获取任务结果。
一旦执行任务线程,就会搜索并下载缩略图。该程序向任务对象提供了一个列表模型,因此它会直接更新列表。另外,ImageSearcher类提供了中间数据,因此可以在下载图片的同时更新JList组件。其运行的直接效果就是改善了程序的性能。如下图8所示,搜索结果是列表中显示的小图片:


SwingWorker_第7张图片

图8. 缩略图是任务线程发布的中间数据

    你可以通过调用其cancel方法取消SwingWorker线程。用户可以在当前搜索正在进行时输入新的搜索条件并提交来取消当前图像搜索。搜索输入栏的事件处理器检查现有线程是否正在运行,如果正在运行则调用cancel来取消之:
private void searchImages(String strSearchText, int page) {
   if (searcher != null && !searcher.isDone()) {
       // Cancel current search to begin a new one.
       // You want only one image search at a time.
       searcher.cancel(true);
       searcher = null;
   }
   ...
}
当调用cancel方法时,代码产生新的任务实例,每一个新的搜索需要自己的任务实例。
总结
所有的GUI事件和交互都运行在EDT上,在EDT上运行耗时或者I/O密集型处理会导致界面变得缓慢失去响应,为改善这种状况应使用Java SE 6中提供的SwingWorker类将这些任务转移到任务线程中。
使用SwingWorker,你可以执行相同的任务而不会延迟EDT运行,会提高程序的性能。并且,任务线程可以安全同界面组件交互,因为有回调方法可以在EDT上运行,允许任务运行和完成时更新GUI组件。

你可能感兴趣的:(SwingWorker)