1. 程式人生 > >android-----帶你一步一步優化ListView(一)

android-----帶你一步一步優化ListView(一)

        ListView作為android中最常使用的控制元件,可以以條目的形式顯示大量的資料,經常被用於顯示最近聯絡人列表,對於每一個 Item,均要求adapter的getView方法返回一個View,因此ListView的實現是離不開Adapter的,如果以MVC的思想來看ListView的話,ListView的顯示相當於V,Adapter部分相當於C,而資料部分就相當於M了,接下來的幾篇部落格計劃對ListView自己所瞭解的一些優化措施總結一下,希望能夠幫助到大家;

        先來看看如果我們不使用任何優化措施的話,使用ListView的方法:

        這裡先補充下獲得LayoutInflater的三種方法:

        (1)如果是在Activity中的話,可以呼叫

            LayoutInflater inflater = getLayoutInflater();

        (2)如果不是在Activity中,則可以將context上下文作為引數,通過

            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        (3)如果不是在Activity中,則可以將context上下文作為引數,通過

            LayoutInflater inflater = LayoutInflater.from(context);

        首先定義Activity介面佈局listview.xml,很簡單,裡面就只有一個ListView

<LinearLayout 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=".MainActivity" >
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
        再定義ListView的每個item的佈局item.xml,也很簡單,只有一個TextView
<?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"
    android:orientation="vertical" > 
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="50dp"/>
</LinearLayout>

        接下來定義一個Adapter,繼承自BaseAdapter,用來為ListView填充資料

public class ListViewAdapter extends BaseAdapter{

	List<String> list = new ArrayList<String>();
	LayoutInflater inflater = null;
	
	public ListViewAdapter(List<String> list,Context context) {
		this.list = list;
		inflater = LayoutInflater.from(context);
	}
	@Override
	public int getCount() {
		return list.size();
	}

	@Override
	public Object getItem(int position) {
		return list.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = null;
		System.out.println("before: "+list.get(position)+"-------  "+convertView);
		view = inflater.inflate(R.layout.item, null);
	    System.out.println("after: "+list.get(position)+"-------  "+view);
		TextView textView = (TextView) view.findViewById(R.id.textView);
		textView.setText(list.get(position));
		return view;
	}
}

       可以發現,我們在getView裡並沒有做任何的優化,待會我們看看這樣的方式會出什麼問題;

        定義Activity,將Adapter繫結到ListView上面

public class MainActivity extends Activity {

	List<String> list = new ArrayList<String>();
	ListView listView = null;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.listview);
		listView = (ListView) findViewById(R.id.listView);
		for(int i = 1;i < 50;i++)
		{
			list.add("item "+i);
		}
		ListViewAdapter adapter = new ListViewAdapter(list, this);
		listView.setAdapter(adapter);
	}
}
        很簡單,我們模擬了有50個條目的ListView,並且通過ListViewAdapter的建構函式將其傳遞給Adapter用於在介面展示這些資料;

        執行效果圖:

                                             

        對應的Logcat輸出:      

                               

        可以發現初始狀態有10個item顯示在介面上,並且他們的before view均是null的,呼叫inflate方法之後生成了新的view,因此after view非空了;

        接著我們向上拖動螢幕,可以在Logcat看到有如下輸出:

        

         可以發現此時的item 19和item 20在before之前convertView得地址都是@4181a590,這個view是已經劃出螢幕的item 9的,這點可以從剛開始第一螢幕的輸出看出來,因為item 9已經被加入到了RecycleBin快取中了,所以在呼叫getView方法的時候convertView獲取到的是快取中的隨機一個view,item 19和item 20完全有可能獲取到同一個view,但是他們的after view是不可能出現相同的,也就是說我們這個例子中的after view的地址值是不可能相同的,因為我們模擬了50個item,所以會有50個view被放到RecycleBin快取中,也許現在對於我們顯示來說沒什麼,那麼如果有大量的資訊需要顯示的話,直接就會報記憶體的;

         通過上面我們會發現一點,在呼叫getView方法的時候,他的第二個引數可能不會是null的,原因就是RecycleBin快取幫我們暫存了那些劃出螢幕的view,所以我們在convertView非空的情況下我們也沒什麼必要重新呼叫inflate方法載入佈局了,因為這個方法畢竟也是要解析xml檔案的,至少是要花時間的,直接使用從快取中取出的view即可啦,先來看看ListView提供的RecycleBin快取圖解:

           

         從上面的圖上可以看到當item 1被劃出螢幕之後,會被放到Recycle快取中,當item 8要劃入螢幕的時候,如果他和item 1的型別相同的話,則直接從Recycle中獲得即可,即此時的convertView不再是null;如果他和item 1型別不一致的話,則會新建view檢視,即此時convertView等於null;

        好的,那我們接下來就該充分使用android提供給我們的RecycleBin機制來優化ListView了;

        只需要修改ListViewAdapter類的getView方法即可了:

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = null;
		System.out.println("before: "+list.get(position)+"-------  "+convertView);
		if(convertView == null)
		{
			view = inflater.inflate(R.layout.item, null);
		}else
			view = convertView;
		TextView textView = (TextView) view.findViewById(R.id.textView);
		textView.setText(list.get(position));
		return view;
	}
        不滑動螢幕的時候,程式輸出:

                                          

          接著我們滑動螢幕,檢視輸出:

          

         注意紅色部分,發現沒經過11個item,都會複用之前的view,這也就是說我們的RecycleBin快取中將只有11個view了,不像前面那樣有50個view,這在很大程度上節約了記憶體,想想如果有上千萬條資料需要顯示,每個資料條目都有一個view在RecycleBin中是一件多麼可怕的事情,進行了convertView是否為null的判斷之後,將只會快取一螢幕的view,當然有可能會多那麼幾個吧;

         上面我們通過判斷convertView是否為空對ListView進行了優化,接下來我們看看getView方法,裡面在獲取TextView的時候,我們使用了findViewById方法,這個方法是與IO有關的操作,想必也會影響效能吧,他只要的目的是獲得某一個view的佈局罷了,我們如果有了view的話,其實只需要第一次將該view和其佈局繫結到一起就可以了,沒必要每次都為view設定佈局了,這也就是使用setTag的目的了;

         這裡也僅僅只是對ListViewAdapter的geyView方法進行修改即可:

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = null;
		ViewHolder viewHolder = null;
		System.out.println("before: "+list.get(position)+"-------  "+convertView);
		if(convertView == null)
		{
			view = inflater.inflate(R.layout.item, null);
			viewHolder = new ViewHolder();
			viewHolder.textView = (TextView) view.findViewById(R.id.textView);
			view.setTag(viewHolder);
		}else
		{
			view = convertView;
			viewHolder = (ViewHolder) view.getTag();
		}
		viewHolder.textView.setText("item "+list.get(position));
		return view;
	}
	static class ViewHolder
	{
		TextView textView;
	}
        這裡採用靜態內部類的方式用於定義item 的各個控制元件 ,如果convertView非空的話,表示該view對應的佈局已經存在了,只需要呼叫getTag獲取到即可了,如果convertView為空的話,則需要通過findViewById來獲取這個佈局中的控制元件,並且最後將該佈局通過setTag設定到view上面即可;

        程式的輸出結果和上面是一樣的,這裡不再列出;

        我們平常的實際應用中,每個條目有可能不都是一樣的,這種情況下會出現不同的專案佈局,那麼這時候該怎麼辦呢?同樣我們通過例項來學習一下這時候的RecycleBin機制是怎麼實現快取的;

        在此,我們增加一個只顯示一個按鈕的條目,佈局檔案button.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"
    android:orientation="vertical" >
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
</LinearLayout>
        修改ListViewAdapter類如下:
public class ListViewAdapter extends BaseAdapter{

	List<String> list = new ArrayList<String>();
	LayoutInflater inflater = null;
	
