轉載請注明出處:http://blog.csdn.net/guolin_blog/article/details/45586553
在Android所有系統自帶的控件當中,ListView這個控件算是用法比較復雜的了,關鍵是用法復雜也就算了,它還經常會出現一些稀奇古怪的問題,讓人非常頭疼。比如說在ListView中加載圖片,如果是同步加載圖片倒還好,但是一旦使用異步加載圖片那么問題就來了,這個問題我相信很多Android開發者都曾經遇到過,就是異步加載圖片會出現錯位亂序的情況。遇到這個問題時,不少人在網上搜索找到了相應的解決方案,但是真正深入理解這個問題出現的原因并對癥解決的人恐怕還并不是很多。那么今天我們就來具體深入分析一下ListView異步加載圖片出現亂序問題的原因,以及怎么樣對癥下藥去解決它。
本篇文章的原理基礎建立在上一篇文章之上,如果你對ListView的工作原理還不夠了解的話,建議先去閱讀 Android ListView工作原理完全解析,帶你從源碼的角度徹底理解?。
問題重現
要想解決問題首先我們要把問題重現出來,這里只需要搭建一個最基本的ListView項目,然后在ListView中去異步請求圖片并顯示,問題就能夠得以重現了,那么我們就新建一個ListViewTest項目。
項目建好之后第一個要解決的是數據源的問題,由于ListView中需要從網絡上請求圖片,那么我就提前準備好了許多張圖片,將它們上傳到了我的CSDN相冊當中,然后新建一個Images類,將所有相冊中圖片的URL地址都配置進去就可以了,代碼如下所示:
[java] view plaincopy
?????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中的代碼,如下所示:
[html] view plaincopy
<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布局,加入如下代碼:
[html] view plaincopy
<?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的適配器,代碼如下所示:
[java] view plaincopy
???????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中的代碼,如下所示:
[java] view plaincopy
?????????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這一個類就可以了,如下所示:
[java] view plaincopy
???????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中的全部代碼都貼出來了,如下所示:
[java] view plaincopy
???????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控件了,代碼如下所示:
[html] view plaincopy
<?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中的代碼,如下所示:
[java] view plaincopy
?????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()方法,源碼分析就從這里開始吧,如下所示:
[java] view plaincopy
?????????????public?void?setImageUrl(String?url,?ImageLoader?imageLoader)?{??????mUrl?=?url;??????mImageLoader?=?imageLoader;????????????loadImageIfNecessary(false);??}??
setImageUrl()方法中并沒有幾行代碼,讓人值得留意的是loadImageIfNecessary()這個方法,看上去具體加載圖片的邏輯就是在這里進行的,那么我們就跟進去瞧一瞧:
[java] view plaincopy
?????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功能擴展,實現高性能的瀑布流布局?。
第一時間獲得博客更新提醒,以及更多技術信息分享,歡迎關注我的微信公眾號,掃一掃下方二維碼或搜索微信號guolin_blog,即可關注。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的Android ListView异步加载图片乱序问题,原因分析及解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。