【Google官方教程】第二課:在非UI執行緒處理Bitmap

yangxi_001發表於2013-11-26

在Google最新的文件中,提供了一系列含金量相當高的教程。因為種種原因而鮮為人知,真是可惜!Ryan將會細心整理,將之翻譯成中文,希望對開發者有所幫助。

        本系列是Google關於展示大Bitmap(點陣圖)的官方演示,可以有效的解決記憶體限制,更加有效的載入並顯示圖片,同時避免讓人頭疼的OOM(Out Of Memory)。

-------------------------------------------------------------------------------------

譯文:

       在高效地載入Bitmap中,我們討論了BitmapFactory.decode*系列方法,如果源資料來自硬碟或者網路(或者除記憶體之外的來源),是不應該在主UI執行緒執行的。這是因為讀取這樣的資料所需的載入時間是不確定的,它依賴於多種因素(從硬碟或網路的讀取速度、圖片的大小、CPU的功率等等)。如果這些任務裡面任何一個阻塞了UI執行緒,系統會將你的應用標記為未響應,並且使用者可以選擇關閉應用(更多資訊,請參閱Designing for Responsiveness)。


        這節課將教會你使用AsyncTask在後臺執行緒處理Bitmap並向你展示如何處理併發問題。

使用AsyncTask(非同步任務)

        AsyncTask類提供了一種簡單的方法,可以在後來執行緒處理一些事情,並將結果返回到UI執行緒。要使用它,需要建立一個繼承於它的子類,並且覆寫它提供的方法。這裡有一個使用AsyncTask和decodeSampledBitmapFromResource()載入大圖片到ImageView中的例子: 
01 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
02     private final WeakReference<ImageView> imageViewReference;
03     private int data = 0;
04  
05     public BitmapWorkerTask(ImageView imageView) {
06         // Use a WeakReference to ensure the ImageView can be garbage collected
07         imageViewReference = new WeakReference<ImageView>(imageView);
08     }
09  
10     // Decode image in background.
11     @Override
12     protected Bitmap doInBackground(Integer... params) {
13         data = params[0];
14         return decodeSampledBitmapFromResource(getResources(), data, 100100));
15     }
16  
17     // Once complete, see if ImageView is still around and set bitmap.
18     @Override
19     protected void onPostExecute(Bitmap bitmap) {
20         if (imageViewReference != null && bitmap != null) {
21             final ImageView imageView = imageViewReference.get();
22             if (imageView != null) {
23                 imageView.setImageBitmap(bitmap);
24             }
25         }
26     }
27 }

        ImageView的WeakReference(弱引用)可以確保AsyncTask不會阻止ImageView和它的任何引用被垃圾回收器回收。不能保證在非同步任務完成後ImageView依然存在,因此你必須在onPostExecute()方法中檢查引用。ImageView可能已經不存在了,比如說,使用者在任務完成前退出了當前Activity或者應用配置發生了變化(橫屏)。

        為了非同步載入Bitmap,我們建立一個簡單的非同步任務並且執行它:

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


處理併發

        常見的View(檢視)元件如ListView和GridView在於AsyncTask配合使用的時候引出了另外一個問題,這個我們在上一節中提到過。為了提升記憶體效率,當使用者滾動這些元件的時候進行子檢視的回收(主要是回收不可見的檢視)。如果每個子檢視都觸發了一個AsyncTask,無法保證在任務完成的時候,關聯檢視還沒有被回收而被用來顯示另一個子檢視。此外,也無法保證非同步任務結束的循序與它們開始的順序一致。

        Multithreading for Performance這篇文章深入討論瞭如何處理併發問題,並且給出瞭如何在任務結束的時候檢測ImageView儲存最近使用的AsyncTask引用的解決方案。使用相似的方法,可以遵循類似的模式來擴充套件前面的AsyncTask。

        建立一個專用的Drawable之類,用來儲存worker task的引用。在這種情況下,任務結束的時候BitmapDrawable可以取代影象佔位符顯示在ImageView中。

01 static class AsyncDrawable extends BitmapDrawable {
02     private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
03  
04     public AsyncDrawable(Resources res, Bitmap bitmap,
05             BitmapWorkerTask bitmapWorkerTask) {
06         super(res, bitmap);
07         bitmapWorkerTaskReference =
08             new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
09     }
10  
11     public BitmapWorkerTask getBitmapWorkerTask() {
12         return bitmapWorkerTaskReference.get();
13     }
14 }
        在執行BitmapWorkerTask前,你需要建立一個AsyncDrawable並將之繫結到目標ImageView: 
1 public void loadBitmap(int resId, ImageView imageView) {
2     if (cancelPotentialWork(resId, imageView)) {
3         final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
4         final AsyncDrawable asyncDrawable =
5                 new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
6         imageView.setImageDrawable(asyncDrawable);
7         task.execute(resId);
8     }
9 }
        在上面的程式碼示例中引用的cancelPotentialWork方法可以檢測一個執行中的任務是否與ImageView有關聯。如果有關聯,它將通過呼叫canceel()方法試圖取消之前的任務。在少數情況下,新的任務中的資料與現有的任務相匹配,因此不需要做什麼。下面是calcelPotentialWork的具體實現: 
01 public static boolean cancelPotentialWork(int data, ImageView imageView) {
02     final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
03  
04     if (bitmapWorkerTask != null) {
05         final int bitmapData = bitmapWorkerTask.data;
06         if (bitmapData != data) {
07             // Cancel previous task
08             bitmapWorkerTask.cancel(true);
09         else {
10             // The same work is already in progress
11             return false;
12         }
13     }
14     // No task associated with the ImageView, or an existing task was cancelled
15     return true;
16 }

        一個助手方法,getBitmapWorkerTask(),在上面用來檢索和指定ImageView相關的任務

01 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
02    if (imageView != null) {
03        final Drawable drawable = imageView.getDrawable();
04        if (drawable instanceof AsyncDrawable) {
05            final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
06            return asyncDrawable.getBitmapWorkerTask();
07        }
08     }
09     return null;
10 }
        最後一步是更新BitmapWorkerTask中的onPostExecute()方法,以便檢測與ImageView關聯的任務是否被取消或者與當前任務相匹配。 

01 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
02     ...
03  
04     @Override
05     protected void onPostExecute(Bitmap bitmap) {
06         if (isCancelled()) {
07             bitmap = null;
08         }
09  
10         if (imageViewReference != null && bitmap != null) {
11             final ImageView imageView = imageViewReference.get();
12             final BitmapWorkerTask bitmapWorkerTask =
13                     getBitmapWorkerTask(imageView);
14             if (this == bitmapWorkerTask && imageView != null) {
15                 imageView.setImageBitmap(bitmap);
16             }
17         }
18     }
19 }
        這裡的方法也適合用在ListView、GridView以及其他任何需要回收子檢視的元件中。當你只需要為ImageView設定圖片,呼叫loadBitmap就可以了。例如,在GridView中實現的方式是在Adapter的getView()方法中。 

相關文章