在上篇文章的例子中,我們使用了一張圖片和一個文本作為每一行的數據,發現效果已經完全達到了,而且沒出現什麼問題。但如果我們將Item的數量調大,比如調到
1000、10000、100000條數據,這個時候當你打開ListView的時候,肯定會不禁感慨“什麼鬼,卡機了?!”等了好幾秒鐘,ListView才顯示出來,用戶體驗非常不好,特別是如果是要上市的項目,後果很嚴重!所以針對ListView的優化至關重要。
ListView內存調用機制的原理
ListView消耗內存的主要地方就在於每一個ListItem的繪制,之前說過了,ListView的每一項的繪制的地方就在於Adapter的getView()方法中,getView()方法的返回值是一個View,這個View就是每一行的視圖,然後ListView再將其展示出來,那如果我們像之前那種寫法,不做任何修改,結果會是怎樣?
我們通過上一篇的例子做個測試,將行數調到100,在getView中打印一句Log看看:
Log打印結果:
可以看到,初始化ListView時getView運行了9次,而界面上剛好也僅顯示到第9條數據,也就是只有屏幕范圍內顯示的才會調用getView(),另外,可以看到它們的convertView都會null,然後我們再將界面稍微往下拖動,如圖:
再看Logcat:
注意到,第九項數據從底部開始進入界面,它的getView也調用了一遍,convertView依然為null,這是因為頂部的第一項數據還未完全脫離屏幕范圍外,也就是第一項的視圖還未進入Android的Recycler中,還不能被重用,我們再繼續往下滑:
Logcat:
發現第10項的convertView不為空了!這是因為頂部的第一項數據已經完全離開了屏幕,所以Android會將它的convertView“推”進RecycleView中,然後第10行出現的時候,getView方法的convertView參數正是第一項存放在Recycler中的視圖。如下圖:
ConvertView的重用
了解了ListView的getView原理,我們就可以開始對它進行優化,上面提到了已經離開屏幕的convertView會被壓入Recycler中,那我們可以在
每次getView的一開始先判斷convertView是否為空,不是為空的話就直接用那個已經存在的convertView來直接進行操作,代碼如下:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
Log.d("getView--->", convertView+"--position:"+position);
if(convertView==null){
convertView = inflater.inflate(R.layout.list_item, null);
}
TextView text = (TextView)convertView.findViewById(R.id.list_item_text);
ImageView image = (ImageView)convertView.findViewById(R.id.list_item_image);
text.setText(data.get(position).get("text").toString());
image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
return convertView;
}
運行滑動到如下圖:
打印結果:
注意我圈起來的兩個地方,兩個地址一模一樣!所以我們成功重用了Recycle中緩存的視圖,這樣可以有效優化ListView的內存消耗(試想一下,100000個視圖我來來回回只用那10個convertView,能不減少內存開銷嗎?)
ViewHolder的使用
以上只是利用convertView的重用來做到優化效果,但是注意到還是有存在問題,每個視圖裡面有一個text和一個image,每次都要通過findViewByID來找到它們,這也是一件龐大的工程...那既然我們可以重用convertView,
那可不可以將這兩個子控件也緩存起來呢?
我們可以通過自定義一個
ViewHolder來,來進行子控件視圖的緩存,以達到更佳的優化效果:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
Log.d("getView--->", convertView+"--position:"+position);
ViewHolder holder = null;
if(convertView==null){
convertView = inflater.inflate(R.layout.list_item, null);
holder = new ViewHolder();
holder.text = (TextView)convertView.findViewById(R.id.list_item_text);
holder.image = (ImageView)convertView.findViewById(R.id.list_item_image);
convertView.setTag(holder);
}
else{
holder = (ViewHolder)convertView.getTag();
}
holder.text.setText(data.get(position).get("text").toString());
holder.image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
return convertView;
}
public static class ViewHolder{
public TextView text;
public ImageView image;
}
代碼分析:首先自定義了一個ViewHolder類,這裡設置為static,這樣ViewHolder無論new多少次都是指向同一個內存空間,在ViewHolder類中添加了兩個成員變量,分別對應我們的子控件。每次getView的時候,同樣先判斷ViewHolder對象是否為空,如果為空,就實例化一個ViewHolder對象,並將convertView通過findViewById找到的子控件賦給holder,再將holder通過setTag()方法設置在convertView上,之後重用的時候可以通過convertView的getTag()來獲得。其實ViewHolder相當於我們子控件的一個封裝類而已,通過這樣實現不用每次都去findViewById查找子控件,每次做的事情只是重用之前的視圖和控件設置一下數據,達到優化的目的。
ListView多種子布局的重用方式
上面的操作雖然已經對ListView進行了一些優化,但是依然存在問題,如果所有的ListItem的布局並不是都一樣(例如類似微信朋友圈,一些是圖片,一些是文字,一些是小視頻等等),就不能全部都用一樣的ViewHolder或者convertView來處理了,因為重用的布局不一定適合新出現的ListItem,ListView中提供了另外兩個方法:
getItemViewType(int position) 【根據下標返回當前視圖的類型】
getViewTypeCount() 【返回類型的種類數】
代碼如下:
public class ListViewAdapter extends SimpleAdapter{
private Context context;
private List
代碼分析:創建另外一個ViewHolder,用於加載和重用另外一種布局,其實就是在原來的基礎上,為每個操作都套上一層switch判斷,然後根據type的類型來分別設置兩種布局。
ListView異步加載亂序問題
出現亂序的原因
上面的操作都是屬於同步加載每一行,所以不會出現什麼問題。但如果當我們是網絡異步加載每一行的圖片時,就會出現數據紊亂,前文已經說了,Android為ListView進行的Reycler的處理,減少內存開銷,那麼,每當有新的元素進入界面時就會回調getView()方法,而在getView()方法中會開啟異步請求從網絡上獲取圖片,注意網絡操作都是比較耗時的,也就是說當我們
快速滑動ListView的時候就很有可能出現這樣一種情況,某一個位置上的元素進入屏幕後開始從網絡上請求圖片,但是還沒等圖片下載完成,它就又被移出了屏幕。這種情況下會產生什麼樣的現象呢?
根據ListView的工作原理,被移出屏幕的控件將會很快被新進入屏幕的元素重新利用起來,而如果在這個時候剛好前面發起的圖片請求有了響應,就會將剛才位置上的圖片顯示到當前位置上,因為雖然它們位置不同,但都是共用的同一個ImageView實例,這樣就出現了圖片亂序的情況。但是還沒完,新進入屏幕的元素它也會發起一條網絡請求來獲取當前位置的圖片,等到圖片下載完的時候會設置到同樣的ImageView上面,因此就會出現先顯示一張圖片,然後又變成了另外一張圖片的情況。
如何解決亂序問題?
總之,以上講述了ListView的多種優化方式,但是並不是萬能,也僅僅只是起到了一部分效果,真實開發中還要視情況而定,比如如果是多圖片,首先需要將圖片壓縮,並且不要再getView中做過多的耗時操作!希望本文對大家理解ListView的優化有所幫助。