Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android ListView優化篇

Android ListView優化篇

編輯:關於Android編程

在上篇文章的例子中,我們使用了一張圖片和一個文本作為每一行的數據,發現效果已經完全達到了,而且沒出現什麼問題。但如果我們將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> data;
	
	private LayoutInflater inflater;
	
	//注意,這裡定義的這些整型數要小於getViewTypeCount()所返回的那個數字,否則會報錯越界
	private final int TYPE_1 = 0;
	private final int TYPE_2 = 1;
	
	public ListViewAdapter(Context context,
			List> data, int resource, String[] from,
			int[] to) {
		super(context, data, resource, from, to);
		// TODO Auto-generated constructor stub
		this.context = context;
		this.data = data;
		inflater = LayoutInflater.from(context);
	}
	//返回數據的大小,即listview的行數
	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return data.size();
	}
	//根據下標獲得某一行的數據
	@Override
	public Object getItem(int position) {
		// TODO Auto-generated method stub
		return data.get(position);
	}
	//獲得指定的Item的下標
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}
	
	@Override
	public int getItemViewType(int position) {
		// TODO Auto-generated method stub
		//如果當前行是偶數行,返回類型1
		if(position%2==0){
			return TYPE_1;
		}
		//如果當前行是奇數行,返回類型2
		else{
			return TYPE_2;
		}
	}
	
	@Override
	public int getViewTypeCount() {
		// TODO Auto-generated method stub
		return 2;
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		Log.d("getView--->", convertView+"--position:"+position);
				
		ViewHolder1 holder1 = null;
		ViewHolder2 holder2 = null;
		int type = getItemViewType(position);
		if(convertView==null){
			switch (type) {
			case TYPE_1:
				convertView = inflater.inflate(R.layout.list_item, null);
				holder1 = new ViewHolder1();
				holder1.text = (TextView)convertView.findViewById(R.id.list_item_text);
				holder1.image = (ImageView)convertView.findViewById(R.id.list_item_image);
				convertView.setTag(holder1);
				break;

			case TYPE_2:
				convertView = inflater.inflate(R.layout.list_item2, null);
				holder2 = new ViewHolder2();
				holder2.text = (TextView)convertView.findViewById(R.id.list_item_text2);
				holder2.detail = (TextView)convertView.findViewById(R.id.list_item_detail2);
				convertView.setTag(holder2);
				break;
			}
			
		}
		else{
			switch (type) {
			case TYPE_1:
				holder1 = (ViewHolder1)convertView.getTag();
				break;

			case TYPE_2:
				holder2 = (ViewHolder2)convertView.getTag();
				break;
			}
			
		}
		
		switch (type) {
			case TYPE_1:
				holder1.text.setText(data.get(position).get("text").toString());
				holder1.image.setImageResource(Integer.parseInt(data.get(position).get("image").toString()));
				break;
	
			case TYPE_2:
				holder2.text.setText(data.get(position).get("text").toString());
				holder2.detail.setText(data.get(position).get("text").toString());
				break;
		}
		
		
		
		return convertView;
	}
	
	public static class ViewHolder1{
		public TextView text;
		public ImageView image;
	}
	
	public static class ViewHolder2{
		public TextView text;
		public TextView detail;
	}

}

 

代碼分析:創建另外一個ViewHolder,用於加載和重用另外一種布局,其實就是在原來的基礎上,為每個操作都套上一層switch判斷,然後根據type的類型來分別設置兩種布局。

 

ListView異步加載亂序問題

出現亂序的原因

上面的操作都是屬於同步加載每一行,所以不會出現什麼問題。但如果當我們是網絡異步加載每一行的圖片時,就會出現數據紊亂,前文已經說了,Android為ListView進行的Reycler的處理,減少內存開銷,那麼,每當有新的元素進入界面時就會回調getView()方法,而在getView()方法中會開啟異步請求從網絡上獲取圖片,注意網絡操作都是比較耗時的,也就是說當我們快速滑動ListView的時候就很有可能出現這樣一種情況,某一個位置上的元素進入屏幕後開始從網絡上請求圖片,但是還沒等圖片下載完成,它就又被移出了屏幕。這種情況下會產生什麼樣的現象呢?根據ListView的工作原理,被移出屏幕的控件將會很快被新進入屏幕的元素重新利用起來,而如果在這個時候剛好前面發起的圖片請求有了響應,就會將剛才位置上的圖片顯示到當前位置上,因為雖然它們位置不同,但都是共用的同一個ImageView實例,這樣就出現了圖片亂序的情況。但是還沒完,新進入屏幕的元素它也會發起一條網絡請求來獲取當前位置的圖片,等到圖片下載完的時候會設置到同樣的ImageView上面,因此就會出現先顯示一張圖片,然後又變成了另外一張圖片的情況。

 

如何解決亂序問題?

總之,以上講述了ListView的多種優化方式,但是並不是萬能,也僅僅只是起到了一部分效果,真實開發中還要視情況而定,比如如果是多圖片,首先需要將圖片壓縮,並且不要再getView中做過多的耗時操作!希望本文對大家理解ListView的優化有所幫助。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved