1. 程式人生 > >ListView複用和優化詳解

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下載

上述的程式碼我放在這裡,傳送門: