Android中高效的顯示圖片 - 非UI線程加載

AsyncTask

Android中高效的顯示圖片 - 加載大圖一文中講到了BitmapFactory.decode*方法的使用,但使用時需要注意不應該在UI線程中調用它們來從硬盤、網絡或者其他非內存的地方加載圖片。因為加載圖片所需要的時間是不可預測的,它跟很多因素有關,比如網絡狀況、硬盤讀寫速度、圖片的大小、CPU的速度等。如果我們阻塞UI線程來加載圖片有可能會導致ANR。

(本文出處:http://www.lxweimin.com/p/adf6c5cf4fbd)

使用AsyncTask加載圖片

解決方法就是開啟一個后臺線程來異步加載圖片。使用Android API提供的AsyncTask類可以很方便的在后臺線程中完成圖片加載然后將結果返回給UI線程。下面就是一個使用AsyncTask來異步加載圖片的例子。

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。例如,在任務完成之前用戶退出了這個Activity,那么這個ImageView是應該被釋放的。由于異步的關系我們無法保證任務執行完成后ImageView仍然存在,所以需要在onPostExecute方法里檢查ImageView的引用。

并發

例如ListView、GridView這類視圖組件結合上面的AsyncTask又會引入另一個問題。為了高效的使用內存,這類組件在滑動的時候會復用子view,如果一個view觸發一個AsyncTask,那我們無法保證任務執行完成后view沒有被復用。如果view被復用從而觸發兩次AsyncTask,我們也無法保證異步任務的執行順序,很有可能先觸發的任務后執行完成,這就會導致結果錯誤。

這里提供的解決方案是在ImageView中綁定最后觸發的AsyncTask的引用,當任務執行完成后返回結果時再比較返回結果的任務是不是ImageView綁定的任務,這樣就可以保證Imageview顯示的結果就是它最后觸發的AsyncTask的結果。

ImageView是系統的一個類,他并沒有給我們預設一個屬性讓我們來記錄AsyncTask,那么我們如何才能將AsyncTask綁定到ImageView中去呢?當然可以繼承ImageView來自定義一個包含AsyncTask字段的AsyncImageView,但是這樣可能會影響到布局文件。這里使用了另外一種實現方式。大家都知道ImageView有一個setImageDrawable(BitmapDrawable b)的方法,這就說明ImageView可以保存一個BitmapDrawable變量,如果我們能將AsyncTask放到BitmapDrawable,那么實際上AsyncTask也就放到ImageView里了。所以我們只需要繼承BitmapDrawable實現一個AsyncDrawable,把這個AsyncDrawable設置給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上是不是有正在運行的異步任務,如果有且上一個任務與當前請求的任務是同一個任務就直接返回false避免重復請求,如果有且任務不一樣就取消上一個任務。

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里獲取綁定的AsyncTask的方法。

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);      
            }    
        }  
    }
}

現在BitmapWorkerTask可用在ListView、GradView或者別的復用子view的組件上完美運行了。只需要在你想要給ImageView設置圖片的地方調用loadBitmap即可。例如,在GridView的適配器的getView方法里調用loadBitmap給子view設置圖片內容。

總結

本文實現了后臺線程加載圖片的功能。并對因多線程引入的并發問題給出了解決方案。


本文是《Android中高效的顯示圖片》專題中的第二篇

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容