ListView複用和優化詳解
前言
我們每一個Android開發人員對ListView的使用肯定是很熟悉的,然而多少人能真正的懂ListView的快取機制呢,說白了就是ListView為了提高效率,而內部實現的一種優化,犧牲一點記憶體。而這種優化就需要複用ItemView(也就是item對應的View).那麼下面樓主來對ListView和RecyclerView的item複用問題做一個深入的講解
先來一張大家學習的時候都遇到過的圖
看不懂也沒啥事,可以接著往下看,先有一個直觀的認識
首先來解答幾個問題
1.ListView為什麼會存在Item複用問題
答:ListView內部為了優化而建立的複用機制,在下面方法中第二個引數就是ListView傳遞給你,讓你進行復用的View.如果你不想複用listview傳遞給你的View,那你每次都需要建立一個新的View進行返回,這樣子是肯定不會出現複用問題的,但是效能卻是很消耗的。
public View getView(int posion, View itemView, ViewGroup viewGroup)
{
return null;
}
2.為什麼上述的getView方法中第二個引數convertView有時候為null呢
因為ListView預設快取一頁的View,什麼叫一頁,也就是你當前listview介面上有幾個Item可以顯示,listview就快取幾個.
當現實第一頁的時候,由於沒有一個Item被建立,所以第一頁的Item的getView方法中的第二個引數都是為null的
假如listview只能最多顯示8條記錄,則第一頁顯示的時候listview內部快取了這8個itemView.當第九條記錄出現在視野中的時候,listview就會在呼叫getView方法的時候在第二個引數處傳入之前用過的itemView。
3.為什麼需要ViewHolder呢?這個又是幹嘛的
為什麼需要 上述我們談到itemView的複用是為了效能,那麼ViewHolder同樣也是為了提高效能.我們都知道我們要顯示列表資料.就要在getView方法中拿到對應下標的資料然後對itemView中的控制元件進行設值,所以我們需要用到findViewById(int id)方法來找到控制元件,並且強轉成我們想要的型別之後,然後設定資料,而findViewById(int id)方法在列表滾動的時候頻繁呼叫getView方法的時候也是一個比較消耗效能的操作.所以ViewHolder來了
ViewHolder是幹嘛的 為了在列表滾動的時候,頻繁呼叫getView方法的時候儘量提高效能.我們可以使用一個普通類,這個類通常就起名字為ViewHolder了,當建立itemView的時候,我們也把裡面要用到的控制元件也找到,然後放在ViewHolder類中,然後再通過itemView.setTag(Object ob)方法實現一個itemView和一個ViewHolder進行繫結.
經過上述的操作,如果在getView方法中傳入了複用的itemView,那麼我們可以毫不客氣地從裡面拿出這個itemView對應的ViewHolder,從而避免了去呼叫多個findViewById(int id)去找到控制元件並設值.因為之前你把找到的控制元件都放在了ViewHolder中
擴充套件 如果你的itemView中只有一個控制元件需要顯示,那麼ViewHolder就不需要了,你可以直接把這個控制元件和itemView進行關聯,也就是你需要深刻理解ViewHolder的作用,它是為了把你找到的多個控制元件和itemView關聯。所以當你只有一個控制元件的時候,這個ViewHolder就不需要啦
itemView.setTag(Object ob)方法直接把這個控制元件設定上去就可以啦,複用的時候直接拿出來
那麼主要的問題解答完了,總得寫點程式碼來讓大家更深刻的體會一下.
博主幾乎會重現我們開發中的常見問題,來對應的講解
getView方法在什麼時候呼叫
回答:在每一個item從不可見變為可見的時候
動手實踐
實現一個簡單的列表,使用ListView控制元件,並且Item中有複選框
Activity的xml檔案
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.xiaojinzi.listdemo.MainActivity">
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
就是一個列表控制元件
ListView的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="wrap_content"
android:gravity="center_vertical"
android:padding="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:textSize="18sp"
android:text="hello" />
<CheckBox
android:id="@+id/cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp" />
</LinearLayout>
ListView的介面卡
public class ListViewAdapter extends BaseAdapter {
private List<String> listViewData;
private Context mContext;
public ListViewAdapter(List<String> listViewData, Context mContext) {
this.listViewData = listViewData;
this.mContext = mContext;
}
@Override
public int getCount() {
return listViewData.size();
}
@Override
public Object getItem(int i) {
return listViewData.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
View item = View.inflate(mContext, R.layout.listview_item, null);
return item;
}
}
這程式碼非常簡單,不再囉嗦
Activity程式碼
public class MainActivity extends AppCompatActivity {
private ListView lv;
private BaseAdapter listViewAdapter;
private List<String> listViewData = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < 50; i++) {
listViewData.add("text" + i);
}
lv = (ListView) findViewById(R.id.lv);
listViewAdapter = new ListViewAdapter(listViewData, this);
lv.setAdapter(listViewAdapter);
}
}
程式碼貼完了,都是非常的簡單,先看下執行效果
這裡很需要你們關注的是我們的介面卡中的getView中的程式碼
public View getView(int i, View view, ViewGroup viewGroup) {
View item = View.inflate(mContext, R.layout.listview_item, null);
return item;
}
我們上面說過了方法中第二個引數是ListView會傳的itemView,提高效率用的,而這裡博主先不用,每次呼叫getView都會建立一個新的View然後返回
實現一個小目標,嗯:奇數的Item中的複選框要被選中
那麼很容易,只需要這樣子
public View getView(int position, View view, ViewGroup viewGroup) {
View item = View.inflate(mContext, R.layout.listview_item, null);
//找到文字框
TextView tv = (TextView) item.findViewById(R.id.tv);
//設定文字內容
tv.setText(listViewData.get(position));
//找到複選框
CheckBox cb = (CheckBox) item.findViewById(R.id.cb);
if(position % 2 != 0){ //如果是奇數
cb.setChecked(true);
}
return item;
}
程式碼也很簡單,就是找到了建立的佈局item中的文字控制元件和複選框,然後設定相應的內容
看效果
我們可以看到,功能實現了,而且沒有出現任何問題,比如常見的複用問題,嗯
喂喂喂,我們沒複用回傳的View,哪裡來的複用問題啊,哈哈哈,所以我們的列表是肯定沒有任何問題的,因為根本沒有複用,效能是最差的一種寫法
實現一個小目標,複用Item,嗯
public View getView(int position, View view, ViewGroup viewGroup) {
View item = null;
if (view == null) {
item = View.inflate(mContext, R.layout.listview_item, null);
}else{
item = view;
}
//找到文字框
TextView tv = (TextView) item.findViewById(R.id.tv);
//設定文字內容
tv.setText(listViewData.get(position));
//找到複選框
CheckBox cb = (CheckBox) item.findViewById(R.id.cb);
if(position % 2 == 0){ //如果是奇數
cb.setChecked(true);
}
return item;
}
這段程式碼改動的地方就是方法最開始,判斷了一下回傳給我的view是不是為null,為null的情況博文最開始已經講過了
如果為null就建立一個新的,如果不是就直接賦值給item,達到條目的複用!
那我們看看效果唄!
請大聲的告訴我,發生了什麼?複用問題
沒錯,複用問題出現了,博主給大家重現了錯誤
那麼這裡是怎麼引起的呢?
只有知道其中的原理,你解決問題才能快準狠!
首先我先幫大家統計一下建立Item的次數
可以看到,我用一個變數記錄建立的次數,我重新執行
從App執行到滑動來滑動去,我們可以看見,最開始建立了16次,然後隨著滑動多來了一次,你可以使用截圖定格一下動圖,你會發現這個列表最多顯示17條記錄(當然了你的介面是多少個和我這個介面是不同的,反正就是介面能顯示的Item最多個數),所以證明了上面的一個觀點,ListView預設快取一個介面的Item個數
原理
所以當我們複用ListView回傳的View的時候,這個View是被之前使用過的,也就是說給你的這個View儲存了之前用過的狀態
這裡的情況就是給你的view剛好是之前複選框被選中的那個View,所以就造成複用啦
解決方法
對產生問題的控制元件進行初始化,初始化時什麼意思呢?
意思就是說,把出問題的控制元件,狀態還原一下
看程式碼!
別看了,就是框框裡面的一句話,是不是感覺很簡單呀,如果你知曉原理,為什麼這樣子就沒有了複用的問題呢?
因為如果給你的View裡面的複選框是被選中的,這裡你對他還原了呀,所以就ok啦
使用ViewHolder
上面我們也說了ViewHolder的作用和使用的必要性,那麼博主直接來用一下吧
由於getView內部稍微改動有點大,我貼上Adapter中的程式碼
public class ListViewAdapter extends BaseAdapter {
private List<String> listViewData;
private Context mContext;
public ListViewAdapter(List<String> listViewData, Context mContext) {
this.listViewData = listViewData;
this.mContext = mContext;
}
@Override
public int getCount() {
return listViewData.size();
}
@Override
public Object getItem(int i) {
return listViewData.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int position, View view, ViewGroup viewGroup) {
//Item對應的試圖
View item = null;
ViewHolder vh = null;
if (view == null) {
item = View.inflate(mContext, R.layout.listview_item, null);
vh = new ViewHolder();
//找到文字框
vh.tv = (TextView) item.findViewById(R.id.tv);
//找到複選框
vh.cb = (CheckBox) item.findViewById(R.id.cb);
//讓item和ViewHolder繫結在一起
item.setTag(vh);
} else {
//複用ListView給的View
item = view;
//拿出ViewHolder
vh = (ViewHolder) item.getTag();
}
//設定文字內容
vh.tv.setText(listViewData.get(position));
//還原狀態
vh.cb.setChecked(false);
if (position % 2 == 0) { //如果是奇數
vh.cb.setChecked(true);
}
return item;
}
/**
* 用於存放一個ItemView中的控制元件,由於這裡只有兩個控制元件,那麼宣告兩個控制元件即可
*/
class ViewHolder {
TextView tv;
CheckBox cb;
}
}
1.如果複用的View為null,我們需要建立一個新的item,同時也建立了一個ViewHolder,然後把條目檢視中的控制元件通過findViewById方法尋找到
ViewHolder中,然後我們說了需要和條目檢視進行繫結,所以呼叫了setTag方法
2.而另一邊,如果複用的View不是為null,那麼直接拿過來用,並且從裡面拿出ViewHolder,因為每一個複用的ViewHolder肯定是經過1處建立並且返回的
到這裡為止,一個完成的列表的展示和優化已經完成啦,並且中間講述了複用問題是如何產生的,如何解決!
下篇
ListView多佈局展示是個什麼鬼
demo下載
上述的程式碼我放在這裡,傳送門: