這是RecyclerView
緩存機制系列文章的第三篇,系列文章的目錄如下:
- RecyclerView緩存機制(咋復用冈在?)
- RecyclerView緩存機制(回收些啥倒慧?)
- RecyclerView緩存機制(回收去哪?)
- RecyclerView緩存機制(scrap view)
上一篇文章講述了“從哪里獲得回收的表項”,這一篇會結合實際回收場景分析下“回收哪些表項纫谅?”炫贤。
(ps: 下文中的 粗斜體字 表示引導源碼閱讀的內心戲)
回收場景
在眾多回收場景中最顯而易見的就是“滾動列表時移出屏幕的表項被回收”。滾動是由MotionEvent.ACTION_MOVE
事件觸發(fā)的付秕,就以RecyclerView.onTouchEvent()
為切入點尋覓“回收表項”的時機:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
@Override
public boolean onTouchEvent(MotionEvent e) {
...
case MotionEvent.ACTION_MOVE: {
...
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
...
}
} break;
...
}
}
去掉了大量位移賦值邏輯后兰珍,一個處理滾動的函數(shù)出現(xiàn)在眼前:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
...
@VisibleForTesting LayoutManager mLayout;
...
boolean scrollByInternal(int x, int y, MotionEvent ev) {
...
if (mAdapter != null) {
...
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
...
}
...
}
RecyclerView
把滾動交給了LayoutManager
來處理,于是移步到最熟悉的LinearLayoutManager
:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
...
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
//更新LayoutState(這個函數(shù)對于“回收哪些表項”來說很關鍵询吴,待會會提到)
updateLayoutState(layoutDirection, absDy, true, state);
//滾動時向列表中填充新的表項
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
...
}
沿著調用鏈往下找掠河,發(fā)現(xiàn)了一個上一篇中介紹過的函數(shù)LinearLayoutManager.fill()
,原來列表滾動的同時也會不斷的向其中填充表項(想想也是猛计,不然怎么會不斷有新的表項出現(xiàn)呢~)唠摹。上一遍只關注了其中填充的邏輯,但其實里面還有回收邏輯:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//不斷循環(huán)獲取新的表項用于填充奉瘤,直到沒有填充空間
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//填充新的表項
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
//在當前滾動偏移量基礎上追加因新表項插入增加的像素(這句話對于“回收哪些表項”來說很關鍵勾拉,待會會提到)
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//回收表項
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
...
}
...
return start - layoutState.mAvailable;
}
}
在不斷獲取新表項用于填充的同時也在回收表項(想想也是,列表滾動的時候有表項插入的同時也有表項被移出)盗温,移步到回收表項的函數(shù):
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
...
/**
* Recycles views that went out of bounds after scrolling towards the end of the layout.
* 當向列表尾部滾動時回收滾出屏幕的表項
* <p>
* Checks both layout position and visible position to guarantee that the view is not visible.
*
* @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
* @param dt This can be used to add additional padding to the visible area. This is used
* to detect children that will go out of bounds after scrolling, without
* actually moving them.(該參數(shù)被用于檢測滾出屏幕的表項)
*/
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
if (dt < 0) {
if (DEBUG) {
Log.d(TAG, "Called recycle from start with a negative value. This might happen"
+ " during layout changes but may be sign of a bug");
}
return;
}
// ignore padding, ViewGroup may not clip children.
final int limit = dt;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
} else {
//遍歷LinearLayoutManager的孩子找出其中應該被回收的
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
//回收索引為0到i-1的表項
recycleChildren(recycler, 0, i);
return;
}
}
}
}
...
}
原來RecyclerView
的回收分兩個方向:1. 從列表頭回收 2.從列表尾回收藕赞。就以“從列表頭回收”為研究對象分析下RecyclerView
在滾動時到底是怎么判斷“哪些表項應該被回收?”卖局。
(“從列表頭回收表項”所對應的場景是:手指上滑斧蜕,列表向下滾動,新的表項逐個插入到列表尾部砚偶,列表頭部的表項逐個被回收惩激。)
回收哪些表項
要回答這個問題,剛才那段代碼中套在recycleChildren(recycler, 0, i)
外面的判斷邏輯是關鍵:mOrientationHelper.getDecoratedEnd(child) > limit
蟹演。
/**
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
* 該類用于幫助LayoutManger抽象出基于視圖方向的測量
* <p>
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
* can also be used to abstract calls around view bounds and child measurements with margins and
* decorations.
*
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
* @see #createVerticalHelper(RecyclerView.LayoutManager)
*/
public abstract class OrientationHelper {
...
/**
* Returns the end of the view including its decoration and margin.
* <p>
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
* decoration and 3px right margin, returned value will be 205.
*
* @param view The view element to check
* @return The last pixel of the element
* @see #getDecoratedStart(android.view.View)
*/
public abstract int getDecoratedEnd(View view);
...
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
...
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
...
}
結合注釋和該方法的實現(xiàn)风钻,原來mOrientationHelper.getDecoratedEnd(child)
表示當前表項的尾部相對于列表頭部的坐標,OrientationHelper
這層抽象屏蔽了列表的方向酒请,所以這句話在縱向列表中可以翻譯成“當前表項的底部相對于列表頂部的縱坐標”骡技。
判斷條件mOrientationHelper.getDecoratedEnd(child) > limit
中的limit
又是什么鬼?在縱向列表中羞反,“表項底部縱坐標 > 某個值”意味著表項位于某條線的下方布朦,回看一眼“回收表項”的邏輯:
//遍歷LinearLayoutManager的孩子找出其中應該被回收的
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//直到表項底部縱坐標大于某個值后,回收該表項以上的所有表項
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
//回收索引為0到索引為i-1的表項
recycleChildren(recycler, 0, i);
return;
}
}
隱約覺得limit
應該等于0昼窗,這樣不正好是回收所有從列表頭移出的表項嗎是趴?不知道這樣YY到底對不對,還是沿著調用鏈向上找一下limit
被賦值的地方吧~澄惊,調用鏈很長唆途,就不全部羅列了富雅,但其中有兩個關鍵點,其實我在上面的代碼中埋了伏筆肛搬,現(xiàn)在再羅列一下:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
//1. 更新LayoutState(這個函數(shù)對于“回收哪些表項”來說很關鍵没佑,待會會提到)
updateLayoutState(layoutDirection, absDy, true, state);
//滾動時向列表中填充新的表項
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
}
...
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
//不斷循環(huán)獲取新的表項用于填充,直到沒有填充空間
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
//2. 在當前滾動偏移量基礎上追加因新表項插入增加的像素(這句話對于“回收哪些表項”來說很關鍵温赔,待會會提到)
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//回收表項
recycleByLayoutState(recycler, layoutState);
}
...
}
...
return start - layoutState.mAvailable;
}
...
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...
int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
mLayoutState.mExtra += mOrientationHelper.getEndPadding();
//獲得當前方向上里列表尾部最近的孩子(最后一個孩子)
final View child = getChildClosestToEnd();
// the direction in which we are traversing children
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// calculate how much we can scroll without adding new children (independent of layout)
// 獲得一個滾動偏移量蛤奢,如果只滾動了這個數(shù)值那不需要添加新的孩子
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
...
}
...
//對mLayoutState.mScrollingOffset賦值
mLayoutState.mScrollingOffset = scrollingOffset;
}
}
一圖勝千語:
關于limit
等于0的YY破滅了,其實limit
是一根橫謂語列表中間的橫線陶贼,它的值表示這一次滾動的總距離啤贩。(圖中是一種理想情況,即當滾動結束后新插入表項的底部正好和列表底部重疊)其實回收表項的時機是在滾動真正發(fā)生之前拜秧,此時我們預先計算出滾動的偏移量痹屹,根據(jù)偏移量篩選出滾動發(fā)生后應該被刪除的表項。即 limit
這根線也可以表述為:當滾動發(fā)生后腹纳,列表當前 limit
這個位置會成為列表的頭部
分析完“回收哪些表項”后痢掠,一不小心發(fā)現(xiàn)篇幅有點長了驱犹,那關于回收去哪里嘲恍?將放到下一篇在講。