閱讀982 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android 適配器教程(五)

在之前四講中,我們已經由淺入深的認識了適配器,從最簡單的ListView寫起,最後完成了自定義適配器的簡單例子,這一次咱們就上次留下來的問題 再進行更加深入的學習,主要是getView方法的原理,還有Holder的使用,還有關於ListView性能方麵的優化問題。自定義適配器這一部分需要好好的進行理解,隻有真正理解的比較透徹,寫起來才會比較順手。這一次就沒有例子了,我會配上幾張圖片,希望能讓大家看得明白。

先從ListView的原理來進行講解吧!


大家先看一張圖:


這就是ListView的工作原理:

ListView 針對List中每個item,要求 adapter “給我一個視圖” (getView)。

   一個新的視圖被返回並顯示

如果我們有上億個項目要顯示怎麼辦?為每個項目創建一個新視圖?這不可能!內存怎麼辦?

實際上Android為你緩存了視圖。

Android中有個叫做Recycler的構件,


如果你有10億個項目(item),其中隻有可見的項目存在內存中,其他的在Recycler中。


ListView先請求一個type1視圖(getView)然後請求其他可見的項目。convertView在getView中是空(null)的。


當item1滾出屏幕,並且一個新的項目從屏幕低端上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1。你隻需設定新的數據然後返回convertView,不必重新創建一個視圖。這樣直接使用convertView從而減少了很不不必要view的創建。


更快的方式是定義一個ViewHolder,將convertView的tag設置為ViewHolder,不為空是重新使用
 
現在我們來深入了解一下ViewHolder吧!

要注意:很多人都認為功勞都是ViewHolder的,其實不然,Tag也是功臣
ViewHolder隻是將需要緩存的那些view封裝好,convertView的setTag才是將這些緩存起來供下次調用
當你的listview裏布局多樣化的時候 viewholder的作用就有比較明顯的體現了。
假如你2種模式的布局 當發生回收的時候 你會用setTag分別記錄是哪兩種   這兩種模式會被封裝到viewholder中進行保存方便你下次使用。ViewHolder就是個靜態類 與緩存無關


