爲何要在後臺加載Bitmap? java
有沒有過這種體驗:你在Android手機上打開了一個帶有含圖片的ListView的頁面,用手猛地一劃,就見那ListView嘎嘎地卡,彷彿每個新的Item都是頂着阻力蹦出來的同樣?看完這篇文章,你將學會怎樣避免這種狀況的發生。 網絡
在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法能夠把圖片加載到Bitmap中。但因爲這些方法是耗時的,因此多數狀況下,這些方法應該放在非UI線程中,不然將有可能致使界面的卡頓,甚至是觸發ANR。 併發
通常狀況下,網絡圖片的加載必須放在後臺線程中;而本地圖片就能夠根據實際狀況自行決定了,若是圖片很少不大的話,也能夠在UI線程中操做來圖個方便。至於谷歌官方的說法,是隻要是從硬盤或者從網絡加載Bitmap,通通不該該在主線程中進行。 異步
基礎操做:使用AsyncTask async
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); } } } }
以上代碼摘自Android官方文檔,是一個後臺加載Bitmap並在加載完成後自動將Bitmap設置到ImageView的AsyncTask的實現。有了這個AsyncTask以後,異步加載Bitmap只須要下面的簡單代碼: ide
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
而後,一句loadBitmap(R.id.my_image, mImageView) 就能實現本地圖片的異步加載了。 性能
併發操做:在ListView和GridView中進行後臺加載 優化
在實際中,影響性能的每每是ListView和GridView這種包含大量圖片的控件。在滑動過程當中,大量的新圖片在短期內一塊兒被加載,對於沒有進行任何優化的程序,卡頓現象必然會隨之而來。經過使用後臺加載Bitmap的方式,這種問題將被有效解決。具體怎麼作,咱們來看看谷歌推薦的方法。 this
首先建立一個實現了Drawable接口的類,用來存儲AsyncTask的引用。在本例中,選擇了繼承BitmapDrawable,用來給ImageView設置一個預留的佔位圖,這個佔位圖用於在AsyncTask執行完畢以前的顯示。 spa
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(); } }
接下來,和上面相似,依然是使用一個loadBitmap()方法來實現對圖片的異步加載。不一樣的是,要在啓動AsyncTask以前,把AsyncTask傳給AsyncDrawable,而且使用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); } }
而後在Adapter的getView()方法中調用loadBitmap()方法,就能夠爲每一個Item中的ImageView進行圖片的動態加載了。
loadBitmap()方法的代碼中有兩個地方須要注意:第一,cancelPotentialWork()這個方法,它的做用是進行兩項檢查:首先檢查當前是否已經有一個AsyncTask正在爲這個ImageView加載圖片,若是沒有就直接返回true。若是有,再檢查這個Task正在加載的資源是否與本身正要進行加載的資源相同,若是相同,那就沒有必要再進行多一次的加載了,直接返回false;而若是不一樣(爲何會不一樣?文章最後會有解釋),就取消掉這個正在進行的任務,並返回true。第二個須要注意的是,本例中的 BitmapWorkerTask 實際上和上例是有所不一樣的。這兩點咱們分開說,首先咱們看cancelPotentialWork()方法的代碼:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData != data) { // 取消以前的任務 bitmapWorkerTask.cancel(true); } else { // 相同任務已經存在,直接返回false,再也不進行重複的加載 return false; } } // 沒有Task和ImageView進行綁定,或者Task因爲加載資源不一樣而被取消,返回true return true; }
在cancelPotentialWork()的代碼中,首先使用getBitmapWorkerTask()方法獲取到與ImageView相關聯的Task,而後進行上面所說的判斷。好,咱們接着來看這個getBitmapWorkerTask()是怎麼寫的:
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; }從代碼中能夠看出,該方法經過imageView獲取到它內部的Drawable對象,若是獲取到了而且該對象爲AsyncDrawable的實例,就調用這個AsyncDrawable的getBitmapWorkerTask()方法來獲取到它對應的Task,也就是經過一個ImageView->Drawable->AsyncTask的鏈來獲取到ImageView所對應的AsyncTask。
好的,cancelPotentialWork()方法分析完了,咱們回到剛纔提到的第二個點:BitmapWorkerTask類的不一樣。這個類的改動在於onPostExecute()方法,具體請看下面代碼:
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); } } } }從代碼中能夠看出,在後臺加載完Bitmap以後,它 並非直接把Bitmap設置給ImageView,而是先判斷這個ImageView對應的Task是否是本身 (爲何會不一樣?文章最後會有解釋)。若是是本身,纔會執行ImageView的setImageBitmap()方法。到此,一個併發的異步加載ListView(或GridView)中圖片的實現所有完成。
延伸:文中兩個「爲何會不一樣」的解答
首先,簡單說一下ListView中Item和Item對應的View的關係(GridView中同理)。假設一個ListView含有100項,那麼它的100個Item應該分別對應一個View用於顯示,這樣一共是100個View。但Android實際上並無這樣作。出於內存考慮,Android只會爲屏幕上可見的每一個Item分配一個View。用戶滑動ListView,當第一個Item移動到可視範圍外後,他所對應的View將會被系統分配給下一個即將出現的Item。
回到問題。
咱們不妨假設屏幕上顯示了一個ListView,而且它最多能顯示10個Item,而用戶在最頂部的Item(不妨稱他爲第1個Item)使用Task加載Bitmap的時候進行了滑動,而且直到第1個Item消失而第11個Item已經在屏幕底部出現的時候,這個Task尚未加載完成。那麼此時,原先與第1個Item綁定的ImageView已經被從新綁定到了第11個Item上,而且第11個Item觸發了getItem()方法。在getItem()方法中,ImageView第二次使用Task爲本身加載Bitmap,但這時它須要加載的圖片資源已經變了(由第1個Item對應的資源變成了第11個Item對應的資源),所以在cancelPotentialWork()方法執行時會判斷兩個資源不一致。這就是爲何相同ImageView卻對應了不一樣的資源。
同理,一個Task持有了一個ImageView,但因爲這個Task有可能已通過時,所以這個ImageView所對應的Task未必就是這個Task自己,也有多是另外一個更年輕的Task。