您好,登錄后才能下訂單哦!
這篇文章給大家介紹深入淺析Android中的ListView復用機制,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
1.ListView的復用機制
ListView是我們經常使用的一個控件,雖然說都會用,但是卻并不一定完全清楚ListView的復用機制,雖然在Android 5.0版本之后提供了RecycleView去替代ListView和GridView,提供了一種插拔式的體驗,也就是所謂的模塊化。本篇主要針對ListView的復用機制進行探討,因此就 提RecycleView。昨天看了一下郭霖大神的ListView原理深度解析的一篇博客,因此學習了一段時間,自己也說一下自己的理解。
i.RecycleBin的基本原理
首先需要說一下RecycleBin的基本原理,這個類也是實現復用的關鍵類。接著我們需要明確ActiveView的概念,ActivityView其實就是在UI屏幕上可見的視圖(onScreenView),也是與用戶進行交互的View,那么這些View會通過RecycleBin直接存儲到mActivityView數組當中,以便為了直接復用,那么當我們滑動ListView的時候,有些View被滑動到屏幕之外(offScreen) View,那么這些View就成為了ScrapView,也就是廢棄的View,已經無法與用戶進行交互了,這樣在UI視圖改變的時候就沒有繪制這些無用視圖的必要了。他將會被RecycleBin存儲到mScrapView數組當中,但是沒有被銷毀掉,目的是為了二次復用,也就是間接復用。當新的View需要顯示的時候,先判斷mActivityView中是否存在,如果存在那么我們就可以從mActivityView數組當中直接取出復用,也就是直接復用,否則的話從mScrapView數組當中進行判斷,如果存在,那么二次復用當前的視圖,如果不存在,那么就需要inflate View了。
這是一個總體的流程圖,復用機制就是這樣的。那么我們先來理解一下ListView第一次加載的時候都做了哪些工作,首先會執行onLayout方法。。
/** * Subclasses should NOT override this method but {@link #layoutChildren()} * instead. */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mInLayout = true; if (changed) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).forceLayout(); } mRecycler.markChildrenDirty(); } layoutChildren(); mInLayout = false; }
這里可以看到onLayout方法會調用layoutChildren()方法,也就是對item進行布局的流程,layoutChildren()方法就不進行粘貼了,代碼量過長我們只需要知道,這是對ListView中的子View進行布局的一個方式就可以了,在我們第一次加載ListView的時候,RecycleBin中的數組都沒有任何的數據,因此第一次加載都需要inflate View,也就是創建新的View。并且第一次加載的時候是自頂向下對數據進行加載的,因此在layoutChildren()會執行fillFromTop()方法。fillFromTop()會執行filleDown()方法。
/** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * * @param nextTop The location where the top of the item associated with pos * should be drawn * * @return The view that is currently selected, if it happens to be in the * range that we draw. * * @param pos:列表中的一個繪制的Item在Adapter數據源中對應的位置 * @param nextTop:表示當前繪制的Item在ListView中的實際位置.. */ private View fillDown(int pos, int nextTop) { View selectedView = null; /** * end用來判斷Item是否已經將ListView填充滿 */ int end = (getBottom() - getTop()) - mListPadding.bottom; while (nextTop < end && pos < mItemCount) { /** * nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的界面就可以了 *pos < mItemCount確保了我們新增的子View在Adapter中都有對應的數據源item */ // is this the selected item? boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); /** *將最新child的bottom值作為下一個child的top值,存儲在nextTop中 */ nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; } return selectedView; }
這里存在一個關鍵方法,也就是makeAndAddView()方法,這是ListView將Item顯示出來的核心部分,也是這個部分涉及到了ListView的復用
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; //判斷數據源是否發生了變化. if (!mDataChanged) { // Try to use an exsiting view for this position //如果mActivityView[]數組中存在可以直接復用的View,那么直接獲取,然后重新布局. child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible /** *如果mActivityView[]數組中沒有可用的View,那么嘗試從mScrapView數組中讀取.然后重新布局. *如果可以從mScrapView數組中可以獲取到,那么直接返回調用mAdapter.getView(position,scrapView,this); *如果獲取不到那么執行mAdapter.getView(position,null,this)方法. */ child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }
這里可以看到如果數據源沒有變化的時候,會從mActivityView數組中判斷是否存在可以直接復用的View,可能很多讀者都不太明白直接復用到底是怎么個過程,舉個例子,比如說我們ListView一頁可以顯示10條數據,那么我們在這個時候滑動一個Item的距離,也就是說把position = 0的Item移除屏幕,將position = 10 的Item移入屏幕,那么position = 1的Item是不是就直接能夠從mActivityView數組中拿到呢?這是可以的,我們在第一次加載Item數據的時候,已經將position = 0~9的Item加入到了mActivityView數組當中,那么在第二次加載的時候,由于position = 1 的Item還是ActivityView,那么這里就可以直接從數組中獲取,然后重新布局。這里也就表示的是Item的直接復用。
如果我們在mActivityView數組中獲取不到position對應的View,那么就嘗試從mScrapView廢棄View數組中嘗試去獲取,還拿剛才的例子來說當position = 0的Item被移除屏幕的時候,首先會Detach讓View和視圖進行分離,清空children,然后將廢棄View添加到mScrapView數組當中,當加載position = 10的Item時,mActivityView數組肯定是沒有的,也就無法獲取到,同樣mScrapView中也是不存在postion = 10與之對應的廢棄View,說白了就是mScrapView數組只有mScrapView[0]這一項數據,肯定是沒有mScrapView[10]這項數據的,那么我們就會這樣想,肯定是從Adapter中的getView方法獲取新的數據嘍,其實并不是這樣,雖然mScrapView中雖然沒有與之對應的廢棄View,但是會返回最后一個緩存的View傳遞給convertview。那么也就是將mScrapView[0]對應的View返回。總體的流程就是這樣。
這里我們可以看到,ListView始終只會在getView方法中inflate一頁的Item,也就是new View只會執行一頁Item的次數。后續的Item通過直接復用和間接復用完成。
注意一種情況:比如說還是一頁的Item,但是position = 0的Item沒有完全滑動出UI,position = 10的Item沒有完全進入到UI的時候,那么position = 0的Item不會被detach掉,同樣不會被加入到廢棄View數組,這時mScrapView是空的,沒有任何數據,那么position = 10的Item即無法從mActivityView中直接復用View,因為是第一次加載。mActivityView[10]是不存在的,同時mScrapView是空的,因此position = 10的Item只能重新生成View,也就是從getView方法中inflate。這里obtainView方法沒有具體貼出,大家可以自己進去看看。obtainView其實就是判斷能否從廢棄View中獲取到View,獲取到了則執行:
if (scrapView != null) { child = mAdapter.getView(position, scrapView, this); }
這里是可以獲取到,那么getView會傳遞scrapView。否則的話:
else { child = mAdapter.getView(position, null, this); }
獲取不到就傳遞null,這樣就會執行我們定義的Adapter中的方法。
@Override public View getView(int position, View convertView, ViewGroup parent) { if(convertView == null){ convertView = View.inflate(context, R.layout.list_item_layout, null); } return convertView; }
至于向上滑動會執行其他的一些方法,也就是自底向上鋪滿ListView,同樣也會直接或者間接復用控件。理解了復用的機制才是關鍵,因此向上滑基本就不難理解了。補充一點,RecycleBin中還存在一個方法,setViewTypeCount()方法。這個是針對Adapter中的getViewTypeCount()設定的。針對每一種數據類型,setViewTypeCount()會為每種數據類型開啟一個單獨的RecycleBin回收機制。這里我們只需要知道就可以了。至于在郭神博客中看到ListView會onLayout多次,這是肯定的,由于Android View加載機制問題,子控件需要根據父控件的大小要重新測量大小,經過多次測量才能夠顯示在UI上。這是View測量多次的原因。至于ListView在多次布局的問題我就不進行贅余了,總之無論幾次測量,ListView是不會多次執行重復的邏輯的,也就是說數據不會有多份,只會存在一份數據。
這里也就是ListView復用的基本原理和RecycleBin的回收機制了。代碼貼的很少,都是一些關鍵代碼,沒必要去一行一行的研究代碼,畢竟和大神還差很大的一個檔次。我們只需要知道這個執行過程和原理就可以了。
2.ViewHolder
最后說一說ViewHolder這個東西,很多Android學習者會把這個東西和ListView的復用機制搞混。這里ViewHolder也是在復用的時候進行使用,但是和復用機制是沒太大關系的。
@Override public View getView(int position, View convertView, ViewGroup parent) { final ViewHolder holder; ListViewItem itemData = items.get(position); if(convertView == null){ convertView = View.inflate(context, R.layout.list_item_layout, null); holder = new ViewHolder(); holder.userImg = (ImageView) convertView.findViewById(R.id.user_header_img); holder.userName = (TextView) convertView.findViewById(R.id.user_name); holder.userComment = (TextView) convertView.findViewById(R.id.user_coomment); convertView.setTag(holder); }else{ holder = (ViewHolder) convertView.getTag(); } holder.userImg.setImageResource(itemData.getUserImg()); holder.userName.setText(itemData.getUserName()); holder.userComment.setText(itemData.getUserComment()); return convertView; } static class ViewHolder{ ImageView userImg; TextView userName; TextView userComment; }
在實現Adapter的時候,我們一般會加上ViewHolder這個東西,ViewHolder和復用機制和原理是無關的,他的主要目的是持有Item中控件的引用,從而減少findViewById()的次數,因為findViewById()方法也是會影響效率的,因此在復用的時候他起的作用是這個,減少方法執行次數增加效率。這里做個簡單的提醒,別弄混就行。
關于深入淺析Android中的ListView復用機制就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。