大家好,我是你們的志哥。
今天打算分享一下如何解決ListView異步加載圖片亂序問題,覺得文章寫得不錯的朋友,歡迎點贊。更歡迎路過的大神指出其中不足。
在Android所有系統自帶的控件當中,ListView這個控件算是用法比較複雜的了,關鍵是用法複雜也就算了,它還經常會出現一些稀奇古怪的問題,讓人非常頭疼。比如說在ListView中加載圖片,如果是同步加載圖片倒還好,但是一旦使用異步加載圖片那麼問題就來了,這個問題我相信很多Android開發者都曾經遇到過,就是異步加載圖片會出現錯位亂序的情況。遇到這個問題時,不少人在網上搜索找到了相應的解決方案,但是真正深入理解這個問題出現的原因並對症解決的人恐怕還並不是很多。那麼今天我們就來具體深入分析一下ListView異步加載圖片出現亂序問題的原因,以及怎麼樣對症下藥去解決它。
本篇文章的原理基礎建立在上一篇文章之上,如果你對ListView的工作原理還不夠了解的話,建議先去閱讀 Android
ListView工作原理完全解析,帶你從源碼的角度徹底理解 。
問題重現
要想解決問題首先我們要把問題重現出來,這裏只需要搭建一個最基本的ListView項目,然後在ListView中去異步請求圖片並顯示,問題就能夠得以重現了,那麼我們就新建一個ListViewTest項目。
項目建好之後第一個要解決的是數據源的問題,由於ListView中需要從網絡上請求圖片,那麼我就提前準備好了許多張圖片,將它們上傳到了我的CSDN相冊當中,然後新建一個Images類,將所有相冊中圖片的URL地址都配置進去就可以了,代碼如下所示:
-
-
-
-
public class Images {
-
-
public final static String[] imageUrls = new String[] {
-
"https://img-my.csdn.net/uploads/201508/05/1438760758_3497.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760758_6667.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760757_3588.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760756_3304.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760755_6715.jpeg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760726_5120.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760726_8364.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760725_4031.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760724_9463.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760724_2371.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760707_4653.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760706_6864.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760706_9279.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760704_2341.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760704_5707.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760685_5091.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760685_4444.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760684_8827.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760683_3691.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760683_7315.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760663_7318.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760662_3454.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760662_5113.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760661_3305.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760661_7416.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760589_2946.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760589_1100.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760588_8297.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760587_2575.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760587_8906.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760550_2875.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760550_9517.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760549_7093.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760549_1352.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760548_2780.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760531_1776.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760531_1380.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760530_4944.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760530_5750.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760529_3289.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760500_7871.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760500_6063.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760499_6304.jpeg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760499_5081.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760498_7007.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760478_3128.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760478_6766.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760477_1358.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760477_3540.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760476_1240.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760446_7993.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760446_3641.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760445_3283.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760444_8623.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760444_6822.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760422_2224.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760421_2824.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760420_2660.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760420_7188.jpg",
-
"https://img-my.csdn.net/uploads/201508/05/1438760419_4123.jpg",
-
};
-
}
設置好了圖片源之後,我們需要一個ListView來展示所有的圖片。打開或修改activity_main.xml中的代碼,如下所示:
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
android:orientation="vertical">
-
-
<ListView
-
android:id="@+id/list_view"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
>
-
</ListView>
-
-
</LinearLayout>
很簡單,只是在LinearLayout中寫了一個ListView而已。接着我們要定義ListView中每一個子View的佈局,新建一個image_item.xml佈局,加入如下代碼:
-
<?xml version="1.0" encoding="utf-8"?>
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<ImageView
-
android:id="@+id/image"
-
android:layout_width="match_parent"
-
android:layout_height="120dp"
-
android:src="@drawable/empty_photo"
-
android:scaleType="fitXY"/>
-
-
</LinearLayout>
仍然很簡單,image_item.xml佈局中只有一個ImageView控件,就是用它來顯示圖片的,控件在默認情況下會顯示一張empty_photo。這樣我們就把所有的佈局文件都寫好了。
接下來新建ImageAdapter做爲ListView的適配器,代碼如下所示:
-
-
-
-
-
public class ImageAdapter extends ArrayAdapter<String> {
-
-
-
-
-
private LruCache<String, BitmapDrawable> mMemoryCache;
-
-
public ImageAdapter(Context context, int resource, String[] objects) {
-
super(context, resource, objects);
-
-
int maxMemory = (int) Runtime.getRuntime().maxMemory();
-
int cacheSize = maxMemory / 8;
-
mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
-
@Override
-
protected int sizeOf(String key, BitmapDrawable drawable) {
-
return drawable.getBitmap().getByteCount();
-
}
-
};
-
}
-
-
@Override
-
public View getView(int position, View convertView, ViewGroup parent) {
-
String url = getItem(position);
-
View view;
-
if (convertView == null) {
-
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
-
} else {
-
view = convertView;
-
}
-
ImageView image = (ImageView) view.findViewById(R.id.image);
-
BitmapDrawable drawable = getBitmapFromMemoryCache(url);
-
if (drawable != null) {
-
image.setImageDrawable(drawable);
-
} else {
-
BitmapWorkerTask task = new BitmapWorkerTask(image);
-
task.execute(url);
-
}
-
return view;
-
}
-
-
-
-
-
-
-
-
-
-
public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
-
if (getBitmapFromMemoryCache(key) == null) {
-
mMemoryCache.put(key, drawable);
-
}
-
}
-
-
-
-
-
-
-
-
-
public BitmapDrawable getBitmapFromMemoryCache(String key) {
-
return mMemoryCache.get(key);
-
}
-
-
-
-
-
-
-
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
-
-
private ImageView mImageView;
-
-
public BitmapWorkerTask(ImageView imageView) {
-
mImageView = imageView;
-
}
-
-
@Override
-
protected BitmapDrawable doInBackground(String... params) {
-
String imageUrl = params[0];
-
-
Bitmap bitmap = downloadBitmap(imageUrl);
-
BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
-
addBitmapToMemoryCache(imageUrl, drawable);
-
return drawable;
-
}
-
-
@Override
-
protected void onPostExecute(BitmapDrawable drawable) {
-
if (mImageView != null && drawable != null) {
-
mImageView.setImageDrawable(drawable);
-
}
-
}
-
-
-
-
-
-
-
-
-
private Bitmap downloadBitmap(String imageUrl) {
-
Bitmap bitmap = null;
-
HttpURLConnection con = null;
-
try {
-
URL url = new URL(imageUrl);
-
con = (HttpURLConnection) url.openConnection();
-
con.setConnectTimeout(5 * 1000);
-
con.setReadTimeout(10 * 1000);
-
bitmap = BitmapFactory.decodeStream(con.getInputStream());
-
} catch (Exception e) {
-
e.printStackTrace();
-
} finally {
-
if (con != null) {
-
con.disconnect();
-
}
-
}
-
return bitmap;
-
}
-
-
}
-
-
}
ImageAdapter中的代碼還算是比較簡單的,在getView()方法中首先根據當前的位置獲取到圖片的URL地址,然後使用inflate()方法加載image_item.xml這個佈局,並獲取到ImageView控件的實例,接下來開啓了一個BitmapWorkerTask異步任務來從網絡上加載圖片,最終將加載好的圖片設置到ImageView上面。注意這裏爲了防止圖片佔用過多的內存,我們還是使用了LruCache技術來進行內存控制,對這個技術不熟悉的朋友可以參考這篇文章 Android高效加載大圖、多圖解決方案,有效避免程序OOM 。
最後,程序主界面的代碼就非常簡單了,修改MainActivity中的代碼,如下所示:
-
-
-
-
-
public class MainActivity extends Activity {
-
-
private ListView listView;
-
-
@Override
-
protected void onCreate(Bundle savedInstanceState) {
-
super.onCreate(savedInstanceState);
-
setContentView(R.layout.activity_main);
-
listView = (ListView) findViewById(R.id.list_view);
-
ImageAdapter adapter = new ImageAdapter(this, 0, Images.imageThumbUrls);
-
listView.setAdapter(adapter);
-
}
-
-
-
}
這就是整個程序所有的代碼了,記得還需要在AndroidManifest.xml中添加INTERNET權限。
那麼目前程序的思路其實是很簡單的,我們在ListView的getView()方法中開啓異步請求,從網絡上獲取圖片,當圖片獲取成功就後就將圖片顯示到ImageView上面。看起來沒什麼問題對嗎?那麼現在我們就來運行一下程序看一看效果吧。
恩?怎麼會這個樣子,當滑動ListView的時候,圖片竟然會自動變來變去,而且圖片顯示的位置也不正確,簡直快亂成一鍋粥了!可是我們所有的邏輯都很簡單呀,怎麼會導致出現這種圖片自動變來變去的情況?很遺憾,這是由於Listview內部的工作機制所導致的,如果你對Listview的工作機制不瞭解,那麼就會很難理解這種現象,不過好在上篇文章中我已經講解過ListView的工作原理了,因此下面就讓我們一起分析一下這個問題出現的原因。
原因分析
上篇文章中已經提到了,ListView之所以能夠實現加載成百上千條數據都不會OOM,最主要在於它內部優秀的實現機制。雖然作爲普通的使用者,我們大可不必關心ListView內部到底是怎麼實現的,但是當你瞭解了它的內部原理之後,很多之前難以解釋的問題都變得有理有據了。
ListView在藉助RecycleBin機制的幫助下,實現了一個生產者和消費者的模式,不管有任意多條數據需要顯示,ListView中的子View其實來來回回就那麼幾個,移出屏幕的子View會很快被移入屏幕的數據重新利用起來,原理示意圖如下所示:
那麼這裏我們就可以思考一下了,目前數據源當中大概有60個圖片的URL地址,而根據ListView的工作原理,顯然不可能爲每張圖片都單獨分配一個ImageView控件,ImageView控件的個數其實就比一屏能顯示的圖片數量稍微多一點而已,移出屏幕的ImageView控件會進入到RecycleBin當中,而新進入屏幕的元素則會從RecycleBin中獲取ImageView控件。
那麼,每當有新的元素進入界面時就會回調getView()方法,而在getView()方法中會開啓異步請求從網絡上獲取圖片,注意網絡操作都是比較耗時的,也就是說當我們快速滑動ListView的時候就很有可能出現這樣一種情況,某一個位置上的元素進入屏幕後開始從網絡上請求圖片,但是還沒等圖片下載完成,它就又被移出了屏幕。這種情況下會產生什麼樣的現象呢?根據ListView的工作原理,被移出屏幕的控件將會很快被新進入屏幕的元素重新利用起來,而如果在這個時候剛好前面發起的圖片請求有了響應,就會將剛纔位置上的圖片顯示到當前位置上,因爲雖然它們位置不同,但都是共用的同一個ImageView實例,這樣就出現了圖片亂序的情況。
但是還沒完,新進入屏幕的元素它也會發起一條網絡請求來獲取當前位置的圖片,等到圖片下載完的時候會設置到同樣的ImageView上面,因此就會出現先顯示一張圖片,然後又變成了另外一張圖片的情況,那麼剛纔我們看到的圖片會自動變來變去的情況也就得到了解釋。
問題原因已經分析出來了,但是這個問題該怎麼解決呢?說實話,ListView異步加載圖片的問題並沒有什麼標準的解決方案,很多人都有自己的一套解決思路,這裏我準備給大家講解三種比較經典的解決辦法,大家通過任何一種都可以解決這個問題,但是我們每多學習一種思路,水平就能夠更進一步的提高。
解決方案一 使用findViewWithTag
findViewWithTag算是一種比較簡單易懂的解決方案,其實早在 Android照片牆應用實現,再多的圖片也不怕崩潰 這篇文章當中,我就採用了findViewWithTag來避免圖片出現亂序的情況。那麼這裏我們先來看看怎麼通過修改代碼把這個問題解決掉,然後再研究一下findViewWithTag的工作原理。
使用findViewWithTag並不需要修改太多的代碼,只需要改動ImageAdapter這一個類就可以了,如下所示:
-
-
-
-
-
public class ImageAdapter extends ArrayAdapter<String> {
-
-
private ListView mListView;
-
-
......
-
-
@Override
-
public View getView(int position, View convertView, ViewGroup parent) {
-
if (mListView == null) {
-
mListView = (ListView) parent;
-
}
-
String url = getItem(position);
-
View view;
-
if (convertView == null) {
-
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
-
} else {
-
view = convertView;
-
}
-
ImageView image = (ImageView) view.findViewById(R.id.image);
-
image.setImageResource(R.drawable.empty_photo);
-
image.setTag(url);
-
BitmapDrawable drawable = getBitmapFromMemoryCache(url);
-
if (drawable != null) {
-
image.setImageDrawable(drawable);
-
} else {
-
BitmapWorkerTask task = new BitmapWorkerTask();
-
task.execute(url);
-
}
-
return view;
-
}
-
-
......
-
-
-
-
-
-
-
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
-
-
String imageUrl;
-
-
@Override
-
protected BitmapDrawable doInBackground(String... params) {
-
imageUrl = params[0];
-
-
Bitmap bitmap = downloadBitmap(imageUrl);
-
BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
-
addBitmapToMemoryCache(imageUrl, drawable);
-
return drawable;
-
}
-
-
@Override
-
protected void onPostExecute(BitmapDrawable drawable) {
-
ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);
-
if (imageView != null && drawable != null) {
-
imageView.setImageDrawable(drawable);
-
}
-
}
-
-
......
-
-
}
-
-
}
改動的地方就只有這麼多,那麼我們來分析一下。由於使用findViewWithTag必須要有ListView的實例才行,那麼我們在Adapter中怎樣才能拿到ListView的實例呢?其實如果你仔細通讀了上一篇文章就能知道,getView()方法中傳入的第三個參數其實就是ListView的實例,那麼這裏我們定義一個全局變量mListView,然後在getView()方法中判斷它是否爲空,如果爲空就把parent這個參數賦值給它。
另外在getView()方法中我們還做了一個操作,就是調用了ImageView的setTag()方法,並把當前位置圖片的URL地址作爲參數傳了進去,這個是爲後續的findViewWithTag()方法做準備。
最後,我們修改了BitmapWorkerTask的構造函數,這裏不再通過構造函數把ImageView的實例傳進去了,而是在onPostExecute()方法當中通過ListView的findVIewWithTag()方法來去獲取ImageView控件的實例。獲取到控件實例後判斷下是否爲空,如果不爲空就讓圖片顯示到控件上。
這裏我們可以嘗試分析一下findViewWithTag的工作原理,其實顧名思義,這個方法就是通過Tag的名字來獲取具備該Tag名的控件,我們先要調用控件的setTag()方法來給控件設置一個Tag,然後再調用ListView的findViewWithTag()方法使用相同的Tag名來找回控件。
那麼爲什麼用了findViewWithTag()方法之後,圖片就不會再出現亂序情況了呢?其實原因很簡單,由於ListView中的ImageView控件都是重用的,移出屏幕的控件很快會被進入屏幕的圖片重新利用起來,那麼getView()方法就會再次得到執行,而在getView()方法中會爲這個ImageView控件設置新的Tag,這樣老的Tag就會被覆蓋掉,於是這時再調用findVIewWithTag()方法並傳入老的Tag,就只能得到null了,而我們判斷只有ImageView不等於null的時候纔會設置圖片,這樣圖片亂序的問題也就不存在了。
這是第一種解決方案。
解決方案二 使用弱引用關聯
雖然這裏我給這種解決方案起名叫弱引用關聯,但實際上弱引用只是輔助手段而已,最主要的還是關聯,這種解決方案的本質是要讓ImageView和BitmapWorkerTask之間建立一個雙向關聯,互相持有對方的引用,再通過適當的邏輯判斷來解決圖片亂序問題,然後爲了防止出現內存泄漏的情況,雙向關聯要使用弱引用的方式建立。相比於第一種解決方案,第二種解決方案要明顯複雜不少,但在性能和效率方面都會有更好的表現。
我們仍然只需要改動ImageAdapter中的代碼,但這次改動的地方比較多,所以我就把ImageAdapter中的全部代碼都貼出來了,如下所示:
-
-
-
-
-
public class ImageAdapter extends ArrayAdapter<String> {
-
-
private ListView mListView;
-
-
private Bitmap mLoadingBitmap;
-
-
-
-
-
private LruCache<String, BitmapDrawable> mMemoryCache;
-
-
public ImageAdapter(Context context, int resource, String[] objects) {
-
super(context, resource, objects);
-
mLoadingBitmap = BitmapFactory.decodeResource(context.getResources(),
-
R.drawable.empty_photo);
-
-
int maxMemory = (int) Runtime.getRuntime().maxMemory();
-
int cacheSize = maxMemory / 8;
-
mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
-
@Override
-
protected int sizeOf(String key, BitmapDrawable drawable) {
-
return drawable.getBitmap().getByteCount();
-
}
-
};
-
}
-
-
@Override
-
public View getView(int position, View convertView, ViewGroup parent) {
-
if (mListView == null) {
-
mListView = (ListView) parent;
-
}
-
String url = getItem(position);
-
View view;
-
if (convertView == null) {
-
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
-
} else {
-
view = convertView;
-
}
-
ImageView image = (ImageView) view.findViewById(R.id.image);
-
BitmapDrawable drawable = getBitmapFromMemoryCache(url);
-
if (drawable != null) {
-
image.setImageDrawable(drawable);
-
} else if (cancelPotentialWork(url, image)) {
-
BitmapWorkerTask task = new BitmapWorkerTask(image);
-
AsyncDrawable asyncDrawable = new AsyncDrawable(getContext()
-
.getResources(), mLoadingBitmap, task);
-
image.setImageDrawable(asyncDrawable);
-
task.execute(url);
-
}
-
return view;
-
}
-
-
-
-
-
class AsyncDrawable extends BitmapDrawable {
-
-
private 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();
-
}
-
-
}
-
-
-
-
-
private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
-
if (imageView != null) {
-
Drawable drawable = imageView.getDrawable();
-
if (drawable instanceof AsyncDrawable) {
-
AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
-
return asyncDrawable.getBitmapWorkerTask();
-
}
-
}
-
return null;
-
}
-
-
-
-
-
-
public boolean cancelPotentialWork(String url, ImageView imageView) {
-
BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
-
if (bitmapWorkerTask != null) {
-
String imageUrl = bitmapWorkerTask.imageUrl;
-
if (imageUrl == null || !imageUrl.equals(url)) {
-
bitmapWorkerTask.cancel(true);
-
} else {
-
return false;
-
}
-
}
-
return true;
-
}
-
-
-
-
-
-
-
-
-
-
public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {
-
if (getBitmapFromMemoryCache(key) == null) {
-
mMemoryCache.put(key, drawable);
-
}
-
}
-
-
-
-
-
-
-
-
-
public BitmapDrawable getBitmapFromMemoryCache(String key) {
-
return mMemoryCache.get(key);
-
}
-
-
-
-
-
-
-
class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {
-
-
String imageUrl;
-
-
private WeakReference<ImageView> imageViewReference;
-
-
public BitmapWorkerTask(ImageView imageView) {
-
imageViewReference = new WeakReference<ImageView>(imageView);
-
}
-
-
@Override
-
protected BitmapDrawable doInBackground(String... params) {
-
imageUrl = params[0];
-
-
Bitmap bitmap = downloadBitmap(imageUrl);
-
BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);
-
addBitmapToMemoryCache(imageUrl, drawable);
-
return drawable;
-
}
-
-
@Override
-
protected void onPostExecute(BitmapDrawable drawable) {
-
ImageView imageView = getAttachedImageView();
-
if (imageView != null && drawable != null) {
-
imageView.setImageDrawable(drawable);
-
}
-
}
-
-
-
-
-
private ImageView getAttachedImageView() {
-
ImageView imageView = imageViewReference.get();
-
BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
-
if (this == bitmapWorkerTask) {
-
return imageView;
-
}
-
return null;
-
}
-
-
-
-
-
-
-
-
-
private Bitmap downloadBitmap(String imageUrl) {
-
Bitmap bitmap = null;
-
HttpURLConnection con = null;
-
try {
-
URL url = new URL(imageUrl);
-
con = (HttpURLConnection) url.openConnection();
-
con.setConnectTimeout(5 * 1000);
-
con.setReadTimeout(10 * 1000);
-
bitmap = BitmapFactory.decodeStream(con.getInputStream());
-
} catch (Exception e) {
-
e.printStackTrace();
-
} finally {
-
if (con != null) {
-
con.disconnect();
-
}
-
}
-
return bitmap;
-
}
-
-
}
-
-
}
那麼我們一點點開始解析。首先剛纔說到的,ImageView和BitmapWorkerTask之間要建立一個雙向的弱引用關聯,上述代碼中已經建立好了。ImageView中可以獲取到它所對應的BitmapWorkerTask,而BitmapWorkerTask也可以獲取到它所對應的ImageView。
下面來看一下這個雙向弱引用關聯是怎麼建立的。BitmapWorkerTask指向ImageView的弱引用關聯比較簡單,就是在BitmapWorkerTask中加入一個構造函數,並在構造函數中要求傳入ImageView這個參數。不過我們不再直接持有ImageView的引用,而是使用WeakReference對ImageView進行了一層包裝,這樣就OK了。
但是ImageView指向BitmapWorkerTask的弱引用關聯就沒這麼容易了,因爲我們很難將BitmapWorkerTask的一個弱引用直接設置到ImageView當中。這該怎麼辦呢?這裏使用了一個比較巧的方法,就是藉助自定義Drawable的方式來實現。可以看到,我們自定義了一個AsyncDrawable類並讓它繼承自BitmapDrawable,然後重寫了AsyncDrawable的構造函數,在構造函數中要求把BitmapWorkerTask傳入,然後在這裏給它包裝了一層弱引用。那麼現在AsyncDrawable指向BitmapWorkerTask的關聯已經有了,但是ImageView指向BitmapWorkerTask的關聯還不存在,怎麼辦呢?很簡單,讓ImageView和AsyncDrawable再關聯一下就可以了。可以看到,在getView()方法當中,我們調用了ImageView的setImageDrawable()方法把AsyncDrawable設置了進去,那麼ImageView就可以通過getDrawable()方法獲取到和它關聯的AsyncDrawable,然後再借助AsyncDrawable就可以獲取到BitmapWorkerTask了。這樣ImageView指向BitmapWorkerTask的弱引用關聯也成功建立。
現在雙向弱引用的關聯已經建立好了,接下來就是邏輯判斷的工作了。那麼怎樣通過邏輯判斷來避免圖片出現亂序的情況呢?這裏我們引入了兩個方法,一個是getBitmapWorkerTask()方法,這個方法可以根據傳入的ImageView來獲取到它對應的BitmapWorkerTask,內部的邏輯就是先獲取ImageView對應的AsyncDrawable,再獲取AsyncDrawable對應的BitmapWorkerTask。另一個是getAttachedImageView()方法,這個方法會獲取當前BitmapWorkerTask所關聯的ImageView,然後調用getBitmapWorkerTask()方法來獲取該ImageView所對應的BitmapWorkerTask,最後判斷,如果獲取到的BitmapWorkerTask等於this,也就是當前的BitmapWorkerTask,那麼就將ImageView返回,否則就返回null。最後,在onPostExecute()方法當中,只需要使用getAttachedImageView()方法獲取到的ImageView來顯示圖片就可以了。
那麼爲什麼做了這個邏輯判斷之後,圖片亂序的問題就可以得到解決呢?其實最主要的奧祕就是在getAttachedImageView()方法當中,它會使用當前BitmapWorkerTask所關聯的ImageView來反向獲取這個ImageView所關聯的BitmapWorkerTask,然後用這兩個BitmapWorkerTask做對比,如果發現是同一個BitmapWorkerTask纔會返回ImageView,否則就返回null。那麼什麼情況下這兩個BitmapWorkerTask纔會不同呢?比如說某個圖片被移出了屏幕,它的ImageView被另外一個新進入屏幕的圖片重用了,那麼就會給這個ImageView關聯一個新的BitmapWorkerTask,這種情況下,上一個BitmapWorkerTask和新的BitmapWorkerTask肯定就不相等了,這時getAttachedImageView()方法會返回null,而我們又判斷ImageView等於null的話是不會設置圖片的,因此就不會出現圖片亂序的情況了。
除此之外還有另外一個方法非常值得大家注意,就是cancelPotentialWork()方法,這個方法可以大大提高整個ListView圖片加載的工作效率。這個方法接收兩個參數,一個圖片的url,一個ImageView。看一下它的內部邏輯,首先它也是調用了getBitmapWorkerTask()方法來獲取傳入的ImageView所對應的BitmapWorkerTask,接下來拿BitmapWorkerTask中的imageUrl和傳入的url做比較,如果兩個url不等的話就調用BitmapWorkerTask的cancel()方法,然後返回true,如果兩個url相等的話就返回false。
那麼這段邏輯是什麼意思呢?其實並不複雜,兩個url做比對時,如果發現是相同的,說明請求的是同一張圖片,那麼直接返回false,這樣就不會再去啓動BitmapWorkerTask來請求圖片,而如果兩個url不相同,說明這個ImageView被另外一張圖片重新利用了,這個時候就調用了BitmapWorkerTask的cancel()方法把之前的請求取消掉,然後重新啓動BitmapWorkerTask來去請求新圖片。有了這個操作保護之後,就可以把一些已經移出屏幕的無效的圖片請求過濾掉,從而整體提升ListView加載圖片的工作效率。
這是第二種解決方案。
解決方案三 使用NetworkImageView
前面兩種解決方案都需要我們自己去做額外的邏輯處理,因爲ImageView本身是不能自動解決這個問題的,但是如果我們使用NetworkImageView這個控件的話就非常簡單了,它自身就已經考慮到了這個問題,我們直接使用它就可以了,不用做任何額外的處理也不會出現圖片亂序的情況。
NetworkImageView是Volley當中提供的控件,對於這個控件我之前專門寫過一篇博客來講解,還不熟悉這個控件的朋友可以先去閱讀Android Volley完全解析(二),使用Volley加載網絡圖片 。
下面我們看一下如何用NetworkImageView來解決這個問題,首先需要修改一下image_item.xml文件,因爲我們已經不再使用ImageView控件了,代碼如下所示:
-
<?xml version="1.0" encoding="utf-8"?>
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<com.android.volley.toolbox.NetworkImageView
-
android:id="@+id/image"
-
android:layout_width="match_parent"
-
android:layout_height="120dp"
-
android:src="@drawable/empty_photo"
-
android:scaleType="fitXY"/>
-
-
</LinearLayout>
很簡單,只是把ImageView替換成了NetworkImageView。然後修改ImageAdapter中的代碼,如下所示:
-
-
-
-
public class ImageAdapter extends ArrayAdapter<String> {
-
-
ImageLoader mImageLoader;
-
-
public ImageAdapter(Context context, int resource, String[] objects) {
-
super(context, resource, objects);
-
RequestQueue queue = Volley.newRequestQueue(context);
-
mImageLoader = new ImageLoader(queue, new BitmapCache());
-
}
-
-
@Override
-
public View getView(int position, View convertView, ViewGroup parent) {
-
String url = getItem(position);
-
View view;
-
if (convertView == null) {
-
view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);
-
} else {
-
view = convertView;
-
}
-
NetworkImageView image = (NetworkImageView) view.findViewById(R.id.image);
-
image.setDefaultImageResId(R.drawable.empty_photo);
-
image.setErrorImageResId(R.drawable.empty_photo);
-
image.setImageUrl(url, mImageLoader);
-
return view;
-
}
-
-
-
-
-
public class BitmapCache implements ImageCache {
-
-
private LruCache<String, Bitmap> mCache;
-
-
public BitmapCache() {
-
-
int maxMemory = (int) Runtime.getRuntime().maxMemory();
-
int cacheSize = maxMemory / 8;
-
mCache = new LruCache<String, Bitmap>(cacheSize) {
-
@Override
-
protected int sizeOf(String key, Bitmap bitmap) {
-
return bitmap.getRowBytes() * bitmap.getHeight();
-
}
-
};
-
}
-
-
@Override
-
public Bitmap getBitmap(String url) {
-
return mCache.get(url);
-
}
-
-
@Override
-
public void putBitmap(String url, Bitmap bitmap) {
-
mCache.put(url, bitmap);
-
}
-
-
}
-
-
}
沒錯,就是這麼簡單,一共60行左右的代碼搞定一切!我們不需要自己再去寫一個BitmapWorkerTask來處理圖片的下載和顯示,也不需要自己再去管理LruCache的邏輯,一切NetworkImageView都幫我們做好了。至於上面的代碼我就不再做解釋了,因爲實在是太簡單了。
那麼當然了,雖然現在沒有做任何額外的邏輯處理,但是也根本不會出現圖片亂序的情況,因爲NetworkImageView在內部都幫我們處理掉了。不過大家可能都很好奇,NetworkImageView到底是如何做到的呢?那麼就讓我們來分析一下它的源碼吧。
NetworkImageView中開始加載圖片的代碼是setImageUrl()方法,源碼分析就從這裏開始吧,如下所示:
-
-
-
-
-
-
-
-
-
-
-
-
-
public void setImageUrl(String url, ImageLoader imageLoader) {
-
mUrl = url;
-
mImageLoader = imageLoader;
-
-
loadImageIfNecessary(false);
-
}
setImageUrl()方法中並沒有幾行代碼,讓人值得留意的是loadImageIfNecessary()這個方法,看上去具體加載圖片的邏輯就是在這裏進行的,那麼我們就跟進去瞧一瞧:
-
-
-
-
-
private void loadImageIfNecessary(final boolean isInLayoutPass) {
-
int width = getWidth();
-
int height = getHeight();
-
-
boolean isFullyWrapContent = getLayoutParams() != null
-
&& getLayoutParams().height == LayoutParams.WRAP_CONTENT
-
&& getLayoutParams().width == LayoutParams.WRAP_CONTENT;
-
-
-
if (width == 0 && height == 0 && !isFullyWrapContent) {
-
return;
-
}
-
-
-
-
if (TextUtils.isEmpty(mUrl)) {
-
if (mImageContainer != null) {
-
mImageContainer.cancelRequest();
-
mImageContainer = null;
-
}
-
setDefaultImageOrNull();
-
return;
-
}
-
-
-
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
-
if (mImageContainer.getRequestUrl().equals(mUrl)) {
-
-
return;
-
} else {
-
-
mImageContainer.cancelRequest();
-
setDefaultImageOrNull();
-
}
-
}
-
-
-
-
ImageContainer newContainer = mImageLoader.get(mUrl,
-
new ImageListener() {
-
@Override
-
public void onErrorResponse(VolleyError error) {
-
if (mErrorImageId != 0) {
-
setImageResource(mErrorImageId);
-
}
-
}
-
-
@Override
-
public void onResponse(final ImageContainer response, boolean isImmediate) {
-
-
-
-
-
if (isImmediate && isInLayoutPass) {
-
post(new Runnable() {
-
@Override
-
public void run() {
-
onResponse(response, false);
-
}
-
});
-
return;
-
}
-
-
if (response.getBitmap() != null) {
-
setImageBitmap(response.getBitmap());
-
} else if (mDefaultImageId != 0) {
-
setImageResource(mDefaultImageId);
-
}
-
}
-
});
-
-
-
mImageContainer = newContainer;
-
}
這裏在第43行調用了ImageLoader的get()方法來去請求圖片,get()方法會返回一個ImageContainer對象,這個對象封裝了圖片請求地址、Bitmap等數據,每個NetworkImageView中都會對應一個ImageContainer。然後在第31行我們看到,這裏從ImageContainer對象中獲取封裝的圖片請求地址,並拿來和當前的請求地址做對比,如果相同的話說明這是一條重複的請求,就直接return掉,如果不同的話就調用cancelRequest()方法將請求取消掉,然後將圖片設置爲默認圖片並重新發起請求。
那麼解決圖片亂序最核心的邏輯就在這裏了,其實NetworkImageView的解決思路還是比較簡單的,就是如果這個控件已經被移出了屏幕且被重新利用了,那麼就把之前的請求取消掉,僅此而已。
而我們都知道,在通常情況下,僅僅這麼處理可能是解決不了問題的,因爲Java的線程無法保證一定可以中斷,即使像第二種解決方案裏使用的BitmapWorkerTask的cancel()方法,也不能保證一定可以把請求取消掉,所以還需要使用弱引用關聯的處理方式。但是在NetworkImageView當中就可以這麼任性,僅僅調用cancelRequest()方法把請求取消掉就可以了,這主要是得益於Volley的出色設計。由於Volley在網絡方面的封裝非常優秀,它可以保證,只要是取消掉的請求,就絕對不會進行回調,既然不會回調,那麼也就不會回到NetworkImageView當中,自然也就不會出現亂序的情況了。
需要注意的是,Volley只是保證取消掉的請求不會進行回調而已,但並沒有說可以中斷任何請求。由此可見即使是Volley也無法做到中斷一個正在執行的線程,如果有一個線程正在執行,Volley只會保證在它執行完之後不會進行回調,但在調用者看來,就好像是這個請求就被取消掉了一樣。
那麼這裏我們只分析與圖片亂序相關部分的源碼,如果你想了解關於Volley更多的源碼,可以參考這篇文章 Android Volley完全解析(四),帶你從源碼的角度理解Volley 。
這是第三種解決方案。
好了,關於ListView異步加載圖片亂序的問題今天我們就討論到這裏,如果你把三種解決方案都理解清楚的話,那麼對於這個問題研究的就算比較透徹了。下一篇文章仍然是ListView主題,我們將學習一下如何對ListView控件進行一些功能擴展,感興趣的朋友請參考 Android
ListView功能擴展,實現高性能的瀑布流佈局 。
-----------------------------------------------------------------------------------------
個人理解與總結:
異步加載圖片亂序的原因:ImageView與AsyncTask中的請求任務沒有對應起來。
解決方法:
1.使用findViewWithTag(屬於View的方法)
在AsyncTask請求執行完後【onPostExecute】,把其處理的圖片的URL地址當成tag去找到其對應的view對象。
例如:onPostExecute { mListView.findViewWithTag(imageUrl);}
2.使用弱引用關聯
通過AsyncDrawable【extends BitmapDrawable】這個中間類,把ImageView與AsyncTask聯繫起來【其中ImageView實例與AsynTask實例都是弱引用】
例如:imageViewReference = new WeakReference<ImageView>(imageView);
bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
3.使用NetworkImageView
原理:NetworkImageView是Volley當中提供的控件,Volley內部已經幫它解決這個問題,核心思路是:把ImageView與AsyncTask聯繫起來,判斷每個ImageView是否與AsyncTask的ImageView相等,不相等則取消請求