複習一下源碼先:
注釋已經寫的很明白了,大家要仔細看哦!

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定義一個ViewHolder對象
		if (convertView == null) {			//當初始化的時候 convertView是空的
			
			holder=new ViewHolder();		//創建一個ViewHolder
			
			//通過LayoutInflater用來找res/layout/下的xml布局文件,並且實例化,這樣convertView的界麵就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法來獲得其中的界麵元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//將holder對象作為標簽添加到View上
			convertView.setTag(holder);
			
		}else {
			//不為空的時候直接重新使用convertView從而減少了很多不必要的View的創建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然後加載數據
		holder.img.setBackgroundResource((Integer)data.get(position).get("imgview2"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			
		
		
		return convertView;
	}

讓我們再回憶一下getView的過程:

ListView有很多Item,getView就是根據Item的序號(position)來獲取這個Item的控件。

convertView =mInflater.inflate(R.layout.itemrow, null);

這句話是將layout下麵的 itemrow 作為ListView每個Item的樣式。下麵就是對Item的控件賦值。

上麵的都很好理解,接下來是重點:


說白了也就是ListView的緩衝機製,不僅是ListView,GridView也可以這樣。

對這個ViewHolder舉個比較極端例子說明它的作用的話就是:

假設整個ListView有1000行,你往下翻頁,翻到最後,如果不用ViewHolder並且代碼完全沒有考慮到效率問題的話他會到布局文件中拿1000次View,所以,ViewHolder就是為了解決這個。打個比方,比如ListView每頁空間隻能顯示5個,那麼你往下翻頁,翻到第6個條目,那麼第一個條目已經不在顯示範圍內了,這樣調用getView的第二個參數convertView 不再是null了,那麼ViewHolder作用就是將這個一開始的第一條反複利用,而不是再inflate一個控件出來,


if (convertView == null)這句很關鍵  你翻動的時候下麵的item如果是剛出現的時候下麵的是沒有的 if下麵的語句就是創建如果你已經翻動過則不會創建了這個類有節省內存的作用。

 

這樣大家就發現一些ViewHolder的優勢了,實際上構建這個ViewHolder是為了把查找的view緩存起來方便多次重用不用重新構建VIEW,利用係統中緩存的VIEW,可以提高效率。當然上麵實際上我把ViewHolder有些過於誇大了,隻有在代碼完全沒有考慮到效率的時候才會出現這樣的對比。

別忽視了Tag,他其實就相當於一個持有者的類,他裏麵一般沒有方法,隻有屬性,作用就是一個臨時的儲存器,把你getView方法中每次返回的View存起來,可以下次再用。這樣做的好處就是不必每次都到布局文件中去拿到你的View,提高了效率。可以理解成ViewHolder,它的作用就在於減少不必要的調用findViewById,不必要每次都重新加載控件布局。所以ViewHolder和settag/gettag的妙用是成就了效率提升的關鍵。不管是調用findViewById還是直接new 一個View組件出來,他們的目的都是為了在內存中創建一個View對象。當有n項數據需要顯示時,顯然必然需要調用findViewById n次或new n次View對象。如果使用ViewHolder以及settag,那麼就可以做到一次創建n次複用。

下麵是摘自google 2010 I/O大會三個例子代碼



在android開發中Listview是一個很重要的組件,用戶可以自由的定義listview每一列的布局,但當listview有大量的數據需要加載的時候,會占據大量內存,影響性能,這時候就需要按需填充並重新使用view來減少對象的創建。


ListView加載數據都是在public View getView(int position, View convertView, ViewGroup parent) {}方法中進行的(要自定義listview都需要重寫listadapter:如BaseAdapter,SimpleAdapter,CursorAdapter的等的getvView方法),優化listview的加載速度就要讓convertView匹配列表類型,並最大程度上的重新使用convertView。


getview的加載方法一般有以下三種種方式:


最慢的加載方式是每一次都重新定義一個View載入布局,再加載數據


public View getView(int position, View convertView, ViewGroup parent) {
 View item = mInflater.inflate(R.layout.list_item_icon_text, null);
 ((TextView) item.findViewById(R.id.text)).setText(DATA[position]);
 ((ImageView) item.findViewById(R.id.icon)).setImageBitmap(
 (position & 1) == 1 ? mIcon1 : mIcon2);
 return item;
}


正確的加載方式是當convertView不為空的時候直接重新使用convertView從而減少了很多不必要的View的創建,然後加載數據


public View getView(int position, View convertView, ViewGroup parent) {
 if (convertView == null) {
 convertView = mInflater.inflate(R.layout.item, parent, false);
 }
 ((TextView) convertView.findViewById(R.id.text)).setText(DATA[position]);
 ((ImageView) convertView.findViewById(R.id.icon)).setImageBitmap(
 (position & 1) == 1 ? mIcon1 : mIcon2);
 return convertView;
 }


 

最快的方式是定義一個ViewHolder,將convetView的tag設置為ViewHolder,不為空時重新使用即可


static class ViewHolder {
TextView text;
ImageView icon;
}
 
public View getView(int position, View convertView, ViewGroup parent) {
 ViewHolder holder;
 
 if (convertView == null) {
 convertView = mInflater.inflate(R.layout.list_item_icon_text,
 parent, false);
 holder = new ViewHolder();
 holder.text = (TextView) convertView.findViewById(R.id.text);
 holder.icon = (ImageView) convertView.findViewById(R.id.icon);
 convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.text.setText(DATA[position]);
holder.icon.setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return convertView;
}


 

三種方式加載效率對比如下圖所示:

 




 

當處理一些耗時的資源加載的時候需要做到以下幾點,以使你的加載更快更平滑:

1.   適配器在界麵主線程中進行修改

2.   可以在任何地方獲取數據但應該在另外一個地方請求數據

3.   在主界麵的線程中提交適配器的變化並調用notifyDataSetChanged()方法


好了,到此為止大家應該明白看似神秘的ViewHolder和set/get tag 的優點了吧,有優點才有使用的動力,同時也有去深入理解的動力,希望大家結合上一篇的內容,能夠學到一些東西。


關於這個問題的理解,我看網上眾說紛紜,以上的都是我個人觀點。


下一講我會用一個比較複雜的自定義適配器結束我們的學習,同時也算是深入學習的大練兵,最近略微有點忙,最後一篇有可能會過一段時間才能寫完,請大家不要錯過哦!


源碼我會在最後一篇的最後附上鏈接,我還沒有寫完,之前都是邊寫博客邊碼代碼的,我覺得這樣思路會更加清晰。

我也是學生,水平十分有限,還請大家多多指教~


最後更新:2017-04-03 05:39:33

  上一篇:go 立帖為據,每日學習一課編程技術
  下一篇:go Automatic logon configuration on Linux OS