ListView回收機製相關分析
ListView回收機製相關分析
最初的分析文檔為word,該文檔是直接從word文檔發布,布局未做詳細調整,湊合看吧。
所用源碼版本為最新的Android 4.4.2(API 19)。更新中……
-
ListView結構關係
首先理清listview的層級關係,
使用Google Online Draw 畫出繼承關係圖如下:
圖中單獨畫出Scrollview是為了說明該ViewGroup並沒有自帶回收機製,如果要是Scrollview顯示大量view,需要手動做處理。
重要的類有三個:Listview、AbsListView、AdapterView。各個類的大小如下:
-
Listview 3800
-
AbsListView 6920
-
AdapterView 1208
從Listview開始, ListView的初始化ListVIew.onLayout過程與普通視圖的layout過程不同,流程圖如下。從左向右,從上向下。
視圖的創建過程的都會執行的三個步驟: onMeasure, onLayout, onDraw
圖中可以看出重要的類有三個:Listview、AbsListView、AdapterView。主要的回收類RecycleBin位於AbsListView中。
-
RecycleBin類解析
位於AbsListView中,6466-6900行。
AbsListView的源碼中可以看到有個RecycleBin 對象mRecycler。(317行, The data set used to store unused views that should be reused during the next layout to avoid creating new ones. 用於存儲不用的view,以便在下個layout中使用來避免創建新的。)注釋說明:
The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the start of a layout. By construction, they are displaying current information. At the end of layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that could potentially be used by the adapter to avoid allocating views unnecessarily.
大意是使用兩級view來進行回收:
ActiveView
:激活view,當期顯示在屏幕上的激活的view。
ScrapView
:廢棄view,被刪除的ActiveView會被自動加入ScrapView。
然後看看RecycleBin內部的重要的的變量和方法:
2.1 RecycleBin變量
mRecyclerListener
: 當發生View回收時,mRecyclerListener若有注冊,則會通知給注冊者.RecyclerListener接口隻有一個函數onMovedToScrapHeap,指明某個view被回收到了scrap heap. 該view不再被顯示,任何相關的昂貴資源應該被丟棄。該函數是處理回收時view中的資源釋放。
mFirstActivePosition
:The position of the first view stored in mActiveViews.存儲在mActiveViews中的第一個view的位置,即getFirstVisiblePosition。
mActiveViews
: Views that were on screen at the start of layout. This array is populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. Views in mActiveViews represent a contiguous range of Views, with position of the first view store in mFirstActivePosition.布局開始時屏幕顯示的view,這個數組會在布局開始時填充,布局結束後所有view被移至mScrapViews。
mScrapViews
:ArrayList<View>[] Unsorted views that can be used by the adapter as a convert view.可以被適配器用作convert view的無序view數組。 這個ArrayList就是adapter中getView方法中的參數convertView的來源。注意:這裏是一個數組,因為如果adapter中數據有多種類型,那麼就會有多個ScrapViews。
mViewTypeCount
:view類型總數,列表中可能有多種數據類型,比如內容數據和分割符。
mCurrentScrap
:跟mScrapViews的卻別是,mScrapViews是個隊列數組,ArrayList<View>[]類型,數組長度為mViewTypeCount,而默認ViewTypeCount = 1的情況下mCurrentScrap=mScrapViews[0]。
下麵三個參數分別對應addScrapView中scrapHasTransientState的三個情況
-
mTransientStateViews: If the data hasn't changed, we can reuse the views at their old positions.
-
mTransientStateViewsById: If the adapter has stable IDs,we can reuse the view forthe same data.
-
mSkippedScrap:Otherwise, we'll have to remove the view and start over.
2.2 RecycleBin方法
markChildrenDirty
():為每個子類調用forceLayout()。將mScrapView中回收回來的View設置一樣標誌,在下次被複用到ListView中時,告訴viewroot重新layout該view。forceLayout()方法隻是設置標誌,並不會通知其parent來重新layout。
shouldRecycleViewType
():判斷給定的view的viewType指明是否可以回收回。viewType < 0可以回收。指定忽略的( ITEM_VIEW_TYPE_IGNORE = -1),或者是 HeaderView / (FootViewITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2)是不被回收的。如有特殊需要可以將自己定義的viewType設置為-1,否則,將會浪費內存,導致OOM。
clear
() :Clears the scrap heap.清空廢棄view堆,並將這些View從窗口中Detach。
fillActiveViews
(int childCount, int firstActivePosition):Fill ActiveViews with all of the children of the AbsListView. childCount:The minimum number of views mActiveViews should hold. firstActivePosition:The position of the first view that will be stored in mActiveViews.用AbsListView.的所有子view填充ActiveViews,其中childCount是mActiveViews應該保存的最少的view數,firstActivePosition是mActiveViews中存儲的首個view的位置。從代碼看該方法的處理邏輯為將當前AbsListView的0-childCount個子類中的非header、footer類添加到mActiveViews數組中。當Adapter中的數據個數未發生變化時,此時用戶可能隻是滾動,或點擊等操作,ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理。
getActiveView
(int position):Get the view corresponding to the specified position. The view will be removed from mActiveViews if it is found. 獲取mActiveViews中指定位置的view,如果找到會將該view從mActiveViews中移除。position是adapter中的絕對下標值,mFirstActivePosition前麵說過了,是當前可視區域的下標值,對應在adapter中的絕對值,如果找到,則返回找到的View,並將mActiveView對應的位置設置為null。
clearTransientStateViews()
:Dump any currently saved views with transient state.清掉當前處於transient(轉換)狀態的所有保存的view。內部為mTransientStateViews和mTransientStateViewsById的clear()調用。
addScrapView
(View scrap, int position):將view放入scrapview list中。If the list data hasn't changed or the adapter has stable IDs, views with transient state will be preserved for later retrieval. scrap:要添加的view。Position:view在父類中的位置。放入時位置賦給scrappedFromPosition 。有transient狀態的view不會被scrap(廢棄),會被加入mSkippedScrap。
就是將移出可視區域的view,設置它的scrappedFromPosition,然後從窗口中detach該view,並根據viewType加入到mScrapView中。
該方法會調用mRecyclerListener 接口的函數onMovedToScrapHeap(6734)
if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } |
mRecyclerListener的設置可通過AbsListView的setRecyclerListener方法。
當view被回收準備再利用的時候設置要通知的監聽器, 可以用來釋放跟view有關的資源。這點似乎很有用。
public void setRecyclerListener(RecyclerListener listener) { mRecycler.mRecyclerListener = listener; } |
getScrapView
(int position) :A view from the ScrapViews collection. These are unordered.該方法中調用了retrieveFromScrap(ArrayList<View> scrapViews, int position)。
retrieveFromScrap
(ArrayList<View> scrapViews, int position):無注釋。(6902)該方法屬於AbsListView。
int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i=0; i<size; i++) { View view = scrapViews.get(i); if (((AbsListView.LayoutParams)view.getLayoutParams()) .scrappedFromPosition == position) { scrapViews.remove(i); return view; } } return scrapViews.remove(size - 1); } else { return null; } |
其中scrappedFromPosition :The position the view was removed from when pulled out of the scrap heap.(6412)根據position,從mScrapView中找:
1. 如果有view.scrappedFromPosition = position的,直接返回該view;
2. 否則返回mScrapView中最後一個;
3. 如果緩存中沒有view,則返回null;
a. 第三種情況,這個最簡單:
一開始,listview穩定後,顯示N個,此時mScrapView中是沒有緩存view的,當我們向上滾動一小段距離(第一個此時仍顯示部分),新的view將會顯示,此時listview會調用Adapter.getView,但是緩存中沒有,因此convertView是null,所以,我們得分配一塊內存來創建新的convertView;
b. 第二種情況:
在a中,我們繼續向上滾動,直接第一個view完全移出屏幕(假設沒有新的item),此時,第一個view就會被detach,並被加入到mScrapView中;然後,我們還繼續向上滾動,直接後麵又將要顯示新的item view時,此時,係統會從mScrapView中找position對應的View,顯然,是找不到的,則將從mScrapView中,取最後一個緩存的view傳遞給convertView;
c. 第一種情況:
緊接著在b中,第一個被完全移出,加入到mScrapView中,且沒有新增的item到listview中,此時,緩存中就隻有第一個view;然後,我此時向下滑動,則之前的第一個item,將被顯示出來,此時,從緩存中查找position對應的view有沒有,當然,肯定是找到了,就直接返回了。
removeSkippedScrap()
:Finish the removal of any views that skipped the scrap heap.清空mSkippedScrap。
scrapActiveViews
():Move all views remaining in mActiveViews to mScrapViews.將mActiveViews 中剩餘的view放入mScrapViews。實際上就是將mActiveView中未使用的view回收(因為,此時已經移出可視區域了)。會調用mRecyclerListener.onMovedToScrapHeap(scrap);
pruneScrapViews
():確保mScrapViews 的數目不會超過mActiveViews的數目 (This can happen if an adapter does not recycle its views)。mScrapView中每個ScrapView數組大小不應該超過mActiveView的大小,如果超過,係統認為程序並沒有複用convertView,而是每次都是創建一個新的view,為了避免產生大量的閑置內存且增加OOM的風險,係統會在每次回收後,去檢查一下,將超過的部分釋放掉,節約內存降低OOM風險。
reclaimScrapViews
(List<View> views):Puts all views in the scrap heap into the supplied list.將mScrapView中所有的緩存view全部添加到指定的view list中,隻看到有AbsListView.reclaimViews有調用到,但沒有其它方法使用這個函數,可能在特殊情況下會使用到,但目前從framework中,看不出來。
setCacheColorHint
(int color):Updates the cache color hint of all known views.更新view的緩存顏色提示setDrawingCacheBackgroundColor。為所有的view繪置它們的背景色。
-
RecycleBin的調用和關鍵方法
3.1 ListView
3.1.1 layoutChildren
1479-1729
1583-當數據發生改變的時候,把當前的view放到scrapviews裏麵,否則標記為activeViews。
// Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } }else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap();//移除所有old views ...... // Flush any cached views that did not get reused above recycleBin.scrapActiveViews();//刷新緩存,將當前的ActiveVies 移動到 ScrapViews。 |
dataChanged,從單詞的意思我們就可以,這裏的優化規則就是基於數據是否有變化,mDataChanged在makeAndAddView(見下文)中有使用。
step1:如果數據發生變化,就將所有view加入到mScrapView中,否則,將所有view放到mActiveView中;
step2:添加view到listview中;
step3:回收mActiveView中的未使用的view到mScrapView中;
注:在step1中,如果是addScrapView,則所有的view將會detach,如果是fillActiveViews,則不會detach,隻有在step3中,未用到的view才會detach。
3.1.2 makeAndAddView
(int position, int y, boolean flow, int childrenLeft,boolean selected)(1772)
Obtain the view and add it to our list of children. The view can be made fresh, converted from an unused view, or used as is if it was in the recycle bin.
View child; if (!mDataChanged) {// 數據沒有更新時,使用以前的view // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned // 對複用的View針對當前需要進行配置。定位並且添加這個view到ViewGrop中的children列表,從回收站獲取的視圖不需要measure,所以最後一個參數為true setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible // 創建或者重用視圖 child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; |
3.1.3 setupChild
(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled)(1812)
Add a view as a child and make sure it is measured (if necessary) and positioned properly.
3.1.4 setAdapter
(ListAdapter adapter) (457)
Sets the data behind this ListView. The adapter passed to this method may be wrapped by a WrapperListAdapter, depending on the ListView features currently in use. For instance, adding headers and/or footers will cause the adapter to be wrapped.
if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver);//移除了與當前listview的adapter綁定數據集觀察者DataSetObserver } resetList();//重置listview,主要是清除所有的view,改變header、footer的狀態 mRecycler.clear();//清除掉RecycleBin對象mRecycler中所有緩存的view,RecycleBin後麵著重介紹,主要是關係到Listview中item的重用機製,它是AbsListview的一個內部類 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {//判斷是否有headerview和footview mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; // AbsListView#setAdapter will update choice mode states. super.setAdapter(adapter);
if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver();//注冊headerview的觀察者 mAdapter.registerDataSetObserver(mDataSetObserver);//在RecycleBin對象mRecycler記錄下item類型的數量 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position);//AdapterView中的方法,記錄當前的position setNextSelectedPositionInt(position);//AdapterView中的方法,記錄下一個position if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { mAreAllItemsSelectable = true; checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); |
3.1.5 scrollListItemsBy
(int amount)(3012-3082)
對子view滑動一定距離,添加view到底部或者移除頂部的不可見view。從注釋看,不可見的item 的自動移除是在scrollListItemsBy中進行的。
private void scrollListItemsBy(int amount) { offsetChildrenTopAndBottom(amount); final int listBottom = getHeight() - mListPadding.bottom;//獲取listview最底部位置 final int listTop = mListPadding.top; //獲取listview最頂部位置 final AbsListView.RecycleBin recycleBin = mRecycler; if (amount < 0) { // shifted items up // may need to pan views into the bottom space int numChildren = getChildCount(); View last = getChildAt(numChildren - 1); while (last.getBottom() < listBottom) {//最後的view高於底部時添加下一個view final int lastVisiblePosition = mFirstPosition + numChildren - 1; if (lastVisiblePosition < mItemCount - 1) { last = addViewBelow(last, lastVisiblePosition); numChildren++; } else { break; } } // may have brought in the last child of the list that is skinnier // than the fading edge, thereby leaving space at the end. need // to shift back if (last.getBottom() < listBottom) {//到達最後一個view offsetChildrenTopAndBottom(listBottom - last.getBottom()); } // top views may be panned off screen View first = getChildAt(0); while (first.getBottom() < listTop) {//頂部view移除屏幕時 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(first, mFirstPosition); //回收view } detachViewFromParent(first); //從父類中移除 first = getChildAt(0); //這行好像沒用啊。。。。 mFirstPosition++; } } else { // shifted items down View first = getChildAt(0); // may need to pan views into top while ((first.getTop() > listTop) && (mFirstPosition > 0)) {//頂部view上部有空間時添加view。 first = addViewAbove(first, mFirstPosition); mFirstPosition--; } // may have brought the very first child of the list in too far and // need to shift it back if (first.getTop() > listTop) {//到達第一個view offsetChildrenTopAndBottom(listTop - first.getTop()); } int lastIndex = getChildCount() - 1; View last = getChildAt(lastIndex); // bottom view may be panned off screen while (last.getTop() > listBottom) {//底部view移除屏幕的情況 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(last, mFirstPosition+lastIndex); } detachViewFromParent(last); last = getChildAt(--lastIndex); } } } |
從以上代碼可以看出,Android中view回收的計算是其父view中不再顯示的,如果scrollview中包含了一個wrap_content屬性的listview,裏麵的內容並不會有任何回收,引起listview 的getheight函數獲取的是一個足以顯示所有內容的高度。
3.2 AbsListView
3.2.1 obtainView
(int position, boolean[] isScrap)(2227)
Get a view and have it show the data associated with the specified position. 當這個方法被調用時,說明Recycle bin中的view已經不可用了,那麼,現在唯一的方法就是,convert一個老的view,或者構造一個新的view。
position: 要顯示的位置
isScrap: 是個boolean數組, 如果view從scrap heap獲取,isScrap [0]為true,否則為false。
isScrap[0] = false; View scrapView; scrapView = mRecycler.getTransientStateView(position); if (scrapView == null) { // 查看回收站中是否有廢棄無用的View,如果有,則使用它,無需New View。 scrapView = mRecycler.getScrapView(position); } View child; if (scrapView != null) { //此時說明可以從回收站中重新使用scrapView。 child = mAdapter.getView(position, scrapView, this); if(child.getImportantForAccessibility()== IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (child != scrapView) { //如果重用的scrapView和adapter獲得的view是不一樣的,將scrapView進行回收 mRecycler.addScrapView(scrapView, position);// scrapView 仍然放入回收站 if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { //如果重用的view和adapter獲得的view是一樣的,將isScrap[0]值為true,否則默認為false isScrap[0] = true; // Clear any system-managed transient state so that we can // recycle this view and bind it to different data. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } child.dispatchFinishTemporaryDetach(); } }else {//回收站中沒有拿到數據,就隻能夠自己去inflate一個xml布局文件,或者new一個view child = mAdapter.getView(position, null, this); //當getview中傳入的 converView=null的時候會在getView的方法中進行新建這個view if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } |
3.2.2 trackMotionScroll
(int deltaY, int incrementalDeltaY)(4991)
監視滑動動作
deltaY: Amount to offset mMotionView. This is the accumulated delta since the motion began. 正數表示向下滑動。
incrementalDeltaY :Change in deltaY from the previous event.
....... // 滾動時,不在可見範圍內的item放入回收站。。。。。。。 if (down) { int top = -incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } else { int bottom = getHeight() - incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { bottom -= listPadding.bottom; } for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } |
4. 用到的關鍵類
4.1 ViewType的使用
在listview中當有多種viewtype的時候,在adapter中繼承設置getItemViewType方法可以更有效率 。示例如下:
....... private static final int TYPE_ITEM = 0; private static final int TYPE_SEPARATOR = 1; private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;
@Override public int getItemViewType(int position) { return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM; } @Override public int getViewTypeCount() { return TYPE_MAX_COUNT; }
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; int type = getItemViewType(position); if (convertView == null) { holder = new ViewHolder(); switch (type) { case TYPE_ITEM: convertView = mInflater.inflate(R.layout.item1, null); holder.textView = (TextView)convertView.findViewById......; break; case TYPE_SEPARATOR: convertView = mInflater.inflate(R.layout.item2, null); holder.textView = (TextView)convertView.findViewById......; break; } convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } ........ } |
如果實現了RecyclerListener接口,當一個View由於ListView的滑動被係統回收到RecycleBin的mScrapViews數組時,會調用RecyclerListener中的onMovedToScrapHeap(View view)方法。RecycleBin相當於一個臨時存儲不需要顯示的那部分Views的對象,隨著列表滑動,這些Views需要顯示出來的時候,他們就被從RecycleBin中拿了出來,RecycleBin本身並不對mScrapViews中的對象做回收操作。
於是在工程裏,為ListView添加RecyclerListener接口,並在onMovedToScrapHeap方法中釋放ListItem包含的Bitmap資源,這樣可以極大的減少內存占用。
4.2 TransientStateView
用來標記這個view的瞬時狀態,用來告訴app無需關心其保存和恢複。從注釋看,這種具有瞬時狀態的view,用於在view動畫播放等情況中。
轉載自:https://www.cnblogs.com/qiengo/p/3628235.html
最後更新:2017-04-03 05:39:56