ListView回收機制相關分析

yangxi_001發表於2017-07-31

所用原始碼版本為最新的Android 4.4.2(API 19)。更新中……

 

ListView回收機制相關分析    1

1.    ListView結構關係    1

2.    RecycleBin類解析    3

2.1 RecycleBin變數    4

2.2 RecycleBin方法    4

3.    RecycleBin的呼叫和關鍵方法    7

3.1 ListView    7

3.1.1 layoutChildren    7

3.1.2 makeAndAddView    8

3.1.3 setupChild    9

3.1.4 setAdapter    9

3.1.5 scrollListItemsBy    10

3.2 AbsListView    12

3.2.1 obtainView    12

3.2.2 trackMotionScroll    14

4. 用到的關鍵類    15

4.1 ViewType的使用    15

4.2 TransientStateView    17

 

 

  1. 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中。

  1. 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重新layoutviewforceLayout()方法只是設定標誌,並不會通知其parent來重新layout

shouldRecycleViewType

():判斷給定的viewviewType指明是否可以回收回。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中的資料個數未發生變化時,此時使用者可能只是滾動,或點選等操作,ListViewitem的個數會發生變化,因此,需要將可視的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中移除。positionadapter中的絕對下標值,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,然後從視窗中detachview,並根據viewType加入到mScrapView中。

該方法會呼叫mRecyclerListener 介面的函式onMovedToScrapHeap(6734)

) {

.onMovedToScrapHeap(scrap);

}

mRecyclerListener的設定可通過AbsListView的setRecyclerListener方法。

當view被回收準備再利用的時候設定要通知的監聽器, 可以用來釋放跟view有關的資源。這點似乎很有用。

public void setRecyclerListener(RecyclerListener listener) {

 = 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.

 i=0; i<size; i++) {

View view = scrapViews.get(i);

 (((AbsListView.LayoutParams)view.getLayoutParams())

.scrappedFromPosition == position) {

scrapViews.remove(i);

 view;

}

}

 scrapViews.remove(size - 1);

else {

;

}

其中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,但是快取中沒有,因此convertViewnull,所以,我們得分配一塊記憶體來建立新的convertView

        b. 第二種情況:

a中,我們繼續向上滾動,直接第一個view完全移出螢幕(假設沒有新的item),此時,第一個view就會被detach,並被加入到mScrapView中;然後,我們還繼續向上滾動,直接後面又將要顯示新的item view時,此時,系統會從mScrapView中找position對應的View,顯然,是找不到的,則將從mScrapView中,取最後一個快取的view傳遞給convertView

        c. 第一種情況:

緊接著在b中,第一個被完全移出,加入到mScrapView中,且沒有新增的itemlistview中,此時,快取中就只有第一個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繪置它們的背景色。

  1. 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) {

 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:新增viewlistview中;

step3:回收mActiveView中的未使用的viewmScrapView中;

    注:在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);

) {

// 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);

 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(mHeaderViewInfosmFooterViewInfos, adapter);

else {

mAdapter = adapter;

}

mOldSelectedPosition = INVALID_POSITION;

mOldSelectedRowId = INVALID_ROW_ID;

// AbsListView#setAdapter will update choice mode states.

.setAdapter(adapter);

 

) {

 = mAdapter.areAllItemsEnabled();

mOldItemCount = mItemCount;

mItemCount = mAdapter.getCount();

checkFocus();

mDataSetObserver = new AdapterDataSetObserver();//註冊headerview的觀察者

mAdapter.registerDataSetObserver(mDataSetObserver);//在RecycleBin物件mRecycler記錄下item型別的數量

mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());

 position;

 (mStackFromBottom) {

position = lookForSelectablePosition(mItemCount - 1, false);

else {

position = lookForSelectablePosition(0, true);

}

setSelectedPositionInt(position);//AdapterView中的方法,記錄當前的position

setNextSelectedPositionInt(position);//AdapterView中的方法,記錄下一個position

 (mItemCount == 0) {

// Nothing selected

checkSelectionChanged();

}

else {

;

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最頂部位置

 AbsListView.RecycleBin recycleBin = mRecycler;

 (amount < 0) {

// shifted items up

// may need to pan views into the bottom space

 numChildren = getChildCount();

View last = getChildAt(numChildren - 1);

while (last.getBottom() < listBottom) {//最後的view高於底部時新增下一個view

lastVisiblePosition 

 (lastVisiblePosition < mItemCount - 1) {

last = addViewBelow(last, lastVisiblePosition);

numChildren++;

else {

;

}

}

// 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();

 (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());

}

 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();

 (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);

) {

child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);

}

 (child != scrapView) {

//如果重用的scrapView和adapter獲得的view是不一樣的,將scrapView進行回收  

mRecycler.addScrapView(scrapView, position);// scrapView 仍然放入回收站

 != 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.

 (child.isAccessibilityFocused()) {

child.clearAccessibilityFocus();

}

child.dispatchFinishTemporaryDetach();

}

}else {//回收站中沒有拿到資料,就只能夠自己去inflate一個xml佈局檔案,或者new一個view

child = mAdapter.getView(position, nullthis); //當getview中傳入的

converView=null的時候會在getView的方法中進行新建這個view  

) {

child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);

}

 != 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) {

 top = -incrementalDeltaY;

) {

top += listPadding.top;

}

 i = 0; i < childCount; i++) {

 View child = getChildAt(i);

 (child.getBottom() >= top) {

;

else {

count++;

 position = firstPosition + i;

 (position >= headerViewsCount && position < footerViewsStart) {

// The view will be rebound to new data, clear any

// system-managed transient state.

 (child.isAccessibilityFocused()) {

child.clearAccessibilityFocus();

}

mRecycler.addScrapView(child, position);//放入回收站

}

}

}

else {

 bottom = getHeight() - incrementalDeltaY;

) {

bottom -= listPadding.bottom;

}

 i = childCount - 1; i >= 0; i--) {

 View child = getChildAt(i);

 (child.getTop() <= bottom) {

;

else {

start = i;

count++;

 position = firstPosition + i;

 (position >= headerViewsCount && position < footerViewsStart) {

// The view will be rebound to new data, clear any

// system-managed transient state.

 (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動畫播放等情況中。


轉載自:http://www.cnblogs.com/qiengo/p/3628235.html

相關文章