本文是閱讀 http://www.cnblogs.com/qiengo/p/3628235.html http://blog.csdn.net/iisPRing/article/details/50967445 http://blog.csdn.net/guolin_blog/article/details/44996879 http://www.jianshu.com/p/9c603a11b0c9 https://my.oschina.net/lorcan/blog/539215 的讀書筆記 代碼是 android N 的,記錄下方便記憶。
ListView 繼承結構 如下圖 
AbsListView 的子類(ListView,GridView)都有RecyleBin機制 可以 防止 OOM,先來看看 RecyleBin,接著看 ListView 的原理(ListView 最終集成自 View,View的子類 主要看 onMeasure()、onLayout(), onDraw())
RecyleBin是view的回收站。
/** * 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,以便在下個layout中使用來避免創建新的 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) * @see android.widget.AbsListView.RecyclerListener */ class RecycleBin { private RecyclerListener mRecyclerListener; /** * The position of the first view stored in mActiveViews. */ private int mFirstActivePosition; /** * 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. */ private View[] mActiveViews = new View[0]; /** * Unsorted views that can be used by the adapter as a convert view. */ private ArrayList<View>[] mScrapViews; ... }ListView中有很多view,屏幕上能看到的view是onScreenView,是RecyleBin中的ActiveView,滑出屏幕的view是OffScreen的View,是RecyleBin中的ScrapView,scrap是廢棄的意思。
ListView把scrapView全部刪除,就不用繪制看不見的view了。ListView會把這些刪除的ScrapView放入到RecycleBin中存起來,就像把暫時無用的資源放到回收站一樣。
當ListView的底部需要顯示新的View的時候,會從RecycleBin中取出一個ScrapView,將其作為convertView參數傳遞給Adapter的getView方法,從而達到View復用的目的,這樣就不必在Adapter的getView方法中執行LayoutInflater.inflate()方法了。
RecyleBin 是防止 ListView 加載數據出現 OOM 的重要原因之一,RecyleBin 是 AbsListView 的內部類,RecycleBin的作用是幫助布局中的View的重用,它存儲了兩種類型的View:
mActiveViews 存儲的是OnScreen的View,這些View很有可能被直接復用mScrapViews 存儲的是OffScreen的View,這些View主要是用來間接復用的,這就是傳回getView中covertView的來源RecyclerListener只有一個方法onMovedToScrapHeap(),addScrapView() 和 scrapActiveViews() 時,使用了該變量,如果注冊了RecyclerListener,就調用onMovedToScrapHeap(),表示該view不再顯示,這個view被回收到了scrap heap,該函數處理回收時view中的資源釋放。
The position of the first view stored in mActiveViews. 存儲在mActiveViews中的第一個view的位置,即AdapterView.getFirstVisiblePosition()返回值。
/** * Returns the position within the adapter's data set for the first item * displayed on screen. * * @return The position within the adapter's data set */ public int getFirstVisiblePosition() { return mFirstPosition; }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。
Unsorted views that can be used by the adapter as a convert view. 這個ArrayList就是adapter中getView方法中的參數convertView的來源。注意:這里是一個數組,因為如果adapter中數據有多種類型,那么就會有多個ScrapViews
view類型總數,列表中可能有多種數據類型,比如內容數據和分割符
private ArrayList mCurrentScrap; 默認情況下,mCurrentScrap = scrapViews[0];
If the data hasn’t changed, we can reuse the views at their old positions.
If the adapter has stable IDs,we can reuse the view forthe same data.
addScrapView()中,如果view不能添加到 mTransientStateViews,mTransientStateViewsById 中,就添加到 mSkippedScrap 中 Otherwise, we’ll have to remove the view and start over.
為每個子類調用forceLayout()。將mScrapView中回收回來的View設置一樣標志,在下次被復用到ListView中時,告訴viewroot重新layout該view。forceLayout()方法只是設置標志,并不會通知其parent來重新layout。
public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; final int scrapCount = scrap.size(); for (int i = 0; i < scrapCount; i++) { scrap.get(i).forceLayout(); } } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; final int scrapCount = scrap.size(); for (int j = 0; j < scrapCount; j++) { scrap.get(j).forceLayout(); } } } if (mTransientStateViews != null) { final int count = mTransientStateViews.size(); for (int i = 0; i < count; i++) { mTransientStateViews.valueAt(i).forceLayout(); } } if (mTransientStateViewsById != null) { final int count = mTransientStateViewsById.size(); for (int i = 0; i < count; i++) { mTransientStateViewsById.valueAt(i).forceLayout(); } } }判斷給定的view的viewType指明是否可以回收回。viewType < 0可以回收。指定忽略的( ITEM_VIEW_TYPE_IGNORE = -1),或者是 HeaderView / FootView(ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2)是不被回收的。如有特殊需要可以將自己定義的viewType設置為-1,否則,將會浪費內存,導致OOM
public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; }清空廢棄view堆,并將這些View從窗口中Detach
/** * Clears the scrap heap. */ void clear() { if (mViewTypeCount == 1) { final ArrayList<View> scrap = mCurrentScrap; clearScrap(scrap); } else { final int typeCount = mViewTypeCount; for (int i = 0; i < typeCount; i++) { final ArrayList<View> scrap = mScrapViews[i]; clearScrap(scrap); } } clearTransientStateViews(); }用AbsListView.的所有子view填充ActiveViews。從代碼看該方法的處理邏輯為將當前AbsListView的0-childCount個子類中的非header、footer的view添加到mActiveViews數組中。當Adapter中的數據個數未發生變化時(用戶滾動,或點擊等操作),ListView中item的個數會發生變化,因此,需要將可視的item加入到mActiveView中來管理
/** * Fill ActiveViews with all of the children of the AbsListView. * * @param childCount The minimum number of views mActiveViews should hold . mActiveViews應該保存的最少的view數 * @param firstActivePosition The position of the first view that will be stored in * mActiveViews. mActiveViews中存儲的首個view的位置,第一個可見的view的位置 */ void fillActiveViews(int childCount, int firstActivePosition) { if (mActiveViews.length < childCount) { mActiveViews = new View[childCount]; } mFirstActivePosition = firstActivePosition; //noinspection MismatchedReadAndWriteOfArray final View[] activeViews = mActiveViews; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); // Don't put header or footer views into the scrap heap if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. // However, we will NOT place them into scrap views. activeViews[i] = child; // Remember the position so that setupChild() doesn't reset state. lp.scrappedFromPosition = firstActivePosition + i; } } }獲取mActiveViews中指定位置的view,如果找到會將該view從mActiveViews中移除。position是adapter中的絕對下標值,mFirstActivePosition前面說過了,是當前可視區域的下標值,對應在adapter中的絕對值,如果找到,則返回找到的View,并將mActiveView對應的位置設置為null
/** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * 根據position在mActiveViews中查找view * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; // 找到了把該view從mActiveViews中移除,mActiveViews中的View不能重復利用 return match; } return null; }清掉當前處于transient(瞬時)狀態的所有保存的view。內部為mTransientStateViews和mTransientStateViewsById的clear()調用
/** * Dumps and fully detaches any currently saved views with transient * state. */ void clearTransientStateViews() { final SparseArray<View> viewsByPos = mTransientStateViews; if (viewsByPos != null) { final int N = viewsByPos.size(); for (int i = 0; i < N; i++) { removeDetachedView(viewsByPos.valueAt(i), false); } viewsByPos.clear(); } final LongSparseArray<View> viewsById = mTransientStateViewsById; if (viewsById != null) { final int N = viewsById.size(); for (int i = 0; i < N; i++) { removeDetachedView(viewsById.valueAt(i), false); } viewsById.clear(); } }將view放入scrapview list中,有transient狀態的view不會被scrap(廢棄),會被加入mTransientStateViewsById,mTransientStateViews 或 調用getSkippedScrap() 加入mSkippedScrap中
/** * Puts a view into the list of scrap views. * <p> * If the list data hasn't changed or the adapter has stable IDs, views * with transient state will be preserved for later retrieval. * 緩存廢棄的view,RecycleBin當中使用 mScrapViews 和 mCurrentScrap 這兩個List來存儲廢棄View * @param scrap The view to add * scrap是要添加的view * * @param position The view's position within its parent * position是view在父類中的位置 */ 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; } // 設置它的scrappedFromPosition,然后從窗口中detach該view,并根據viewType加入到mScrapView中 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. if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } 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 SparseArray<>(); } 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) { // RecyclerListener可以通過AbsListView.setRecyclerListener()設置 mRecyclerListener.onMovedToScrapHeap(scrap); } } }返回null的情況舉例: ListView第一次加載完整屏item,此時mScrapView中是沒有緩存view的,新的view將會顯示,此時listview會調用Adapter.getView,但是緩存中沒有,因此convertView是null,所以,我們得分配一塊內存來創建新的convertView;
返回scrapViews最后一個view的情況: 接著上邊的情況,我們繼續向上滾動,第一個view完全移出屏幕(假設還沒有加載新的item),此時,第一個view就會被detach,并被加入到mScrapView中;然后,我們還繼續向上滾動,直接后面又將要顯示新的item view時,此時,系統會從mScrapView中找position對應的View,顯然是找不到的,則將從mScrapView中,取最后一個緩存的view傳遞給convertView
根據position返回view: 接著上邊的情況,第一個被完全移出 加入到mScrapView中,假設此時還沒有新增的item到listview中,此時緩存中就只有第一個view;然后,現在向下滑動,則之前的第一個item,將被顯示出來,此時,從緩存中查找position對應的view有沒有,當然,肯定是找到了,就直接返回了
清空 mSkippedScrap
/** * Finish the removal of any views that skipped the scrap heap. */ void removeSkippedScrap() { if (mSkippedScrap == null) { return; } final int count = mSkippedScrap.size(); for (int i = 0; i < count; i++) { removeDetachedView(mSkippedScrap.get(i), false); } mSkippedScrap.clear(); }Move all views remaining in mActiveViews to mScrapViews. 將mActiveView中未使用的view回收(因為,此時已經移出可視區域了)。會調用mRecyclerListener.onMovedToScrapHeap(scrap);回收view的資源
scrapActiveViews()調用了該方法, pruneScrapViews() 確保mScrapViews 的數目不會超過mActiveViews的數目 (This can happen if an adapter does not recycle its views)。 mScrapView中每個ScrapView數組大小不應該超過mActiveView的大小,如果超過,系統認為程序并沒有復用convertView,而是每次都是創建一個新的view,為了避免產生大量的閑置內存且增加OOM的風險,系統會在每次回收后,去檢查一下,將超過的部分釋放掉,節約內存降低OOM風險。
/** * Makes sure that the size of mScrapViews does not exceed the size of * mActiveViews, which can happen if an adapter does not recycle its * views. Removes cached transient state views that no longer have * transient state. */將mScrapView中所有的緩存view全部添加到指定的view list中,只看到有AbsListView.reclaimViews有調用到,但沒有其它方法使用這個函數,可能在特殊情況下會使用到,但目前從framework中,看不出來。
/** * Puts all views in the scrap heap into the supplied list. */ void reclaimScrapViews(List<View> views) { if (mViewTypeCount == 1) { views.addAll(mCurrentScrap); } else { final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> scrapPile = scrapViews[i]; views.addAll(scrapPile); } } }Updates the cache color hint of all known views. 更新view的緩存顏色提示setDrawingCacheBackgroundColor。為所有的view繪置它們的背景色。
界面顯示一個view,經過三個階段:onMeasure()->onLayout()->onDraw()
onMeasure()用于測量View的大小,占用的大小通常是整個屏幕onDraw()用于將View繪制到界面上onLayout()用于確定View的布局AdapterView繼承自ViewGroup,ViewGroup通過addView()添加View,AdapterView重寫了addView()方法,禁用了該方法: android/widget/AdapterView.java中
/** * This method is not supported and throws an UnsupportedOperationException when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invoked. */ @Override public void addView(View child) { throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); }AdapterView把addView方法給禁用了,那么ListView怎么向其中添加child呢?通過onLayout中調用layoutChildren(),AbsListView的主要實現也在 onLayout中,
layoutChildren()關于RecyleBin主要干了3件事:
ListView的children放到RecycleBin中ListView清空childrenRecycleBin中緩存的view復用,變成ListView的children下面來看 onLayout 流程 
onLayout() 代碼不多,判斷 changed,如果ListView的大小或者位置發生了變化,子布局重繪,調用子類的 layoutChildren()
layoutChildren() 調用 fillFromTop() 加載 item 的布局
fillFromTop() 確定 mFirstPosition,調用 fillDown(),沒做具體加載 子布局的工作,來看 fillDown()
fillDown用子View從指定的position自上而下填充ListView,,fillUp則是自下而上填充
/** * Fills the list from pos down to the end of the list view. * * @param pos The first position to put in the list * pos 表示列表中第一個要繪制的item的position,其對應著Adapter中的索引 * * @param nextTop The location where the top of the item associated with pos * should be drawn * nextTop表示第一個要繪制的item在ListView中實際的位置, * * @return The view that is currently selected, if it happens to be in the * range that we draw. */ private View fillDown(int pos, int nextTop) { View selectedView = null; int end = (mBottom - mTop); // end是ListView底部減去頂部所得的像素值,是ListView的高度 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { end -= mListPadding.bottom; } // nextTop 是下一個子item的頭部,end是listveiw的底部的高度, // 比較nextTop 和 end,判斷是否繼續填充下一個item,nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的界面,nextTop >= end 時,子元素超出屏幕 // mItemCount 是Adapter 元素的數量,pos < mItemCount確保了我們新增的子View在Adapter中都有對應的數據源item,pos >= mItemCount 時,adapter元素都被遍歷完了 while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; // 將pos和nextTop傳遞給makeAndAddView方法 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); // child的bottom值表示的是該child的底部到ListView頂部的距離,將該child的bottom作為下一個child的top,nextTop保存著下一個child的top值 nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; // 循環一次,pos加1,position指針下移 } setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); return selectedView; }fillDown() 是加載item 布局的主要實現,while循環 保證了 只加載一屏,超出的數據不會加載
nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的界面就可以了,比如ListView的高度最多顯示10個子View,我們沒必要向ListView中加入11個子View。pos < mItemCount確保了我們新增的子View在Adapter中都有對應的數據源item,比如ListView的高度最多顯示10個子View,但是我們Adapter中一共才有5條數據,這種情況下只能向ListView中加入5個子View,從而不能填充滿ListView的全部高度。再看看 每個item的 布局 是怎么加載的
該方法會創建View, 返回這個View作為child,,并把該子View添加到ListView的children中
/** * 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. * * @param position Logical position in the list * position表示的是數據源item在Adapter中的索引 * * @param y Top or bottom edge of the view to add * y表示要生成的View的top值或bottom值 * * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * flow是true,那么y表示top值,否則表示bottom值 * * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // 如果數據沒有變化,嘗試用該position從RecycleBin的mActiveViews中獲取可復用的View // Try to use an existing view for this position child = mRecycler.getActiveView(position); // 第一次進入時 ,child 是null if (child != null) { // 如果child 不為空,說明我們找到了一個已經存在的child, // 這樣mActiveViews中存儲的View就被直接復用了 // 調用setupChild,對child進行定位 // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // 如果數據變化了,新建一個View,或者如果可能的話去ScrapView中拿一個緩存的view // Make a new view for this position, or convert an unused view if possible child = obtainView(position, mIsScrap); // 這個方法負責進行定位和量算,把View放到ListView中合適的位置 // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }makeAndAddView() 有兩種方式加載一個view,一個是 mRecycler.getActiveView(),一個是 obtainView() 第一次加載時,activeview僅僅在第一次layoutChildren中賦值且為空值,所以走 obtainView() 加載了 一個View,加載完的View傳入 setupChild() 來顯示
如果沒能夠從mActivieViews中直接復用View,那么就要調用obtainView方法獲取View,該方法嘗試間接復用RecycleBin中的mScrapViews中的View,如果不能間接復用,則創建新的View。
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { ... return transientView; } final View scrapView = mRecycler.getScrapView(position); // 嘗試獲取一個廢棄緩存中的View final View child = mAdapter.getView(position, scrapView, this); // 調用了getView方法,去初始化我們的covertView // 第一次加載時,scrapView = null ... }obtainView() 要返回一個View,這個view可能是從廢棄緩存或 Adapter的getView()中獲取的,第一次加載時 mRecycler.getTransientStateView()返回值是null,所以obtainView() 返回的是view是 mAdapter.getView()
獲取到view,makeAndAddView() 接著往下執行,調用 setupChild(),obtainView() 返回的View也傳入 setupChild() 中
setupChild() 調用 ViewGroup.addViewInLayout() 顯示 這個item的View
加載完view,ListView.layoutChildren() 都到了 RecyleBin.scrapActiveViews()
/** * Move all views remaining in mActiveViews to mScrapViews. * 將mActiveViews廢棄到mScrapViews中 */ void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean hasListener = mRecyclerListener != null; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> scrapViews = mCurrentScrap; final int count = activeViews.length; for (int i = count - 1; i >= 0; i--) { final View victim = activeViews[i]; if (victim != null) { final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) victim.getLayoutParams(); final int whichScrap = lp.viewType; activeViews[i] = null; if (victim.hasTransientState()) { // Store views with transient state for later use. victim.dispatchStartTemporaryDetach(); if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<View>(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim);// 將 victim 添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); // 將 victim 添加到 mTransientStateViews 中 } ... }RecyleBin.scrapActiveViews()中有2個重要的賦值操作,是將view添加到 瞬間狀態的數組中,瞬態的來源是view中hasTransientState方法,View.hasTransientState() 如下,其方法作用看源碼解釋就行,item中如果有view正在進行動畫之類的動態改變發生。
/** * Indicates whether the view is currently tracking transient state that the * app should not need to concern itself with saving and restoring, but that * the framework should take special note to preserve when possible. * * <p>A view with transient state cannot be trivially rebound from an external * data source, such as an adapter binding item views in a list. This may be * because the view is performing an animation, tracking user selection * of content, or similar.</p> * * @return true if the view has transient state */ @ViewDebug.ExportedProperty(category = "layout") public boolean hasTransientState() { return (mPrivateFlags2 & PFLAG2_HAS_TRANSIENT_STATE) == PFLAG2_HAS_TRANSIENT_STATE; }第一次layoutchildern 結束了

第二次加載時 recycleBin.fillActiveViews() 給activeview添加數據。將第一次加載的view放在了activeViews中 detachAllViewsFromParent() 清除掉 ListView中的子View,防止數據重復,ListView中所有的子View都是處于detach狀態,后續從RecyleBin中獲取緩存的view 進入switch (mLayoutMode),LayoutMode還是LAYOUT_NORMAL,進入default分支,getChildCount() 不再是0了,是 一屏顯示的item的個數 調用 fillSpecific() 加載view
將所有的子View從ListView中分離,也就是清空了children
/** * Detaches all views from the parent. Detaching a view should be followed * either by a call to * {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)} * or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be * temporary; reattachment or removal should happen within the same drawing cycle as * detachment. When a view is detached, its parent is null and cannot be retrieved by a * call to {@link #getChildAt(int)}. * * @see #detachViewFromParent(View) * @see #detachViewFromParent(int) * @see #detachViewsFromParent(int, int) * @see #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams) * @see #removeDetachedView(View, boolean) */ protected void detachAllViewsFromParent() { final int count = mChildrenCount; if (count <= 0) { return; } final View[] children = mChildren; mChildrenCount = 0; for (int i = count - 1; i >= 0; i--) { children[i].mParent = null; children[i] = null; } }fillSpecific()方法會優先將指定位置的子View先加載到屏幕上,然后再加載該子View往上以及往下的其它子View。那么由于這里我們傳入的position就是第一個子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,接著看 makeAndAddView()
layoutChildren() 中調用 recycleBin.fillActiveViews 緩存了子View makeAndAddView() 中 調用 mRecycler.getActiveView() 取出緩存的子View 調用 setupChild(),最后一個參數 是true,表示是從RecyleBin中取出的緩存的view 第二次 layout時,不走obtainView(),會節省一些時間
舉一個例子,假設在某一時刻ListView中顯示了10個子View,position依次為從0到9。然后我們手指向上滑動,且向上滑動了一個子View的高度,ListView需要繪制下一幀。這時候ListView在layoutChildren方法中把這10個子View都放入到了RecycleBin的mActiveViews數組中了,然后清空了children數組,然后調用fillDown方法,向ListView中依次添加position1到10的子View,在添加position為1的子View的時候,由于在上一幀中position為1的子View已經被放到mActiveViews數組中了,這次直接可以將其從mActiveViews數組中取出來,這樣就是直接復用子View,所以說RecycleBin的mActiveViews數組主要是用于直接復用的。
/** * Get the view corresponding to the specified position. The view will be removed from * mActiveViews if it is found. * * @param position The position to look up in mActiveViews * @return The view if it is found, null otherwise */ View getActiveView(int position) { int index = position - mFirstActivePosition; final View[] activeViews = mActiveViews; if (index >=0 && index < activeViews.length) { final View match = activeViews[index]; activeViews[index] = null; // 將activeview的對應item給置空,說明activeview僅僅只能使用一次 return match; } return null; }setupChild() 調用 attachViewToParent() ,將view添加到 viewGroup中
getTransientStateView() 從 mTransientStateViewsById 或 mTransientStateViews 返回一個view
mTransientStateViewsById , mTransientStateViews 在哪賦值的 scrapActiveViews() 和 addScrapView() 中
AbsListView.RecyleBin.scrapActiveViews()
/** * Move all views remaining in mActiveViews to mScrapViews. * 將mActiveViews廢棄到mScrapViews中 */ void scrapActiveViews() { ... if (victim.hasTransientState()) { // Store views with transient state for later use. victim.dispatchStartTemporaryDetach(); if (mAdapter != null && mAdapterHasStableIds) { if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<View>(); } long id = mAdapter.getItemId(mFirstActivePosition + i); mTransientStateViewsById.put(id, victim);// 將 victim 添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); // 將 victim 添加到 mTransientStateViews 中 } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { // The data has changed, we can't keep this view. removeDetachedView(victim, false); } } ... }AbsListView.RecyleBin.addScrapView()
/** * Puts a view into the list of scrap views. * <p> * 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) { ... // 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. if (mTransientStateViewsById == null) { mTransientStateViewsById = new LongSparseArray<>(); } mTransientStateViewsById.put(lp.itemId, scrap);//將 scrap添加到 mTransientStateViewsById 中 } else if (!mDataChanged) { // If the data hasn't changed, we can reuse the views at // their old positions. if (mTransientStateViews == null) { mTransientStateViews = new SparseArray<>(); } mTransientStateViews.put(position, scrap);// 將 scrap 添加到 mTransientStateViews 中 } else { // Otherwise, we'll have to remove the view and start over. getSkippedScrap().add(scrap); } } ... }transient state類似 view 的一個屬性,該屬性的作用就是標注當前view是否正在變化中。 ListView的item 在加載數據時或者 播放動畫時,view處于 瞬態 transient state。
obtainView()中final View updatedView = mAdapter.getView(position, transientView, this); mAdapter.getView() 獲得的view是靜態的view,沒有發生任何動態改變和展現任何正在發生中的動畫的item。 接著下邊 mRecycler.addScrapView(updatedView, position);添加到RecyleBin中的是這個靜態的view
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this);// 獲取一個靜態的view // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position);// 將這個靜態的view添加到RecyleBin中 } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } ... }上邊介紹了 怎么加載數據,下邊將是 ListView最神奇的部分,滑動加載更多, 比如說我們的Adapter當中有1000條數據,但是第一屏只顯示了10條,ListView中也只有10個子View而已,那么剩下的990是怎樣工作并顯示到界面上的呢? ListView 和 GridView 都支持 滑動加載數據,所以滑動的代碼在 AbsListView 中 onTouchEvent() 中代碼非常多,邏輯也復雜 滑動部分對應的 ACTION_MOVE,我們看 ACTION_MOVE 的邏輯 
ACTION_MOVE 調用了 onTouchMove()
手指上滑時,TouchMode是TOUCH_MODE_SCROLL,onTouchMove() 調用了 scrollIfNeeded()
scrollIfNeeded() 中 屏幕有移動就會調用 trackMotionScroll(),滑動屏幕時,trackMotionScroll() 會被多次調用
通過 down 來判斷 上滑 或下滑,遍歷子View,移出屏幕的子View 通過 mRecycler.addScrapView() 添加到 RecyleBin的廢棄緩存中 detachViewsFromParent() ListView 中移出屏幕的子view全detach掉 offsetChildrenTopAndBottom() 使view隨著 incrementalDeltaY 偏移,實現ListView隨著手指動的效果 listView滑到頭了,調用 fillGap(),fillGap() 是一個抽象方法,由子類實現
/** * Fills the gap left open by a touch-scroll. During a touch scroll, children that * remain on screen are shifted and the other ones are discarded. The role of this * method is to fill the gap thus created by performing a partial layout in the * empty space. * * @param down true if the scroll is going down, false if it is going up */ abstract void fillGap(boolean down);fillGap() 中調用 fillDown() 或 fillUp()
fillDown() 和 fillUp() 都調用了 makeAndAddView()
makeAndAddView() 中 調用 mRecycler.getActiveView() 返回的是 null (因為第二次加載時,已經調用了 getActiveView(), mActiveViews是不能夠重復利用的) 調用 obtainView(),setupChild()
mRecycler.getScrapView() 從廢棄緩存中獲取一個view 從緩存中拿到子View之后再調用setupChild()方法將它重新attach到ListView當中
getView()例子
public View getView(int position, View convertView, ViewGroup parent){ ViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.list_item, parent, false); holder = new ViewHolder(); holder.text = (TextView)convertView.findViewById(R.id.text); holder.icon = (ImageView)convertView.findViewById(R.id.icon); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.text.setText(DATA[position]); holder.icon.setImageBitmap(Icons[position]); return convertView;}public static class ViewHolder{ TextView text; ImageView icon;}第二個參數就是我們最熟悉的convertView呀,難怪平時我們在寫getView()方法是要判斷一下convertView是不是等于null,如果等于null才調用inflate()方法來加載布局,不等于null就可以直接利用convertView,因為convertView就是我們之間利用過的View,只不過被移出屏幕后進入到了廢棄緩存中,現在又重新拿出來使用而已。然后我們只需要把convertView中的數據更新成當前位置上應該顯示的數據,那么看起來就好像是全新加載出來的一個布局一樣,這背后的道理你是不是已經完全搞明白了?
setupChild() 調用 ViewGroup.attachViewToParent() 加載子view 
最后再來看看 RecyleBin 是怎么用的,上邊涉及到RecyleBin的操作如下圖 
下邊介紹下 ListView、AbsListView中是怎么使用RecyleBin的
如果數據沒變,調 mRecycler.getActiveView() 獲取一個active view
/** * 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. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position);// 從 RecyleBin 中獲取一個active view // 第一次進入時 ,child 是null // 第二次進入時,getActiveView()會返回一個View,child != null // 根據RecycleBin的機制,mActiveViews是不能夠重復利用的, 第二次加載時 獲取過,再調用 getActiveView 返回的是 null 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 child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; }Scroll the children by amount, adding a view at the end and removing views that fall off as necessary. 對子view滑動一定距離,添加view到底部或者移除頂部的不可見view。從注釋看,不可見的item 的自動移除是在scrollListItemsBy中進行的。
Android中view回收的計算是其父view中不再顯示的,如果scrollview中包含了一個wrap_content屬性的listview,里面的內容并不會有任何回收,引起listview 的getheight函數獲取的是一個足以顯示所有內容的高度。
當這個方法被調用時,說明Recycle bin中的view已經不可用了,那么,現在唯一的方法就是,convert一個老的view,或者構造一個新的view
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position The position to display * @param isScrap Array of at least 1 boolean, the first entry will become true if * the returned view was taken from the "temporary detached" scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); isScrap[0] = false; // Check whether we have a transient state view. Attempt to re-bind the // data and discard the view if we fail. final View transientView = mRecycler.getTransientStateView(position); if (transientView != null) { final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); // If the view type hasn't changed, attempt to re-bind the data. if (params.viewType == mAdapter.getItemViewType(position)) { final View updatedView = mAdapter.getView(position, transientView, this); // If we failed to re-bind the data, scrap the obtained view. if (updatedView != transientView) { setItemViewLayoutParams(updatedView, position); mRecycler.addScrapView(updatedView, position); } } isScrap[0] = true; // Finish the temporary detach started in addScrapView(). transientView.dispatchFinishTemporaryDetach(); return transientView; } // trackMotionScroll()中,一旦有子view移除屏幕,就加入到廢棄緩存中 final View scrapView = mRecycler.getScrapView(position); // 嘗試獲取一個廢棄緩存中的View final View child = mAdapter.getView(position, scrapView, this); // 通過 Adapter.getView() 獲取一個 View // 第一次加載時,scrapView = null if (scrapView != null) { if (child != scrapView) { // 如果重用的scrapView和adapter獲得的view是不一樣的,將scrapView進行回收 // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position);// 將 scrapView 回收 } else { // 如果重用的view和adapter獲得的view是一樣的,將isScrap[0]值為true,否則默認為false if (child.isTemporarilyDetached()) { isScrap[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } else { // we set isScrap to "true" only if the view is temporarily detached. // if the view is fully detached, it is as good as a view created by the // adapter isScrap[0] = false; } } } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } setItemViewLayoutParams(child, position); if (AccessibilityManager.getInstance(mContext).isEnabled()) { if (mAccessibilityDelegate == null) { mAccessibilityDelegate = new ListItemAccessibilityDelegate(); } if (child.getAccessibilityDelegate() == null) { child.setAccessibilityDelegate(mAccessibilityDelegate); } } Trace.traceEnd(Trace.TRACE_TAG_VIEW); return child; }新聞熱點
疑難解答