利用多線程提高程式效能(for Android)

來源:互聯網
上載者:User

要想搞出一個反應迅速的Android應用程式,一個很好的做法就是確保在主UI線程裡執行盡量少的代碼。任何有可能花費較長時間來執行的代碼如果在主UI線程執行,則會讓程式掛起無法響應使用者的操作,所以應該放到一個單獨的線程裡執行。典型的例子就是與網路通訊相關的操作了,因為通過網路收發資訊的快慢我們無法預測,有可能“biu”地一下就搞定了,也有可能磨磨唧唧半天。使用者心情好的話可能會容忍一點點遲延,而且前提是你給出了必要的提示,但是一個看上去根本不動貌似嗝兒屁的程式……(譯註:就好比Ajax技術出現之前的網頁,使用者可以習慣短時間的載入,但是一個載入了半天都是空白的瀏覽器視窗就常常讓那個撥號時代的我們感到困惑和抓狂。)
在這篇文章中,我們將建立一個簡單的圖片下載程式來示範一下多線程模式。我們將從網上下載一坨圖片,然後用這些圖片產生一個縮圖列表。建立一個非同步工作的任務,讓它在後台下載圖片,會讓我們的程式看上去更快。(譯註:這裡我加上“看上去”,因為我認為所謂多線程讓程式更快,更多的意義在於“提高對使用者操作的響應”。包括本文題目,所謂的“高效能”,主要指的還是避免UI的硬直(格鬥遊戲術語,請自行google)、掛起。畢竟多線程無法避免代碼固有的主要資源開銷。)

一個圖片下載器

從web下載圖片很簡單,使用SDK提供的HTTP相關的類即可實現。下面是一個簡單的實現。
(譯註:下面用到的AndroidHttpClient等類從2.2版,也就是API Level 8才開始提供。請2.1以下各位從代碼領會精神即可。直接用HttpClient應該亦可實現。)

static Bitmap downloadBitmap(String url) {
final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
final HttpGet getRequest = new HttpGet(url);

try {
HttpResponse response = client.execute(getRequest);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
return null;
}

final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
try {
inputStream = entity.getContent();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// Could provide a more explicit error message for IOException or IllegalStateException
getRequest.abort();
Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e.toString());
} finally {
if (client != null) {
client.close();
}
}
return null;
}

首先我們建立了一個HTTP用戶端和HTTP請求。如果請求成功,就把響應中包含的圖片內容解碼成位元影像格式並返回,以備後續使用。另外補充一句,為了讓程式可以訪問網路,必須在程式的manifest檔案中聲明使用INTERNET。
注意:舊版的BitmapFactory.decodeStream有個bug,可能使得在網路較慢的時候無法正常工作。可以使用 FlushedInputStream(inputStream)代替原始的inputStream來解決這個問題。下面是這個helper class的實現:

static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}

@Override
public long skip(long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in.skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int byte = read();
if (byte < 0) {
break; // we reached EOF
} else {
bytesSkipped = 1; // we read one byte
}
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}

這個類可以保證skip()確實跳過了參數提供的位元組數,直到流檔案的末尾。

如果你在ListAdapter的getView方法中直接使用上面的downloadBitmap方法,結果可以想象的出,隨著我們滾動螢幕,一定是一頓一頓很不爽的。因為每顯示一個新的view,都必須等待一張圖片完成下載,勢必會影響滾屏的流暢度。

正是因為這想都想得出來的糟糕體驗,AndroidHttpClient根本就不允許在主線程裡啟動!上面的代碼在主線程裡將會提示“本線程無法進行HTTP請求”。如果你不見棺材不落淚,說啥也要親手試試這糟糕的使用者體驗的話,可以用DefaultHttpClient代替 AndroidHttpClient,給自己一個交代。

非同步任務

AsyncTask類提供了一個從主線程產生新任務的方法。讓我們建立一個ImageDownloader類來負責產生任務。這個類將提供一個download方法,從指定URL下載圖片,並在ImageView裡顯示出來。

public class ImageDownloader {

public void download(String url, ImageView imageView) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
task.execute(url);
}
}

/* class BitmapDownloaderTask, see below */
}

BitmapDownloaderTask繼承自AsyncTask。它真正執行圖片下載的任務。任務通過execute方法啟動,該方法是立即返回的,從而使得調用它的主線程代碼可以迅速執行完畢。這正是我們使用AsyncTask的意義所在。下面是BitmapDownloaderTask的實現:

class BitmapDownloaderTask extends AsyncTask {
private String url;
private final WeakReference imageViewReference;

public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference(imageView);
}

@Override
// Actual download method, run in the task thread
protected Bitmap doInBackground(String... params) {
// params comes from the execute() call: params[0] is the url.
return downloadBitmap(params[0]);
}

@Override
// Once the image is downloaded, associates it to the imageView
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}

if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}

