在非 UI 線程來處理圖片

在非 UI 線程來處理圖片


課程內容

高效的加載大尺寸圖片 中介紹的 用來解析圖片的 BitmapFactory.decode* 函數,需要在非UI線程中調用。
如果是讀取網絡圖片或者磁盤圖片,在UI線程中可能會導致程序ANR;如果是解析已經在內存中的圖片,則可以在UI線程中調用這些函數。

這節內容介紹如何使用AsyncTask 來處理圖片,以及如何處理併發訪問。

 

使用 AsyncTask

AsyncTask 類是一個在後臺線程中處理任務,並把結果反饋給UI線程的工具類。使用該類,只需要繼承她並且重寫對應的函數即可。
下面的代碼演示瞭如何用這個類來載入一個大尺寸圖片並顯示在
ImageView 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BitmapWorkerTask extends AsyncTask<INTEGER, Bitmap Void,> {
    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可能已經不存在了。

只要創建該Task並執行即可開始異步加載圖片了:

1
2
3
4
public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

處理併發訪問

ListViewGridView
這種控件使用上面介紹的方式來載入圖片可能會引入新的問題。爲了提高內存的使用率,當用戶做滾動操作的時候這些控件會重複利用子控件,如果每個子控件都觸發一個AsyncTask,當任務完成的時候 無法保證該子控件是否已經被重用了。甚至,這些任務開始的順序和完成的順序也是不一樣的。

這篇博文Multithreading for Performance 進一步
討論瞭如何處理併發操作,並且提供了一個解決方案:
ImageView 中保存了最近觸發的AsyncTask對象,當任務完成的時候可以用來檢測該引用的對象。
使用相似的函數,在前面介紹的AsyncTask對象可以使用相似的模式來擴展。

創建一個特殊的 Drawable 子類來保存載入圖片的Task引用。
這裏使用 BitmapDrawable 類,這樣當任務完成的時候可以直接在
ImageView 中顯示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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中:

1
2
3
4
5
6
7
8
9
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 函數用來檢測是否已經有一個Task綁定到 ImageView了。
如果已經有個Task了就嘗試取消這個Task(調用 cancel()函數)。
在一些情況下,如果新的任務和已經存在任務的數據一樣,則不需要額外的處理。下面是 cancelPotentialWork 函數的一種實現方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static boolean cancelPotentialWork(int data, 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(),用來獲取和 ImageView關聯的任務:

1
2
3
4
5
6
7
8
9
10
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引用的任務是否爲同一個任務:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BitmapWorkerTask extends AsyncTask<INTEGER, Bitmap Void,> {
    ...
  
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        <STRONG>if (isCancelled()) {
            bitmap = null;
        }</STRONG> 
  
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            <STRONG>final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);</STRONG>
            if (<STRONG>this == bitmapWorkerTask &&</STRONG> imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

這樣就可以在 ListViewGridView
控件中使用該圖片加載任務了。只需要在您設置
ImageView圖片的地方調用
loadBitmap 函數即可。
例如:在 GridView 中可能需要在Adapter中的 getView() 函數中調用
loadBitmap 函數。



原文轉載自 雲在千峯: http://yunfeng.sinaapp.com/?p=394#ixzz20lLvHtnN

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章