將此句說100遍, 你就會用了: 適配器的作用就是將資料繫結到條目介面的每一個顯示控制項上.
---------------------------------屎一樣的分割線-------------------------------------
1.自訂Adapter的時候的getview()方法遇到了類型轉換異常.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView = new ImageView(mContext);
imageView.setLayoutParams(new GridView.LayoutParams(120, 120));
imageView.setImageResource(data[position]);
imageView.setScaleType(ScaleType.CENTER);
return imageView;
}
紅色部分, 要用GridView(如果你用GridView的話).不能直接導包匯入ViewGroup的LayoutParams. 這樣編譯時間不會報錯, 但是運行時報這個傻X錯誤java.lang.ClassCastException: android.view.ViewGroup$LayoutParams.
2. getView裡的convertView參數有什麼用?
解決ListView滾動卡的問題
寫了個類似下面的GridView,滾動的時候有卡或者跳格的現象,尤其當記錄比較多的時候。
GridView和ListView機制原理是類似的,都是基於ListAdapter來處理View的控制的。在排查問題的時候也測試了用ListView替換GridView,問題依舊。
實現的樣本大致是這個樣子:
測試資料有600條左右。不過,即使減到40條左右,也是會卡的。在ddms log中監控dalvik日誌,會有大量下面的資訊:
01-14 10:37:47.579: DEBUG/dalvikvm(13626): GC_EXTERNAL_ALLOC freed 58 objects / 2072 bytes in 37ms
基本上每滾動一次,就會出10多條。這是造成卡的主要原因。也就是說,要釋放大量的臨時對象。
這些臨時對象還不是我自己建立的對象,我把結果集合已經一次性載入到記憶體中了。也不是圖片顯示造成的,我把圖片去掉現象一樣。
另外,也考慮了比如擴大堆記憶體的方法,但是不對症下藥,因為並不是記憶體達到堆的上限造成full gc。有關堆的日誌可以過濾dalvikvm-heap看到。如果堆記憶體full gc會有類似這樣的日誌:
dalvikvm-heap(11651): Heap Massage needed (768000-byte external allocation too big)
dalvikvm-heap(11651): Full GC (don’t collect SoftReferences)
可以初步判斷,是GridView/ListView為實現滾屏產生的瞬態使用對象,隨著滾動而被快速丟棄了,需要記憶體回收,造成效能問題。
可是,為什麼Android內建的通訊錄卻不卡呢?My Phone上的通訊錄也有將近300條的記錄。通過ddms日誌觀察,沒有頻繁的GC_EXTERNAL_ALLOC日誌,從頭滾動到尾只出現了1-2次。
於是決定查看通訊錄的原始碼。這幾行:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
……
View v;
if (convertView == null || convertView.getTag() == null) {
newView = true;
v = newView(mContext, cursor, parent);
} else {
newView = false;
v = convertView;
}
說明了對ListView元素視圖的複用。再查api:
convertView The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view.
因此,可以認為,convertView是元素的緩衝,因為元素本身沒有變化,因此可以判斷如果非空,就可以複用。比如我樣本中是這樣寫的:
if (convertView == null) {
convertView=activity.getLayoutInflater().inflate(
R.layout.album_grid_element, parent, false);
}
之前是每次直接建立新的view的,沒有考慮複用convertView。這是造成卡的根本原因。
這次的另外的一個教訓是,解決問題沒有從api層面層層深入解決問題,即這樣的次序:api是否能解決?是否需要通過自己的最佳化解決?是否需要系統的部署方面的配置比如堆的調整來解決等。
結果是走了彎路,消耗了時間。
以後還是要先通讀api文檔。這次出問題,是建立在以前隨手寫的簡單樣本基礎上的。
3.BaseAdapter中重寫getview的心得以及發現convertView回收的機制
下面先講講我遇到的幾個問題:
一.View getview(int position, View convertview, ViewGroup parent )中的第二個參數是什麼含義;
二.View的SetTag和getTag方法的用途;
先來解決第一個問題:
android SDK中這樣講參數 convertview :
the old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using.
If it is not possible to convert this view to display the correct data, this method can create a new view.
翻譯:
如果可以的話,這是舊View(這裡不便翻譯有的人翻成視圖)的重用。 建議:在用之前,你應該檢查這個View是不是非空,是不是一個合適的類型。
如果不可能讓這個VIew去顯示一個恰當的資料,這個方法會建立一個新的View。
如果我們要做的是一個ListView,在手機上顯示的只有那麼幾條Item,而整個ListView可能有很長,可能是100條甚至是上萬條,總不能讓這麼多條Item都駐留在記憶體中,所以android為你準備了一套機制,就是Recycler(反覆迴圈器),他的具體工作原理可以到 http://www.cnblogs.com/xiaowenji/archive/2010/12/08/1900579.html去看。但是有些地方他沒有講清,所以我再講一下。先把代碼貼出來
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/result"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#00000000" >
</ListView>
</LinearLayout>
此處注意ListView的android:layout_height屬性值為"fill_paternt",如果為“wrap_content"將會是另一種情況adapter的代碼
class ListViewAdapter extends BaseAdapter
{
private Context mContext;
int i=0;
public ListViewAdapter (Context context)
{
this.mContext=context;
}
@Override
public int getCount()
{
return 30;
}
@Override
public Object getItem(int position)
{
return position;
}
@Override
public long getItemId(int position)
{
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
System.out.println("getView " + position + " " + convertView);//調試語句
Holder holder;
if(null==convertView)
{
holder=new Holder();
convertView=LayoutInflater.from(mContext).inflate(R.layout.textview, null); //mContext指的是調用的Activtty
holder.textView=(TextView)convertView.findViewById(R.id.textview);
convertView.setTag(holder);
}
else
{
holder=(Holder)convertView.getTag();
}
holder.textView.setText("position: "+position);
return convertView;
}
class Holder
{
public TextView textView;
}
}
運行程式之後發現螢幕上顯示出的Item的convertview都為空白,向下滑動新產生的Item的convetview都不為空白。到此為止和上面連結中講的是一致的,但是如果設定ListView的android:layout_height屬性值為“wrap_content之後,發現只有第一個Item的convertview為null其他的不為空白。雖然兩種設定不同,結果也不同,但是convertview的機制沒有變。其實到此為止我們可以總結出convertview的機制了,就是在初始顯示的時候,每次顯示一個item都調用一次getview方法但是每次調用的時候covertview為空白(因為還沒有舊的view),當顯示完了之後。如果螢幕移動了之後,並且導致有些Item(也可以說是view)跑到螢幕外面,此時如果還有新的item需要產生,則這些item顯示時調用的getview方法中的convertview參數就不是null,而是那些移出螢幕的view(舊view),我們所要做的就是將需要顯示的item填充到這些回收的view(舊view)中去,最後注意convertview為null的不僅僅是初始顯示的那些item,還有一些是已經開始移入螢幕但是還沒有view被回收的那些item。最終我們用親手寫的代碼實現了Recycler(反覆迴圈器).第二個問題其實應該在第一個問題中嵌套來講,但是為了思路清晰我分開了:view的setTag和getTag方法其實很簡單,在實際編寫代碼的時候一個view不僅僅是為了顯示一些字串、圖片,有時我們還需要他們攜帶一些其他的資料以便我們對該view的識別或者其他動作。於是android 的設計者們就創造了setTag(Object)方法來存放一些資料和view綁定,我們可以理解為這個是view 的標籤也可以理解為view 作為一個容器存放了一些資料。而這些資料我們也可以通過getTag() 方法來取出來。到這裡setTag和getTag大家應該已經明白了。再回到上面的話題,我們通過convertview的setTag方法和getTag方法來將我們要顯示的資料來綁定在convertview上。如果convertview 是第一次展示我們就建立新的Holder對象與之綁定,並在最後通過return convertview 返回,去顯示;如果convertview 是回收來的那麼我們就不必建立新的holder對象,只需要把原來的綁定的holder取出加上新的資料就行了。至此我的問題講完了,你的問題解決了嗎?
關於adapter,現在理解得多了一點。有一類View叫做AdapterView,比如ListView就是AdapterView的一種。這類的View有一個setAdapter(Adapter adapter)方法,通過這個方法,來update View。
Java代碼
ListView listView = (ListView)findViewById(R.layout.list_view);
listView.setAdapter(new MyAdapter(this));
上面的listView在初始化的時候是沒有資料的,也就是沒有ListItem,通過setAdapter()方法,才填充了資料。
Adapter類的作用,就是根據資料來源,來返回View,每個View最終會成為AdapterView的其中一個Child View。如同其名字一樣,Adapter是一個資料到視圖的適配器。可以將其理解為是一個容器的處理器,它將容器內的每一項,映射成一個View並返回。資料來源可以是資源檔指定的資料集合,也可以來自SQLite,也可以是記憶體中的自訂對象……總之是一種資料對象的集合。而將要返回的View,是由Adapter中的getView()方法建立並返回的,可以來自layout檔案的設定,也可以是自訂的View對象。
搞清楚原理,就明白AdapterView和Adapter是怎樣協同工作,產生頁面的了。Adapter根據資料來源,迴圈調用getView()方法,產生View對象的集合。然後AdapterView.setAdapter()方法,用前面產生的View對象集合,來給檢視窗產生資料項目。ListView只是AdapterView的一個子類,還有很多其他的AdapterView,但是原理都是相同的。有空的話可以看看ListAdapter類的getView()方法的源碼,應該會對這個過程理解得更清晰一些。
Dev Guide 上對於這個有專門的介紹:http://developer.android.com/guide/topics/ui/binding.html
也可以看看它的子類到底有哪些:http://developer.android.com/reference/android/widget/AdapterView.html
搞清楚了上一點,再來看ListActivity,其實ListActivity只是一個普通的Activity,只是它內建了一個ListView,以及提供了getListView()方法來擷取內建的ListView;還提供了setListAdapter()方法,來給這個內建的ListView設定Adapter;以及諸如onListItemClick()等方法而已。在要使用ListActivity的地方,用普通的Activity也是完全可以的,只是要多寫一些方法而已。