doInBackground方法是真正在單獨進程中執行非同步任務的代碼。它調用前面介紹的downloadBitmap方法,完成下載,取得位元影像。
onPostExecute在任務結束後由主線程調用。它通過傳入的參數得到下載回來的位元影像,並設定到ImageView顯示(該ImageView在執行個體化BitmapDownloaderTask時傳入)。需要注意的是這裡對ImageView的引用是以WeakReference的形式儲存在 BitmapDownloaderTask執行個體裡,所以在下載過程中如果activity被關掉,無法阻止activity裡的ImageView被回收。因此我們必須在使用前檢查imageViewReference和imageview是否為空白。
這個簡單的小例子示範了如何使用AsyncTask。如果你親自動手實驗一下,應該會發現這短短几行代碼顯著地改善了ListView的滾屏體驗。推薦閱讀developer.android.com的文章《Painless threading》來學習AsyncTasks的更多細節。
但是,這個基於ListView的例子暴露出一個問題。出於對記憶體的利用效率考慮,ListView會在使用者滾屏的時候對view進行迴圈再利用。如果使用者快速猛烈發飆般地滾屏,一個ImageView對象將會被反覆使用多次。每一次它被顯示出來,都會觸發產生一個下載圖片的任務,從而改變這個 ImageView的顯示內容。那麼問題在哪呢?跟大部分並行程式一樣,關鍵問題在於順序。在我們這個例子中,沒有採取任何措施保證所有下載任務按順序完成,換句話說,無法保證先啟動的任務先完成,後啟動的任務後完成。這樣就導致顯示在list中的圖片可能來自之前的任務,該任務因為花費的時間更長,所以最後結束,最終導致預期外的結果。如果你要下載的圖片們是一次性綁定到一坨ImageView的,那麼就不存在問題,但我們還是從大局出發,為了通用的情況,修正一下吧。

並發處理

要想解決上面提到的問題,我們需要知道並儲存下載任務的順序,以保證最後啟動的任務最後結束,並完成對ImageView的更新。要達到這個目的,讓每個ImageView記住自己的最後一個下載任務就可以了。我們使用一個專用的Drawable類給ImageView添加這份資訊。這個 Drawable類將在下載過程中臨時綁定到ImageView。下面是這個DownloadedDrawable類的代碼:

static class DownloadedDrawable extends ColorDrawable {
private final WeakReference bitmapDownloaderTaskReference;

public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference(bitmapDownloaderTask);
}

public BitmapDownloaderTask getBitmapDownloaderTask() {
return bitmapDownloaderTaskReference.get();
}
}

這個實現方法引入了一個ColorDrawable,這會導致ImageView在下載過程中顯示黑色的背景。需要的話,可以使用一個顯示“下載中…”之類的圖片代替之,換取更友好的使用者介面。再提一遍,注意使用WeakReference來降低與對象執行個體的耦合。
讓我們修改之前的代碼來讓這個類起作用。首先,download方法將建立這個類的執行個體並綁定到ImageView:

public void download(String url, ImageView imageView) {
if (cancelPotentialDownload(url, imageView)) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute(url, cookie);
}
}

cancelPotentialDownload方法將在一個新的下載開始前取消尚在進行中的下載任務。注意,這並不足以保證新開始的下載任務得到的圖片一定能夠被顯示,因為之前的任務可能已經完成了,處於等待onPostExecute方法執行的時間點,而這個onPostExecute方法還是有可能在新任務的onPostExecute方法之後執行。

private static boolean cancelPotentialDownload(String url, ImageView imageView) {
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

if (bitmapDownloaderTask != null) {
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
bitmapDownloaderTask.cancel(true);
} else {
// The same URL is already being downloaded.
return false;
}
}
return true;
}

cancelPotentialDownload調用AsyncTask類的cancel方法來停止進行中的下載任務。大部分情況下它返回true,所以調用它的download方法中可以開始新的下載。唯一的例外情況是如果進行中的下載任務與新工作要求的是同一個URL,我們就不取消舊任務了,讓它繼續下載。注意在我們這個實現方法中,如果ImageView被回收了,與其關聯的下載不會停止(可以藉助RecyclerListener實現)。
這個方法還調用了一個helper函數getBitmapDownloaderTask。代碼很直觀,不做贅述:

private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null;
}

最後,必須修改一下onPostExecute方法,保證只在ImageView尚與下載進程關聯的情況下綁定位元影像到ImageView:

if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
// Change bitmap only if this process is still associated with it
if (this == bitmapDownloaderTask) {
imageView.setImageBitmap(bitmap);
}
}

嗯,做了這些修改之後,我們的ImageDownloader類基本可以提供預期的服務了。你可以在自己的項目中靈活運用這些代碼或者它示範的非同步思想,改善使用者體驗。

Demo

本文的原始碼可以從Google Code擷取。你可以在本文提到的三種實現方式(非非同步、無並發處理以及最終版本)中切換、比較。注意,緩衝大小已經被限制到10張圖片以便更好地示範可能出現的問題。

進一步的工作

文中代碼為了集中討論並行問題而做了簡化,因此缺少很多功能。首先ImageDownloader類應該利用緩衝,特別是與ListView結合使用的時候。因為ListView在使用者上下往返滾屏的時候會多次顯示相同圖片,而緩衝可以大大降低開銷。通過使用一個基於LinkedHashMap(該 hashmap提供從URL到Bitmap SoftReference的映射)的LRU緩衝可以很容易地實現這一點。更加複雜的緩衝機制還可以依賴於本機存放區。縮圖的建立、圖片縮放等功能也可以考慮加進來。
本文代碼已經考慮到了下載錯誤和逾時的情況。這些情況下將會返回一個空位元影像。你也可以顯示一張帶有提示資訊的圖片。
本文樣本的HTTP請求很簡單。根據實際情況的不同(大都依賴於伺服器端),可以在HTTP請求中加入各種參數或者cookie等等。
本文使用的AsyncTask類是一個把任務從主線程分離出來很簡單方便的途徑。你可能會用到Handler類來實現對任務流程更好的控制,比如控制並行的下載線程數,等等。

來自: http://hi.baidu.com/zhuawatianhou/blog/item/b012921723513109972b4398.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.