前言
本文從源碼角度出發學習listview,主要分析首次RecycleBin的組成,layout的過程,滑動過程,item的點擊實現,如何支持Header,notifyDataSetChanged原理。
問題
用了好幾年的listview,有幾個問題卻一直不清楚
1、如何讓一個itemview不被回收,比如我的listview里有個viewpager比較復雜,不想讓他被回收又重新創建。
2、head和foot的原理是怎么樣的,會被回收嗎?
3、scrap里的view會被進一步回收掉嗎?
基礎知識
讀了3遍郭神的 http://blog.csdn.net/sinyu890807/article/details/44996879 真是受益匪淺,原來listview是這么實現的。
listview的實現方法跟scrollview完全不同,scrollview是內部實例化了所有的view,在滾動的時候只是改變可見的部分,scrollview的高度可能是幾千幾萬。如果item數很多的話,必然會oom。
而listview是首先畫出listview的殼,然后去adapter里取數據,取到數據inflate為view,填到listview里面去,填滿了就好了。即使adapter里有1萬個數據,第一次layout的時候取的也是很少的數據(看當前屏幕需要,假設10個)。然后在上滑的過程中,首先用offsetTopAndBottom對所有child進行移動,此時頂部view就會滑出部分,那么底部會出現gap,再去adapter里面撈數據,填到底部;然后頂部的view逐漸的被完全移出屏幕,先detach,然后把這個view丟到scrap里面去,繼續滑動底部又出現了gap,就去scrap里面拿現成的view。如此往復循環,這就是listview的原理。
和scrollview對比,listview的滑動過程中伴隨著view的detach,attach,但是這些都不是耗時的東西,時間上沒什么損失,但是空間上減少了大量的內存開銷。先分析下layout過程和滑動過程。
listview內的緩存主要就是scrap,離屏就會進入scrap。scrap在layout的時候會進行裁剪,去調尾部的一些view,但是實際上這種情況發生的不多,后邊會詳細說。
layout過程
我測試了下layout的次數,郭神文章說的是2次,我這里會有3次。
第一次layout
onLayout -gt; layoutChildren -gt; fillFromTop-gt; fillDown-gt; while() makeAndAddView makeAndAddView -gt; 1、obtainView -gt; getView -gt; inflate 2、setupChild -gt; addViewInLayout -gt;child.measure -gt;child.layout
第二次layout
onLayout -gt; layoutChildren -gt; 1、fillActiveViews 2、detachAllViewsFromParent 3、fillSpecific-gt; fillDown-gt;while() makeAndAddView makeAndAddView -gt; getActiveView -gt;setupChild -gt; attachViewToParent
第三次layout
onLayout -gt; layoutChildren -gt; 1、fillActiveViews 2、detachAllViewsFromParent 3、fillSpecific-gt; fillDown-gt;while() makeAndAddView makeAndAddView -gt; getActiveView -gt;setupChild -gt; attachViewToParent -gt;child.measure -gt;child.layout
可以看到后2次基本差不多,區別在于在setupChild內是否要執行child的measure和layout。為什么第3次layout會調用measure和layout,而第二次不會呢?看下邊的代碼,差別就在于第三次child.isLayoutRequested()變為了true。
//setupChild final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
我在重新捋一下流程
1、ViewRootImpl#dispatchResized被調用,發出MSG_RESIZED_REPORT消息
2、第一次ListView:layoutChildren
3、第二次ListView:layoutChildren
4、收到發出MSG_RESIZED_REPORT消息,ViewRootImpl#forceLayout
5、第三次ListView:layoutChildren
所以第三次ListView:layoutChildren的時候會觸發child.measure和child.layout。奇怪的是,每次都是第2次layout之后收到MSG_RESIZED_REPORT消息
滑動
對移除屏幕的view addScrapView、detachViewsFromParent
對屏幕內的view offsetChildrenTopAndBottom
對屏幕內空白的地方 fillGap -gt; fillDown-gt;while() makeAndAddView
makeAndAddView -gt; obtainView、setupChild
obtainView-》getScrapView-》adapter.getView(convertview....)
void fillGap(boolean down) { final int count = getChildCount(); if (down) { final int startOffset = count gt; 0 ? getChildAt(count - 1).getBottom() mDividerHeight : getListPaddingTop(); //手指向上滑動,所以需要填充底部 fillDown(mFirstPosition count, startOffset); correctTooHigh(getChildCount()); } else { final int startOffset = count gt; 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - getListPaddingBottom(); fillUp(mFirstPosition - 1, startOffset); correctTooLow(getChildCount()); } }
滑動過程中不會調用onMeasure或者onLayout
RecycleBin基本成員與方法
view的回收復用主要就依靠RecycleBin,所以重點分析下RecycleBin
mScrapViews
RecycleBin內有個垃圾箱,mScrapViews用來存放移除屏幕的view。
private ArrayListlt;Viewgt;[] mScrapViews; private ArrayListlt;Viewgt; mCurrentScrap = mScrapViews[0];;
為什么是個數組呢?數組的每一項都是個ArrayListlt;Viewgt;,代表著某個type的垃圾view集合.如果只有一種type,那么垃圾都存在mScrapViews[0]內,mCurrentScrap = scrapViews[0];如果只有一個類型,我們直接操作mCurrentScrap即可
addScrapView
addScrapView就是把一個view加入到垃圾箱內,一般在view離開屏幕的時候調用。如果數據未變,adapter有stable IDs,有暫態,那就不會被收到垃圾箱里,會存著備用。。如果是header、footer那么就放入mSkippedScrap內,不放入mScrapViews。如果是暫態而且有有stable IDs,就丟到mTransientStateViewsById里面去。如果不需要stable IDs,數據未變可以丟到mTransientStateViews
/** * Puts a view into the list of scrap views. * lt;pgt; * If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * * @param scrap The view to add * @param position The view's position within its parent */ void addScrapView(View scrap, int position) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); if (lp == null) { // Can't recycle, but we don't know anything about the view. // Ignore it completely. return; } lp.scrappedFromPosition = position; // Remove but don't scrap header or footer views, or views that // should otherwise not be recycled. final int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { // Can't recycle. If it's not a header or footer, which have // special handling and should be ignored, then skip the scrap // heap and we'll fully detach the view later. if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { getSkippedScrap().add(scrap); } return; } scrap.dispatchStartTemporaryDetach(); // The the accessibility state of the view may change while temporary // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. final boolean scrapHasTransientState = scrap.hasTransientState(); if (scrapHasTransientState) { if (mAdapter != null mAdapterHasStableIds) { // If the adapter has stable IDs, we can reuse the view for // the same data. //是暫態view,并且需要stable ID就丟到mTransientStateViewsById里面去 if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArraylt;gt;(); } mTransientStateViewsById.put(lp.itemId, scrap); } else if (!mDataChanged) { // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArraylt;gt;(); } //數據未變可以丟到mTransientStateViews mTransientStateViews.put(position, scrap); } else { // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } else { if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } } }
retrieveFromScrap
從scrap里取view,核心代碼如下,如果是固定id的,那就根據adapter的id來找,否則就根據scrappedFromPosition 來找,比如第7個item被回收到scrap里了,記下這個view的scrappedFromPosition為7, 那下次滑回第7個item,就盡量給scrappedFromPosition為7的view給他,簡單的說就是從哪里回收來的,還回哪里去。如果根據scrappedFromPosition找不到,那就直接取scrap的最后一個
if (mAdapterHasStableIds) { final long id = mAdapter.getItemId(position); if (id == params.itemId) { return scrapViews.remove(i); } } else if (params.scrappedFromPosition == position) { final View scrap = scrapViews.remove(i); clearAccessibilityFromScrap(scrap); return scrap; } final View scrap = scrapViews.remove(size - 1); return scrap;
activeViews
這個有什么意義,沒看懂。根據上面的分析,在第二次layout的過程中,首先會把當前屏幕的itemview給detach掉,扔到activeViews內,然后又把他們抓出來,給attach上,此時activeViews必定為空,如果不為空,把殘余的view丟到mScrapViews內(scrapActiveViews) 我實在不明白這么搞有什么意義。
shouldRecycleViewType
根據type類型來確定這個view是否能回收,type類型一般可以在adapter里指定,但是系統默認提供了2個類型,一個是ITEM_VIEW_TYPE_IGNORE=-1,一個是ITEM_VIEW_TYPE_HEADER_OR_FOOTER=-2。第二個很明顯就是listview的頭和尾。第一個是什么呢?如果我們希望某個view不被回收的話,可以設置ITEM_VIEW_TYPE_IGNORE,這樣就可以了。(recyclerView有類似的嗎?)
public boolean shouldRecycleViewType(int viewType) { return viewType gt;= 0; }
mRecyclerListener
當發生View回收時,mRecyclerListener若有注冊,則會通知給注冊者.RecyclerListener接口只有一個函數onMovedToScrapHeap,指明某個view被回收到了scrap heap.可以在這個接口回調里進行昂貴資源的回收(比如bitmap)。可以直接用listview來注冊監聽者.
listview.setRecyclerListener(new AbsListView.RecyclerListener() { @Override public void onMovedToScrapHeap(View view) { } });
點擊item
點擊一個item,是和第二次layout類似的,會調用layoutChildren,然后把界面上的view抓起來丟到activeViews內,然后又重新填充,setupChild內不會調用measure和layout
onTouchUp -gt; layoutChildren -gt; 1、fillActiveViews 2、detachAllViewsFromParent 3、fillSpecific-gt; fillDown-gt;while() makeAndAddView makeAndAddView -gt; getActiveView -gt;setupChild -gt; attachViewToParent
跟第二次layout的區別就是沒有調用child的onMeasure和onLayout,關鍵代碼如下,這里needToMeasure為false
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
Header
我們可以輕易的給使用addHeaderView一個listview加上header。
我知道 addHeaderView必須在setAdapter之前,可以add多個head 。
那么問題來了,為什么addHeaderView必須在setAdapter之前?
看setAdapter的部分代碼可以明白,如果之前設置了header,那mAdapter將會被包裝起來HeaderViewListAdapter
//setAdapter if (mHeaderViewInfos.size() gt; 0|| mFooterViewInfos.size() gt; 0) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; }
再看看HeaderViewListAdapter,看下邊的代碼可以看到實際上HeaderViewListAdapter實現了Adapter的各種接口,比如getCount,getItem,getItemViewType,getView,這就是把原來的adapter進行包裝,然后實現對應接口,把Header作為一種特殊類型AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER,在ListView看來,他就是一個普通的adapter。
//HeaderViewListAdapter public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { public int getCount() { if (mAdapter != null) { return getFootersCount() getHeadersCount() mAdapter.getCount(); } else { return getFootersCount() getHeadersCount(); } } public Object getItem(int position) { // Header (negative positions will throw an IndexOutOfBoundsException) int numHeaders = getHeadersCount(); if (position lt; numHeaders) { return mHeaderViewInfos.get(position).data; } // Adapter final int adjPosition = position - numHeaders; int adapterCount = 0; if (mAdapter != null) { adapterCount = mAdapter.getCount(); if (adjPosition lt; adapterCount) { return mAdapter.getItem(adjPosition); } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) return mFooterViewInfos.get(adjPosition - adapterCount).data; } public View getView(int position, View convertView, ViewGroup parent) { // Header (negative positions will throw an IndexOutOfBoundsException) int numHeaders = getHeadersCount(); if (position lt; numHeaders) { return mHeaderViewInfos.get(position).view; } // Adapter final int adjPosition = position - numHeaders; int adapterCount = 0; if (mAdapter != null) { adapterCount = mAdapter.getCount(); if (adjPosition lt; adapterCount) { return mAdapter.getView(adjPosition, convertView, parent); } } // Footer (off-limits positions will throw an IndexOutOfBoundsException) return mFooterViewInfos.get(adjPosition - adapterCount).view; } public int getItemViewType(int position) { int numHeaders = getHeadersCount(); if (mAdapter != null position gt;= numHeaders) { int adjPosition = position - numHeaders; int adapterCount = mAdapter.getCount(); if (adjPosition lt; adapterCount) { return mAdapter.getItemViewType(adjPosition); } } return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; } }
我們在分析下header是否會被多次創建,是否會被丟到scrap里去
首先看,滑動的時候會不會回收header,這里明顯可以看到position的限制,header和footer是不會被回收的。既然不會回收,那下次再滑到header的時候還是找adapter要,看上邊的adapte的getView代碼,mHeaderViewInfos.get(position).view,只是從mHeaderViewInfos.get內取,所以header是不會被回收的,永遠存在mHeaderViewInfos里面。這里可以得到啟發,Recyclerview是不支持header,footer的,那我們是不是可以針對Recyclerview來一次類似的包裝,讓他支持header,footer
//trackMotionScroll if (position gt;= headerViewsCount position lt; footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. child.clearAccessibilityFocus(); mRecycler.addScrapView(child, position); }
notifyDataSetChanged
之前一直沒有說過,數據發生變化的情況會怎么樣,我們都知道,數據發生變化調用adpater的notifyDataSetChanged就會刷新界面。這里面的原理是什么? 這里面有個觀察者模式,BaseAdapter內有個mDataSetObservable,AbsListView在onAttachedToWindow的時候會注冊觀察者,代碼如下,這樣就注冊了一個觀察者mDataSetObserver
//AbsListView#onAttachedToWindow if (mAdapter != null mDataSetObserver == null) { mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); // Data may have changed while we were detached. Refresh. mDataChanged = true; mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); }
notifyDataSetChanged會調用mDataSetObserver.onChanged,里面更新了mItemCount,然后調用了rememberSyncState和requestLayout。
@Override public void onChanged() { mDataChanged = true; mOldItemCount = mItemCount; mItemCount = getAdapter().getCount(); // Detect the case where a cursor that was previously invalidated has // been repopulated with new data. if (AdapterView.this.getAdapter().hasStableIds() mInstanceState != null mOldItemCount == 0 mItemCount gt; 0) { AdapterView.this.onRestoreInstanceState(mInstanceState); mInstanceState = null; } else { rememberSyncState(); } checkFocus(); requestLayout(); }
這里rememberSyncState比較陌生,實際上他做的事情也很少,主要就2行代碼,設置mSyncMode和mSyncPosition。重新布局的時候默認有個原則,之前誰在第一個,那么這次誰還是在第一個.
mSyncPosition = mFirstPosition; mSyncMode = SYNC_FIRST_POSITION;
然后我們看又一次layout的過程
首先,handleDataChanged會定下mSyncPosition,然后把
mLayoutMode = LAYOUT_SYNC;,這個mLayoutMode后邊會用到
第二步,因為dataChanged所以這里直接把所有的界面上的view丟到scrap里,不像以前放在activeViews里
第三步,detachAllViewsFromParent
第四步,fillSpecific是因為mLayoutMode是LAYOUT_SYNC所以直接調用fillSpecific。
里面的getView是adapter的getView一般在這里設置實際view的內容(比如文本圖片)。所以view一般都會設置為PFLAG_FORCE_LAYOUT,所以會重新measure、layout。(這里可以再思考下,其實大部分情況下,重用view,并不用重新measure,而layout的時候只要把item往listview的框里丟就可以了,item內部也不需要layout,這樣應該能夠提供效率,但是看了代碼后發現setupChild內是根據needToMeasure來決定是否measure、layout的,不能分別對待,哎。)
onLayout -gt; layoutChildren -gt; 1、handleDataChanged:定個mSyncPosition、mLayoutMode = LAYOUT_SYNC; 2、for() addScrapView 3、detachAllViewsFromParent 4、fillSpecific-gt; fillDown-gt;while() makeAndAddView makeAndAddView -gt; obtainView-gt;getView -gt;setupChild -gt; attachViewToParent -gt;child.measure -gt;child.layout
這次layout跟之前的區別主要是第二步和第四步。
listview動畫錯亂
listview的item如果在執行動畫的同時,listview在滑動,我們知道listview滑動過程中,是會重用view的,所以可能本來針對position 為1的動畫,跑到position為11的地方去了,所以我們得禁止這個view進入scrap,如何禁止?
setHasTransientState(true),讓view進入暫態
setHasTransientState是API16引入的函數,在View里,下邊是對他的介紹,主要是用于動畫開始和結束,在開始的時候setHasTransientState(true),結束的時候setHasTransientState(false),在這之間就是暫態的。
常見用法如下,在動畫開始的時候進入暫態,動畫結束退出暫態。我們再對比listview的代碼可以發現,進入暫態的view不會進入scrap,而是進入mTransientStateViewsById這個LongSparseArray內,這樣就不會被重用而導致動畫錯亂了。
//Listview 的 OnItemClickListener 的內容 //本范例點了 item 后會淡出并刪除該 item public void onItemClick(AdapterView parent, final View view, int position, long id) { final String item = (String) parent.getItemAtPosition(position); ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, 0); anim.setDuration(500); view.setHasTransientState(true); //設為 true 宣告 item 要被追蹤 anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { myListview.remove(item); adapter.notifyDataSetChanged(); //重新整理 listview view.setAlpha(1); view.setHasTransientState(false); //完成后設定回 false } }); anim.start(); }
不可否認這是一種解決方法,但并不是好的解決方法,因為item都滑出去了,還在搞動畫,沒啥意義,真正好的方法是什么,如下所示,在onMovedToScrapHeap里面停止動畫,這才是最合適的。
listView.setRecyclerListener(new RecyclerListener() { @Override public void onMovedToScrapHeap(View view) { // Stop animation on this view } });
hasStableIds
adapter有個接口叫hasStableIds,這個有什么意義,我查了資料和代碼。發現hasStableIds一般情況下都是false,只有2個情況是true。
什么樣的adapter是stable的,個人以為是里面的數據的id不變化,數據可以變化,但是id不能變化。
首先,如果要用到listview的選中功能時,只有hasStableIds返回true,才能通過getCheckedItemIds方法才能正常獲取用戶選中的選項的id(當然adapter內必須復寫getItemId)。
還有個地方就是CursorAdapter,因為cursor是sql查詢的結果,所以說是stable的無可厚非。CursorAdapter里面的hasStableIds就是返回true的。
總的來說hasStableIds沒啥用,我也沒看到改為true能優化什么。
問題
addViewInLayout和attachViewToParent有什么區別呢
addViewInLayout和attachViewToParent兩者接收的參數是一樣的,主要功能也相似,也就是往ViewGroup的view數組里添加View, 但是調用addViewInLayout會使被添加的View在界面上添加時會有動畫效果呈現。兩者的使用場景差別也很明顯了:一般來說某一個View第一次添加進ViewGroup時比較適合調用addViewInLayout,而以后同一個View再次被添加時則適合使用attachViewToParent。因為一般情況想我們會希望進入的動畫效果執行一次就夠了,而不需要多次執行。
具體可參考 http://www.itdadao.com/articles/c15a444236p0.html
scap有數量限制嗎
在滑動過程中scrap是沒限制的,但是在layout的過程中調用scrapActiveViews-gt;pruneScrapViews,在這里會把mScrapViews內的每組緩存,都限制在mActiveViews.length大小。
scrap里的view會被去掉嗎
為什么要考慮這個問題呢?因為有的view創建成本很高,我們不希望重復創建,什么情況下會重復創建呢?那就是view離屏,進scrap,scrap裁剪,被裁剪的view就沒有了,下次必須重新inflate出來。
我看了下要想remove scrap里的view,只有pruneScrapViews和clear方法,clear在設置setAdapter(ListAdapter)和onDetachedFromWindow()時會被調用。而pruneScrapViews是在layout過程中被調的。所以主要看pruneScrapViews。這里可以看到其實處理scrap長度的方法是比較粗暴的,查一下mActiveViews.length,任何一個scrap堆都不準超過這個長度,否則直接截尾。于是我又看了下mActiveViews,每次顯示在界面上的view都會丟到mActiveViews里,又發現 mActiveViews只會變長不會變短 。這就有意思了,比如當前頁面有5個item,那mActiveViews.length就是5,待會當前頁面有10個item了,那mActiveViews.length就是10,再過一塊又只有3個item了,那mActiveViews.length還是10(只增不減)。要注意一點只有在layout的時候才會更新mActiveViews.length,如果只是滑來滑去是不會觸發layout的。所以如果,mActiveViews.length值比較小,而scrap的item又很多的話,會進入到L10,進行裁剪(一般會發生在header高度比較大的情況下),這種裁剪方式其實是比較奇怪的,憑什么根據mActiveViews.length來裁剪。
所以 scrap里的view是有可能被丟棄的,但是如果某個scap堆里只有一個view,那放心,他絕不會被丟棄。
另外如果我們希望緩存的view數量多一些的話,我們可以在view比較多的時候掉一遍requestLayout,這樣讓他更新mActiveViews.length
final int maxViews = mActiveViews.length; final int viewTypeCount = mViewTypeCount; final ArrayListlt;Viewgt;[] scrapViews = mScrapViews; for (int i = 0; i lt; viewTypeCount; i) { final ArrayListlt;Viewgt; scrapPile = scrapViews[i]; int size = scrapPile.size(); final int extras = size - maxViews; size--; for (int j = 0; j lt; extras; j ) { removeDetachedView(scrapPile.remove(size--), false); } }
其他
1、layoutChildren必定調用invalidate
2、initAbsListView內設置ListView本身可以點擊即可以消耗父View分發的事件: setClickable(true);
3、我們常常用的convertView實際上來自scrapView
ref
Tags: ListView
文章來源:http://www.jianshu.com/p/62bf84c183ac