在UI主線程外處理Bitmap
BitmapFactory.decode*
系列的方法,討論的是怎麼樣去高效的載入大圖片,但是不管圖片資料的來源,這些方法都不應該在UI主線程上使用。因為這些方法耗費的時間是不可預估的,圖片載入耗費的時間依賴於很多的因素(網路或硬碟的讀寫速度,圖片的大小,手機CPU的處理能力等等)。在程式中,只要其中一個圖片載入任務阻塞了UI主線程,那麼你的應用將會無響應(ANR,不能再與使用者進行互動了),Android系統會顯示通知使用者,使用者將會選擇關閉你的應用,這是一項非常不好的使用者體驗。 出於這種原因,你可以使用
AsyncTask
來在UI主線程外的線程中處理圖片載入,但還有一個問題,要妥善處理好並發的問題,下面將介紹這兩種問題的處理方法使用AsyncTask
AsyncTask
類為我們提供了一種很好的方式來在UI主線程之外的線程中執行一些任務,並且把任務的結果返回到UI主線程上。你需要繼承AsyncTask 類並重載一些提供的方法來使用這種非同步任務方式,下面是一個樣本程式
class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; private int data = 0; public BitmapWorkerTask(ImageView imageView) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference(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. // 此方法運行在UI主線程上 @Override protected void onPostExecute(Bitmap bitmap) { if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } }}
使用弱引用去儲存 ImageView 對象是為了確保 AsyncTask
不會去影響ImageView 的使用和記憶體回收行程能夠回收ImageView 引用的對象。不能保證當任務完成的時候ImageView 引用的對象沒有被回收,所以呢你必須在onPostExecute() 方法中進行檢查是否為null。ImageView 對象可能會不存在了,例如,在一個任務結束前,使用者可能已經離開了當前的使用者介面(Activity被destroy)或者一個配置項(例如橫豎屏切換)發生改變
,所以在使用的時候必須對ImageView 對象做必要的檢測。
你可以直接建立一個AsyncTask
對象並調用execute()方法來非同步載入圖片
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId);}
處理並發問題 像ListView和GridView 這種常見的視圖組件,結合AsyncTask 一起使用的時候會產生另外一個問題。為了儘可能的提高記憶體的使用效率,當使用者滾動組件時,這類別檢視組件會迴圈利用它的子視圖,如果每個子視圖都觸發一個 AsyncTask
,那麼將不會有保證相關聯的視圖是否已經回收供另外一個子視圖迴圈使用了。而且,非同步任務的執行順序並不意味著任務的執行結束時的順序也是這樣的。結合這兩種原因,引發的這個並發問題有可能會導致圖片設定到子視圖上時會發生錯位。 針對這種並發、錯位問題,我們可以讓ImageView Object Storage Service一個最近使用的AsyncTask 的引用,這樣我們可以去檢測任務是否完成 建立一個專用的Drawable 子類來儲存一個與之對應的AsyncTask (這個AsyncTask 實際上是用來載入Drawable 的任務),當圖片載入任務還在執行時,就可以使用這個Drawable 來顯示一張預設的圖片了,當任務完成時這個Drawable隨之被替換
//儲存著一個與之對應的task, 在任務還在執行時充當一個預留位置,當任務完成時,隨之被替換static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); }}
在執行任務之前,先設定預留位置,ImageView 將顯示預設圖片
public void loadBitmap(int resId, ImageView imageView) { if (cancelPotentialWork(resId, imageView)) { final BitmapWorkerTask task = new BitmapWorkerTask(imageView); // mPlaceHolderBitmap 引用的就是預設要顯示的圖片對象 final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); }}
cancelPotentialWork
方法是用來檢測與ImageView 相關聯的另外一個任務是否正在執行,如果是的話,將會通過調用cancel()方法來關閉這個
任務。只有極少部分的情況下兩個任務的圖片資料會相等,如果相等的話,那也不會出現什麼並發問題了,我們也不需要做任何的處理
public static boolean cancelPotentialWork(int data, ImageView imageView) { // 取得當前與ImageView相關聯的任務 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); // 如果存在的話,則取消 if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (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() 方法的定義
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { // imageView有可能被回收 if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null;}
現在在 onPostExecute()
方法中我們就需要檢測任務是否被取消了,還有需要檢測下當前的任務是否是與imageView相關聯的任務
class BitmapWorkerTask extends AsyncTask { ... @Override protected void onPostExecute(Bitmap bitmap) { // if task cancelled , this method is never invoked // why check here ? if (isCancelled()) { bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); // 判斷當前task是否是相關聯的task if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } }}
前面的ImageView載入的實現方式適用於像 ListView
and GridView
這類的UI視圖組件,我們可以在Adapter中的getView()
方法中調用loadBitmap 方法