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中高效的顯示圖片》專題中的第二篇

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容