- 論壇徽章:
- 0
|
利用多線程提高程序性能(for Android)
要想搞出一個反應(yīng)迅速的Android應(yīng)用程序,一個很好的做法就是確保在主UI線程里執(zhí)行盡量少的代碼。任何有可能花費較長時間來執(zhí)行的代碼如果在主UI線程執(zhí)行,則會讓程序掛起無法響應(yīng)用戶的操作,所以應(yīng)該放到一個單獨的線程里執(zhí)行。典型的例子就是與網(wǎng)絡(luò)通信相關(guān)的操作了,因為通過網(wǎng)絡(luò)收發(fā)信息的快慢我們無法預(yù)測,有可能“biu”地一下就搞定了,也有可能磨磨唧唧半天。用戶心情好的話可能會容忍一點點遲延,而且前提是你給出了必要的提示,但是一個看上去根本不動貌似嗝兒屁的程序……(譯注:就好比Ajax技術(shù)出現(xiàn)之前的網(wǎng)頁,用戶可以習(xí)慣短時間的載入,但是一個載入了半天都是空白的瀏覽器窗口就常常讓那個撥號時代的我們感到困惑和抓狂。)
在這篇文章中,我們將創(chuàng)建一個簡單的圖片下載程序來演示一下多線程模式。我們將從網(wǎng)上下載一坨圖片,然后用這些圖片生成一個縮略圖列表。創(chuàng)建一個異步工作的任務(wù),讓它在后臺下載圖片,會讓我們的程序看上去更快。(譯注:這里我加上“看上去”,因為我認為所謂多線程讓程序更快,更多的意義在于“提高對用戶操作的響應(yīng)”。包括本文題目,所謂的“高性能”,主要指的還是避免UI的硬直(格斗游戲術(shù)語,請自行g(shù)oogle)、掛起。畢竟多線程無法避免代碼固有的主要資源開銷。)
一個圖片下載器
從web下載圖片很簡單,使用SDK提供的HTTP相關(guān)的類即可實現(xiàn)。下面是一個簡單的實現(xiàn)。
(譯注:下面用到的AndroidHttpClient等類從2.2版,也就是API Level 8才開始提供。請2.1以下各位從代碼領(lǐng)會精神即可。直接用HttpClient應(yīng)該亦可實現(xiàn)。)
- 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;
- }
復(fù)制代碼 首先我們創(chuàng)建了一個HTTP客戶端和HTTP請求。如果請求成功,就把響應(yīng)中包含的圖片內(nèi)容解碼成位圖格式并返回,以備后續(xù)使用。另外補充一句,為了讓程序可以訪問網(wǎng)絡(luò),必須在程序的manifest文件中聲明使用INTERNET。
注意:舊版的BitmapFactory.decodeStream有個bug,可能使得在網(wǎng)絡(luò)較慢的時候無法正常工作?梢允褂 FlushedInputStream(inputStream)代替原始的inputStream來解決這個問題。下面是這個helper class的實現(xiàn):
- 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;
- }
- }
復(fù)制代碼 這個類可以保證skip()確實跳過了參數(shù)提供的字節(jié)數(shù),直到流文件的末尾。
如果你在ListAdapter的getView方法中直接使用上面的downloadBitmap方法,結(jié)果可以想象的出,隨著我們滾動屏幕,一定是一頓一頓很不爽的。因為每顯示一個新的view,都必須等待一張圖片完成下載,勢必會影響滾屏的流暢度。
正是因為這想都想得出來的糟糕體驗,AndroidHttpClient根本就不允許在主線程里啟動!上面的代碼在主線程里將會提示“本線程無法進行HTTP請求”。如果你不見棺材不落淚,說啥也要親手試試這糟糕的用戶體驗的話,可以用DefaultHttpClient代替 AndroidHttpClient,給自己一個交代。
異步任務(wù)
AsyncTask類提供了一個從主線程生成新任務(wù)的方法。讓我們創(chuàng)建一個ImageDownloader類來負責(zé)生成任務(wù)。這個類將提供一個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 */
- }
復(fù)制代碼 BitmapDownloaderTask繼承自AsyncTask。它真正執(zhí)行圖片下載的任務(wù)。任務(wù)通過execute方法啟動,該方法是立即返回的,從而使得調(diào)用它的主線程代碼可以迅速執(zhí)行完畢。這正是我們使用AsyncTask的意義所在。下面是BitmapDownloaderTask的實現(xiàn):- 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);
- }
- }
- }
- }
復(fù)制代碼 doInBackground方法是真正在單獨進程中執(zhí)行異步任務(wù)的代碼。它調(diào)用前面介紹的downloadBitmap方法,完成下載,取得位圖。
onPostExecute在任務(wù)結(jié)束后由主線程調(diào)用。它通過傳入的參數(shù)得到下載回來的位圖,并設(shè)置到ImageView顯示(該ImageView在實例化BitmapDownloaderTask時傳入)。需要注意的是這里對ImageView的引用是以WeakReference的形式保存在 BitmapDownloaderTask實例里,所以在下載過程中如果activity被關(guān)掉,無法阻止activity里的ImageView被回收。因此我們必須在使用前檢查imageViewReference和imageview是否為空。
這個簡單的小例子演示了如何使用AsyncTask。如果你親自動手實驗一下,應(yīng)該會發(fā)現(xiàn)這短短幾行代碼顯著地改善了ListView的滾屏體驗。推薦閱讀developer.android.com的文章《Painless threading》來學(xué)習(xí)AsyncTasks的更多細節(jié)。
但是,這個基于ListView的例子暴露出一個問題。出于對內(nèi)存的利用效率考慮,ListView會在用戶滾屏的時候?qū)iew進行循環(huán)再利用。如果用戶快速猛烈發(fā)飆般地滾屏,一個ImageView對象將會被反復(fù)使用多次。每一次它被顯示出來,都會觸發(fā)生成一個下載圖片的任務(wù),從而改變這個 ImageView的顯示內(nèi)容。那么問題在哪呢?跟大部分并行程序一樣,關(guān)鍵問題在于順序。在我們這個例子中,沒有采取任何措施保證所有下載任務(wù)按順序完成,換句話說,無法保證先啟動的任務(wù)先完成,后啟動的任務(wù)后完成。這樣就導(dǎo)致顯示在list中的圖片可能來自之前的任務(wù),該任務(wù)因為花費的時間更長,所以最后結(jié)束,最終導(dǎo)致預(yù)期外的結(jié)果。如果你要下載的圖片們是一次性綁定到一坨ImageView的,那么就不存在問題,但我們還是從大局出發(fā),為了通用的情況,修正一下吧。
并發(fā)處理
要想解決上面提到的問題,我們需要知道并保存下載任務(wù)的順序,以保證最后啟動的任務(wù)最后結(jié)束,并完成對ImageView的更新。要達到這個目的,讓每個ImageView記住自己的最后一個下載任務(wù)就可以了。我們使用一個專用的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();
- }
- }
復(fù)制代碼 這個實現(xiàn)方法引入了一個ColorDrawable,這會導(dǎo)致ImageView在下載過程中顯示黑色的背景。需要的話,可以使用一個顯示“下載中…”之類的圖片代替之,換取更友好的用戶界面。再提一遍,注意使用WeakReference來降低與對象實例的耦合。
讓我們修改之前的代碼來讓這個類起作用。首先,download方法將創(chuàng)建這個類的實例并綁定到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);
- }
- }
復(fù)制代碼 cancelPotentialDownload方法將在一個新的下載開始前取消尚在進行中的下載任務(wù)。注意,這并不足以保證新開始的下載任務(wù)得到的圖片一定能夠被顯示,因為之前的任務(wù)可能已經(jīng)完成了,處于等待onPostExecute方法執(zhí)行的時間點,而這個onPostExecute方法還是有可能在新任務(wù)的onPostExecute方法之后執(zhí)行。
- 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;
- }
復(fù)制代碼 cancelPotentialDownload調(diào)用AsyncTask類的cancel方法來停止進行中的下載任務(wù)。大部分情況下它返回true,所以調(diào)用它的download方法中可以開始新的下載。唯一的例外情況是如果進行中的下載任務(wù)與新任務(wù)請求的是同一個URL,我們就不取消舊任務(wù)了,讓它繼續(xù)下載。注意在我們這個實現(xiàn)方法中,如果ImageView被回收了,與其關(guān)聯(lián)的下載不會停止(可以借助RecyclerListener實現(xiàn))。
這個方法還調(diào)用了一個helper函數(shù)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;
- }
復(fù)制代碼 最后,必須修改一下onPostExecute方法,保證只在ImageView尚與下載進程關(guān)聯(lián)的情況下綁定位圖到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);
- }
- }
復(fù)制代碼 嗯,做了這些修改之后,我們的ImageDownloader類基本可以提供預(yù)期的服務(wù)了。你可以在自己的項目中靈活運用這些代碼或者它演示的異步思想,改善用戶體驗。
Demo
本文的源代碼可以從Google Code獲取。你可以在本文提到的三種實現(xiàn)方式(非異步、無并發(fā)處理以及最終版本)中切換、比較。注意,緩存大小已經(jīng)被限制到10張圖片以便更好地演示可能出現(xiàn)的問題。
進一步的工作
文中代碼為了集中討論并行問題而做了簡化,因此缺少很多功能。首先ImageDownloader類應(yīng)該利用緩存,特別是與ListView結(jié)合使用的時候。因為ListView在用戶上下往返滾屏的時候會多次顯示相同圖片,而緩存可以大大降低開銷。通過使用一個基于LinkedHashMap(該 hashmap提供從URL到Bitmap SoftReference的映射)的LRU緩存可以很容易地實現(xiàn)這一點。更加復(fù)雜的緩存機制還可以依賴于本地存儲?s略圖的創(chuàng)建、圖片縮放等功能也可以考慮加進來。
本文代碼已經(jīng)考慮到了下載錯誤和超時的情況。這些情況下將會返回一個空位圖。你也可以顯示一張帶有提示信息的圖片。
本文示例的HTTP請求很簡單。根據(jù)實際情況的不同(大都依賴于服務(wù)器端),可以在HTTP請求中加入各種參數(shù)或者cookie等等。
本文使用的AsyncTask類是一個把任務(wù)從主線程分離出來很簡單方便的途徑。你可能會用到Handler類來實現(xiàn)對任務(wù)流程更好的控制,比如控制并行的下載線程數(shù),等等。
來自: http://hi.baidu.com/zhuawatianho ... 513109972b4398.html |
|