	public ListViewAdapter(List<String> list,Context context) {
		this.list = list;
		inflater = LayoutInflater.from(context);
	}
	@Override
	public int getItemViewType(int position) {
		//0表示顯示的是TextView,1表示顯示的是Button
		if(position % 4 != 0)
			return 0;
		else 
			return 1;
	}
	@Override
	public int getViewTypeCount() {
		//表示有兩種型別的item佈局
		return 2;
	}
	@Override
	public int getCount() {
		return list.size();
	}

	@Override
	public Object getItem(int position) {
		return list.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = null;
		TextViewHolder textViewHolder = null;
		ButtonViewHolder buttonViewHolder = null;
		System.out.println("before: "+list.get(position)+"-------  "+convertView);
		int type = getItemViewType(position);
		if(convertView == null)
		{
			switch (type) {
			case 0:
				view = inflater.inflate(R.layout.item, null);
				textViewHolder = new TextViewHolder();
				textViewHolder.textView = (TextView) view.findViewById(R.id.textView);
				view.setTag(textViewHolder);
				textViewHolder.textView.setText(list.get(position));
				break;
			case 1:
				view = inflater.inflate(R.layout.button, null);
				buttonViewHolder = new ButtonViewHolder();
				buttonViewHolder.button = (Button) view.findViewById(R.id.button);
				view.setTag(buttonViewHolder);
				buttonViewHolder.button.setText("button "+list.get(position));
				break;
			default:
				break;
			}
		}else
		{
			view = convertView;
			switch (type) {
			case 0:
				textViewHolder = (TextViewHolder) view.getTag();
				textViewHolder.textView.setText(list.get(position));
				break;
			case 1:
				buttonViewHolder = (ButtonViewHolder) view.getTag();
				buttonViewHolder.button.setText("button "+list.get(position));
				break;
			default:
				break;
			}
		}
		return view;
	}
	static class TextViewHolder
	{
		TextView textView;
	}
	static class ButtonViewHolder
	{
		Button button;
	}
}
        通過getViewTypeCount()返回的是你到底有多少種佈局,我們這裡有兩種

        通過getItemViewType(int)返回的是根據你的position得到的對應佈局的ID,當然這個ID是可以由你來定的;

        修改Activity類

public class MainActivity extends Activity {

	List<String> list = new ArrayList<String>();
	ListView listView = null;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.listview);
		listView = (ListView) findViewById(R.id.listView);
		for(int i = 1;i < 50;i++)
		{
			if(i % 4 == 0)
			{
				list.add("button "+i);
			}else
				list.add("item "+i);
		}
		ListViewAdapter adapter = new ListViewAdapter(list, this);
		listView.setAdapter(adapter);
	}
}
        介面執行效果圖為:

 

        可以發現每隔3個條目我們都會載入另一個不同的條目,接著檢視Logcat輸出:

 

        在我們滑動螢幕之後,輸出結果為:

        注意圖中突出顯示部分,可以發現對於button item,我們也得到了複用,當然這個複用順序並不一定是按順序來的,因為RecycleBin機制只會把你已經滑出螢幕的item快取下來,但是在從快取中取得時候,並不一定就是按你存進去的順序取出來的,這點要注意啦,到此一般的ListView優化測試結束了,我們來總結一下:

        (1)通過複用view的方式來充分利用android系統本身自帶的RecycleBin快取機制,能夠保證即使有再多的item實際中也僅僅會有有限多個item,大大節省記憶體;

        (2)使用靜態內部類以及setTag方式,將view與其對應的控制元件繫結起來,避免了每次得到view以後都需要通過findViewById的方式來獲取控制元件;

        (3)對於有多種佈局的ListView來說,我們可以通過getViewTypeCount()獲得佈局的種類,通過getItemViewType(int)獲得當前位置上的佈局到底是屬於哪一類;

        好了,這篇先介紹到這裡,接下來的一篇將重點介紹ListView載入圖片方面的優化措施;