標籤:
原文地址:http://android.xsoftlab.net/training/displaying-bitmaps/process-bitmap.html
我們在上節課Load Large Bitmaps Efficiently中討論了BitmapFactory.decode*方法,說到了不應該在UI線程中執行讀取資料的過程,尤其是從磁碟或者網路上讀取資料(或者其它讀取速度次於記憶體的地方)。讀取資料的時間是不可預料的,這取決於各種各樣的因素(從磁碟或者網路讀取的速度、圖片的大小、CPU的功率,etc.)。如果這其中的一個因素阻塞了UI線程,那麼系統會標誌程式為無響應標誌,並會給使用者提供一個關閉的選項(請查看Designing for Responsiveness擷取更多資訊)。
這節課討論了通過使用AsyncTask在非UI線程中處理位元影像以及展示如何處理並發問題。
使用AsyncTask
類AsyncTask提供了一種簡要的方式來處理後台進程的工作,並會將處理後的結果推送到UI線程中。如果要使用這個類,需要建立該類的子類,然後重寫所提供的方法。這裡有個例子,展示了如何使用AsyncTask及decodeSampledBitmapFromResource()來載入一張大圖到ImageView上:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference<ImageView>(imageView); } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } }}
ImageView的WeakReference可以確保AsyncTask不會阻止ImageView及它所引用的事務被記憶體回收行程回收。這不能保證在任務執行完畢的時候ImageView還依然存在,所以你還必須在onPostExecute()方法中檢查一下它的引用。ImageView可能已經不存在了,比如說吧,當使用者離開了activity或者在任務結束的時候一些配置發生了變化。
為了啟動非同步任務來載入圖片,需要簡單的建立一個新任務並執行它:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId);}
處理並發
一些普通的View控制項比如ListView和GridView會涉及到另一個問題,就是當與AsyncTask結合使用的時候會出現並發問題。為了能有效使用記憶體,這些控制項會隨著使用者的滑動來回收子View。如果每一個子View都會觸發一個AsyncTask,那麼就不能保障在任務完成的時候,與之相關聯的View沒有被回收利用。此外,對於順序啟動的任務也不能保障可以按順序完成。
部落格Multithreading for Performance進一步的討論了如何處理並發,它提供了一個解決方案:在ImageView中儲存了最近的AsyncTask的引用,這個引用可以在任務完成的時候對最近的AsyncTask進行檢查。通過類似的辦法,那麼上面章節的AsyncTask可以被擴充成類似的模式。
建立一個專用的Drawable子類來儲存工作任務的引用。在這種情況下,BitmapDrawable就會被用到,所以在任務完成之前可以有一個佔位圖顯示在ImageView上:
static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); }}
在執行BitmapWorkerTask任務之前,你可以建立一個AsyncDrawable並將這個任務綁定到目標ImageView上:
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); }}
上面代碼所引用的cancelPotentialWork()方法用來檢查是否有另外在進行中的任務已經與ImageView關聯上了。如果是這樣的話,它會通過cancel()嘗試取消原來的任務。在少數情況下,建立的任務資料可能會與已經存在的任務相匹配,所以就不要有進一步的動作。下面是cancelPotentialWork()方法的實現:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; // If bitmapData is not yet set or it differs from the new data if (bitmapData == 0 || bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true;}
有個輔助方法:getBitmapWorkerTask(),它被用來接收與指定ImageView相關聯的任務:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null;}
最後一步就是在BitmapWorkerTask中更新onPostExecute(),所以它會檢查任務是否已經被取消和檢查當前的任務是否與與之相關聯的ImageView相匹配:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } }}
現在這個實現就適合用到類似ListView和GridView這種會回收它們子View的組件上了,簡單的調用loadBitmap()就可以正常給ImageView設定圖片了。比如,在一個GridView的實現中,這個方法就可以在相應適配器的getView()方法中使用。
Android官方開發文檔Training系列課程中文版:高效顯示位元影像之在非UI線程中處理圖片