使用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.addPropertyChangeListener(listenerMatchedImages);
  progressMatchedImages.setIndeterminate(true);
  // Start the search!
  searcher.execute();
  // This event thread continues immediately here without blocking.
}
     注意代碼在ImageSearcher構造函數中提供一個listModel作爲參數,這個模型允許任務線程能直接訪問更新列表內容。也可以向任務對象添加一個屬性改變處理器。上文添加了一個屬性改變處理器來更新進度條。除此外還需要添加一個處理器以響應任務線程的狀態變化,特別是要偵聽DONE狀態並使用前文提到的get方法獲取任務結果。
一旦執行任務線程,就會搜索並下載縮略圖。該程序向任務對象提供了一個列表模型,因此它會直接更新列表。另外,ImageSearcher類提供了中間數據,因此可以在下載圖片的同時更新JList組件。其運行的直接效果就是改善了程序的性能。如下圖8所示,搜索結果是列表中顯示的小圖片:




圖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組件。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章